Leon's Blogging

Coding blogging for hackers.

Vue

| Comments

剛好最近在玩 vue 就來記錄一下

參考文件:

Javascript:

Vue - Week1 Todo list

之前就有玩過 vue 不過太久沒碰,就變得生疏,剛好這次同事有開 vue 的 bookclub,就再來複習一下吧~

vs code 套件

Vetur
Vue 2 Snippets

環境建置

1. 安裝 vue-cl
// -g 為 global 的意思,沒有加的話,會裝在當下的
npm install -g vue-cli
yarn global add vue-cli
2. 建立專案
vue init webpack vue-book-club
3.專案選項
? Project name vue-book-club // 直接enter
? Project description A Vue.js project // 直接enter
? Author GuaHsu <guaswork@gmail.com> // 直接enter
? Vue build standalone
// ❯ Runtime + Compiler: recommended for most users
//   Runtime-only: about 6KB lighter min+gzip, but templates (or any Vue-specific HTML) are ONLY allowed in .vue files - render functions are required elsewhere
? Install vue-router? Yes
? Use ESLint to lint your code? Yes  // [可選]程式碼規範,建議安裝
? Pick an ESLint preset Standard
// ❯ Standard (https://github.com/standard/standard)
//   Airbnb (https://github.com/airbnb/javascript)
//   none (configure it yourself)
? Set up unit tests Yes // [可選]建議先裝JEST日後有空可讀這個
? Pick a test runner jest
// ❯ Jest
//   Karma and Mocha
//   none (configure it yourself)
? Setup e2e tests with Nightwatch? Yes // [可選]建議先裝起來,日後有空可讀這個
? Should we run `npm install` for you after the project has been created? (recommended) npm // [可選] npm or yarn
// ❯ Yes, use NPM
//   Yes, use Yarn
//   No, I will handle that myself
4.安裝套件
  • Sass(Scss) - for css 建議使用
npm install sass sass-loader node-sass --save
yarn add sass sass-loader node-sass
  • Pug - for html 可選用
npm install pug pug-loader pug-filters --save
yarn add pug pug-loader pug-filters
  • 安裝 package.json 內其它套件
npm install
yarn install
5. 修改一下eslint規則 - for Prettier code / standard

Prettier code 目前不支援 standard 的 function 空格設定所以改一下

// vue-book-club/.eslintrc.js
rules: {
    // allow async-await
    'generator-star-spacing': 'off',
    // allow debugger during development
    'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
    // 新增的是下面這兩個
    'space-before-function-paren': 0,
    'new-cap': [
      'error', {
        'properties': false
      }
    ]
  }

TODO List

vue 主要都是在 src 的資料夾底下

src
├── App.vue
├── assets
│   └── logo.png
├── components
│   ├── HelloWorld.vue
│   └── TodoList.vue
├── main.js
└── router
    └── index.js
使用 v-router
import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld'
// 這邊的 @ 指的是 src/

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      name: 'HelloWorld',
      component: HelloWorld
    }
  ]
})

// name 主要在之後要 mapping 可以用 name 去對應
// component 對應最上面 import 進來的

vue 的一個 component 大致上分

  • template - html
  • script - js
  • style - css
template
<template>
  <div id="TodoList">
    <div class="top-area">
      <input class="top-area-input" type="text" v-model.trim="todoMessage">
      <button class="top-area-add" @click="addTodo">Add Todo</button>
      <div class="top-area-info">
        共有:  筆資料,有  筆完成
      </div>
      <div v-if="isMaxLimit" class="top-area-info--full">已達新增上限!v-if</div>
      <div v-show="isMaxLimit" class="top-area-info--full">已達新增上限!v-show</div>
    </div>
    <div class="content">
      <ul class="todolist">
        <li v-for="(todo, index) in todoList" :key="index" class="todolist-item">
          <input :id="`todo-${index}`" type="checkbox" v-model="todo.checked">
          <label :for="`todo-${index}`" :class="[{ 'todolist-item--checked': todo.checked }]"></label>
          <button class="todolist-item-remove" @click="removeTodo(index)">remove</button>
        </li>
      </ul>
    </div>
  </div>
</template>
v-model

設定 data

<input class="top-area-input" type="text" v-model.trim="todoMessage">
// trim 去掉前後多於的空白
v-bind

屬性繫結,綁在 HTML 上的屬性 (attribute)

<label :for="`todo-${index}`" :class="[{ 'todolist-item--checked': todo.checked }]"></label>

// : = v-bind
// :class = "{checked: true}" 後面是 true 才會吃
// 複數加 [], :class = [anotherClass, "{checked: true}"]
// 字串裡塞變數,必須外面包 ``,變數用 ${}
v-on

在 click 的時候觸發 addTodo method

<button class="top-area-add" @click="addTodo">Add Todo</button>
// @ = v-on

v-for
<li v-for="(todo, index) in todoList" :key="index" class="todolist-item">
// 可識別唯一性的 key,可用來排序
v-show, v-if
<div v-if="isMaxLimit" class="top-area-info--full">已達新增上限!v-if</div>
// 要是 true 的時候才會有畫面,並且 dom 會長出來
<div v-show="isMaxLimit" class="top-area-info--full">已達新增上限!v-show</div>
// 如果是 false 就是畫面隱藏,dom 還在
// 另外還有 v-else v-else-id,同一組必須寫在一起
script
export default {
  name: 'TodoList',
  data() {
    return {
      maxLimit: 3,
      todoMessage: '',
      todoList: [{ name: 'test', checked: false }]
    }
  },
  computed: {
    checkedItemCount() {
      return this.todoList.filter(todo => todo.checked)
    },
    isMaxLimit() {
      return this.todoList.length === this.maxLimit
    }
  },
  watch: {
    // todoMessage (newVal, oldVal) {
    //   console.log(newVal, oldVal)
    // }
    // todoList: {
    //   handler(newVal, oldVal) {
    //     console.log(newVal, oldVal)
    //     this.checkedItemCount = this.todoList.filter(todo => todo.checked)
    //   },
    //   deep: true
    // }
  },
  methods: {
    addTodo() {
      if (!this.isMaxLimit && this.todoMessage) {
        this.todoList.push({
          name: this.todoMessage,
          checked: false
        })
        this.todoMessage = ''
      }
    },
    removeTodo(index) {
      this.todoList.splice(index, 1)
    }
  }
}
</script>

// computed 可當作一種屬性,當裡面值有變動會跟著變動
// watch 不能 watch array object 必須用 deep watch (比較耗效能)
// methods 可以 call 的 method
style
<style lang="scss" scoped>
#TodoList {
  * {
    user-select: none;
  }
  .top-area {
    &-info {
      &--full {
        color: pink;
      }
    }
  }
  .todolist {
    &-item {
      &--checked {
        text-decoration: line-through;
      }
    }
  }
}
</style>

/*
scoped 為了將每個 components 會產生亂數的屬性 data-v-3de47834,這樣每個 components 就都可以用同樣的 class & id name
*/
run
npm run dev
yarn run start

範例

官方文件


Vue - Week 2 Lifecycle & nvm

npm 之前有寫過 Ruby on Rails 裝機趴 (Only Mac)

所以主要是 vue 的 life cycle

Instance Lifecycle Hooks

Vue 提供了每個 liftcycle 的各種 hook,可以藉由這些 hook,在不同階段,處理不同事情

var vm = new Vue({
  beforeCreate: function() {
    //vue instance 被 constructor 建立前
    console.log('beforeCreate');
  },
  created: function() {
    //vue instance 被 constructor 建立後,在這裡完成 data binding
    // call API 可以在這邊處理好,這樣 DOM 綁定前就可以拿到資料
    console.log('created');
  },
  beforeMount: function() {
    //繫結 DOM 之前
    console.log('beforeMount');
  },
  mounted: function() {
    //繫結 DOM 之後
    console.log('mounted');
  },
  beforeUpdate: function() {
    //資料更新,但尚未更新 DOM
    console.log('beforeUpdate');
  },
  updated: function() {
    //因資料更新,而更新 DOM
    console.log('updated');
  },
  beforeDestroy: function() {
    //移除 vue instance 之前
    console.log('beforeDestroy');
  },
  destroyed: function() {
    //移除 vue instance 之後
    console.log('destroyed');
  }
});
  • 一般狀況使用v-if和v-for來控制資料即可,不需要用到vm.$destroy()。
  • 如果是使用 server-side rendering,則除了beforeCreate和created,其他 method 都不會被呼叫。
// 官方解釋: 在下次 DOM 更新循環結束之後執行延遲回調。在修改數據之後立即使用這個方法,獲取更新後的 DOM。
// nextTick 可以到下一個階段再去執行,比如說等 update 完 dom 的值,再透過 reference 將 dom 的值拉下來
this.$nextTick(() => {
  this.msg = this.$refs.msgDiv.innerHTML
})

範例

參考文件

Vue - Week3 Component

主要介紹如何使用 component 來架構頁面

透過 import 將要使用的 components 引入,並放置在 script 裡面的 components

父層

使用與 script 裡面引入同名的 components 當作 tag 來使用 TabA TabB TabC

<template>
  <div id="W3Index">
    <h1>Index </h1>
    <h2>ChildMessage: </h2>
    <h2></h2>
    <input v-model="message" type="text">
    <div class="tab-buttons">
      <span
        v-for="(tab, index) in tabs"
        @click="tabIndex = index"
        :class="{ 'active': tabIndex === index }"
        :key="index">
        
      </span>
    </div>
    <div class="tab-content">
      <!-- data 是由 TabA 作定義的 props 屬性 -->
      <TabA v-show="tabIndex === 0" :data="obj"></TabA>
      <TabB v-show="tabIndex === 1" :data="childMessage">
        <!-- mapping component 的 slot 名稱,就可將 template 傳到下層 -->ㄋ
        <div slot="content">
          <div style="width: 100px; height: 100px; border: solid 1px #333"> I am slot </div>
        </div>
      </TabB>
      <!-- 監聽 childMessage,監聽到就執行 getChildMessage -->
      <TabC v-show="tabIndex === 2" :message="message" @childMessage="getChildMessage"></TabC>
    </div>
  </div>
</template>

透過 import 將需要的 component 引入,並放進 components object

<script>
import TabA from '@/components/week3/TabA'
import TabB from '@/components/week3/TabB'
import TabC from '@/components/week3/TabC'
export default {
  name: 'W3Index',
  components: {
    // es6 於許 key value 一樣時,只需要寫一次,如果不一樣就必須都寫 TabZ: TabA
    TabA,
    TabB,
    TabC
  },
  data() {
    return {
      message: '',
      childMessage: '',
      obj: {
        name: 'AAAAA',
        value: 123
      },
      tabIndex: 0,
      tabs: [{ name: 'TabA' }, { name: 'TabB' }, { name: 'TabC' }]
    }
  },
  methods: {
    getChildMessage(msg) {
      this.childMessage = msg
    }
  }
}
</script>

子層

  • props 可接收上層給的 data,如果子層需要更改到資料,必須使用 deep-copy,否則像 obejct arrayreference 就會更改到
  • slot 可接收上層給的 template
  • $emit 可回傳給上層的 event,上層必須有人接收,並且上層接收後執行的 method,自動會將 $emit 後面的參數傳進去

deep-copy: JSON.parse(JSON.stringify(this.data))

TabA
<template>
  <div id="TabA">
    <h1>TabA</h1>
    <h2></h2>
    <button @click="changeChildObj">changeChildObj</button>
  </div>
</template>
<script>
export default {
  name: 'TabA',
  // props: ['data'], 可以這樣寫需要的屬性,但就沒辦法限制 type 等等..
  props: {
  // 這樣寫就可以限制每個屬性要的條件
    data: {
      type: Object,
      default: () => { return {} },
      required: false
    }
  },
  data() {
    return {
      // 要改 props 資料,必須做深拷貝,不然原本 object 會 reference 上一層的資料
      childData: JSON.parse(JSON.stringify(this.data))
    }
  },
  methods: {
    changeChildObj() {
      this.childData.name = 'BBBBBB'
    }
  }
}
</script>
TabB
<template>
  <div id="TabB">
    <h1>TabB</h1>
    <h2></h2>
    <!-- slot 設定,外面必須 name 也是 content -->
    <slot name="content"></slot>
  </div>
</template>
<script>
export default {
  name: 'TabB',
  props: ['data', 'data2'],
  data() {
    return {
    }
  }
}
</script>
TabC
<template>
  <div id="TabC">
    <h2>TabC</h2>
    <input v-model="childMessage" type="text">
    <h3 :class="{'hasPink': hasPink}">Message: </h3>
    <button @click="emitMsg">emit</button>
  </div>
</template>
<script>
export default {
  name: 'TabC',
  props: {
    message: {
      type: String,
      default: '',
      required: true
    }
  },
  data() {
    return {
      childMessage: ''
    }
  },
  computed: {
    hasPink() {
      return this.message.match(/pink/gi)
    }
  },
  methods: {
    emitMsg() {
      // 傳回上一層,上一層必須有在監聽 childMessage
      this.$emit('childMessage', this.childMessage)
    }
  }
}
</script>
<style lang="scss">
#TabC {
  .hasPink {
    color: pink;
  }
}
</style>

範例

參考文件


Week4 - Vuex

yarn add vuex

照慣例會將 vux 統一放在 src/store/ 裡面

因此在 main.js

import store from './store'

new Vue({
  el: '#app',
  router,
  store,
  components: { App },
  template: '<App/>'
})

預設都會抓 index.js

// src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

// 第一種方式全部都分開
import { state, mutations } from './mutations.js'
import * as getters from './getters.js'
import * as actions from './actions.js'

// 第二種方式全都放在一起
import shop from './modules/shop'

Vue.use(Vuex)

export default new Vuex.Store({
  state,
  mutations,
  getters,
  actions,
  modules: {
    shop
  },
  // 嚴格模式,禁止直接修改 state
  strict: true
})
  • State: 所有資料狀態存取的地方,
  • Getter 給前端資料 (像是計算屬性 compute)
  • Mutation: 更改資料只能用 commit 的方式透過 mutation 來更改
    • 必須是同步函數
  • Action 類似於 mutation,不同在於:
    • Action 提交的是 mutation,而不是直接變更狀態。
    • Action 可以包含任意非同步操作。
// src/store/modules/shop.js

import shopAPI from '@/api/shop'

// 命名 muation 觸發的名稱,方便管理
const types = {
  ADD_CART_ITEM: 'ADD_CART_ITEM',
  REMOVE_CART_ITEM: 'REMOVE_CART_ITEM',
  SET_PRODUCT_LIST_DATA: 'SET_PRODUCT_LIST_DATA',
  SET_COUNTRY: 'SET_COUNTRY'
}

// 拿資料可以透過 state 或是 getter 來拿,要怎麼組合就看自己
// 也可以資料透過 state 要計算的資料就透過 getter 去拿取
const state = {
  country: 'tw',
  cartItems: [],
  productListData: []
}

const getters = {
  country: state => state.country,
  productListData: state => state.productListData,
  cartItemCount: state => state.cartItems.length,
  cartItemTotalAmount: state => state.cartItems.reduce((total, product) => total + product.price, 0)
}

// actions 主要是執行一些非同步,最後更改資料還是要透過 mutation
// 跟 state 和 getter 一樣,都可以組合

const actions = {
  setCountry({ commit }, country) {
    commit(types.SET_COUNTRY, country)
  },
  getProductListData({ commit }, data) {
    shopAPI.getProductData().then(res => {
      commit(types.SET_PRODUCT_LIST_DATA, res)
    })
  },
  addCartItem({ commit }, data) {
    // if 庫存檢查 then
    commit(types.ADD_CART_ITEM, data)
    // else alert('庫存不足)
  },
  removeCartItem({ commit }, index) {
    commit(types.REMOVE_CART_ITEM, index)
  }
}

const mutations = {
  [types.ADD_CART_ITEM](state, data) {
    state.cartItems.push(data)
  },
  [types.REMOVE_CART_ITEM](state, index) {
    state.cartItems.splice(index, 1)
  },
  [types.SET_PRODUCT_LIST_DATA](state, data) {
    state.productListData = data
  },
  [types.SET_COUNTRY](state, country) {
    state.country = country
  }
}

export default {
// 宣告 namespaced true 在 import 時就必須指定 file 名稱
  namespaced: true,
  state,
  getters,
  actions,
  mutations
}
// src/components/ProductList.vue
<template>
  <div id="Index">
    <AppHeader></AppHeader>
    <h1 v-if="!productListData.length">讀取中</h1>
    <div class="product" v-for="product in productListData" :key="product.id">
      <img class="product-img" src="@/assets/logo.png">
      <div class="product-name"></div>
      <div class="product-price"></div>
      <div class="product-add" @click="addToCart(product)">加入購物車</div>
    </div>
  </div>
</template>

<script>
// import shopAPI from '@/api/shop'
import AppHeader from './AppHeader'
// 先 iport 進來才可以使用
import { mapActions, mapGetters } from 'vuex'
export default {
  name: 'ProductList',
  components: {
    AppHeader
  },
  data() {
    return {
      productList: []
    }
  },
  computed: {
       // states 和 getters 寫在 computed 裡面
    ...mapGetters('shop', ['productListData'])
  },
  created() {
    this.getProductListData()
    // shopAPI.getProductData().then(res => {
    //   this.productList = res
    // })
  },
  methods: {
  // actions 和 mutations 寫在 methods 裡面
    ...mapActions('shop', ['addCartItem', 'getProductListData']),
    addToCart(product) {
      this.addCartItem(product)
      // 原本 mutation 觸發的寫法
      // this.$store.commit('shop/ADD_CART_ITEM', product)
      // 原本 action 觸發的寫法
      // this.$store.dispatch('shop/addCartItem', product)
    }
  }
}
</script>

參考文件

Comments