iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 21
0
Modern Web

用範例理解 Vue.js系列 第 21

用範例理解 Vue.js #21:Vuex in Vuetify

Imgur

圖片來源

延續之前的主題 Vuetify 中用到的技術大方向有:

  • vue-router
  • vuex
  • vue-server-renderer
  • webpack
  • express

今天要分享的是 Vuex,並且解釋在 Vuetify 的模板 WebpackSSR 是如何使用 Vuex。

還沒看前幾篇可以先從 Day19: Vuetify 開始閱讀。

Vuex 是什麼?

Vuex is a state management pattern + library for Vue.js applications.

Vuex 是一個專門為 Vue.js 開發的狀態管理模式,採用集中式儲存管理應用的所有 Vue Instance 或 Component 的狀態,並以相應的規則保證狀態以可預期的方式變化。

什麼情況下應該使用 Vuex?

當很多的 Component 需要共用同一個狀態的時候,或者是打算開發大型的 SPA 時,可以考慮使用 Vuex。

但如果只是簡單的應用,基本上用 global event bus 的方法即可。

Vuex 快速開始

直接看一個範例,記得要先引入 vue.js 和 vuex。
透過 new Vuex.Store({ options }) 建立一個 storestore即為一個 Vuex 應用的核心,也可以想像成一個容器,內容包含了應用程式中大部分的狀態。

創建的過程非常簡單,這個範例僅提供了一個初始的statemutations

<div id="app">
  <p>{{ count }}</p>
  <p>
    <button @click="increment">+</button>
    <button @click="decrement">-</button>
  </p>
</div>
// make sure to call Vue.use(Vuex) if using a module system

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment: state => state.count++,
    decrement: state => state.count--
  }
})

new Vue({
  el: '#app',
  computed: {
    count () {
      return store.state.count
    }
  },
  methods: {
    increment () {
      store.commit('increment');

      // 也可以直接修改但這樣就喪失使用 Vuex的意義
      // store.state.count++
    },
    decrement () {
      store.commit('decrement');

      // 也可以直接修改但這樣就喪失使用 Vuex的意義
      // store.state.count--
    }
  }
})

附上 codepen 範例 Vuex: Quick Start

注意:

  • Vuex 的狀態儲存是響應式的,當store中的狀態發生變化時,那麼有依賴這些狀態的 Vue Component 也會跟著高效更新。
  • 唯一更改store狀態的方式是透過(commit)mutation,雖然我們可以透過store.state.count直接更改store當中的狀態,但這是不對的。
    因為通常我們會透過actions來提交mutation和處理非同步操作,若直接以修改store.state.count的方式,將會喪失了使用 Vuex 的核心意義。

看完範例再強調一次:

Vuex is a state management pattern + library for Vue.js applications.

很重要所以寫兩次的原因在於,我個人糾結於官網的這段話:

你不能直接改變 store 中的狀態。改變 store 狀態的唯一途徑就是提交 (commit) mutation。這樣使得我們可以方便地跟踪每一個狀態的變化,從而讓我們能夠實現一些工具幫助我們更好地了解我們的應用。

但實際上卻可以直接透過store.state.count更改狀態,有潔癖的我天真的以為這樣的寫法應該是要報錯的啊。

深深地認為官網這段話是否不夠精確,或者是mutation背後有甚麼我不明白的機制?
歡迎打臉。

更正(2018/01/05):

Vuex 有提供一個嚴格模式,可使直接修改store.state.count時報錯誤訊息。

簡單修改上面的範例如下:

const store = new Vuex.Store({
  stict: true,
  state: {
    count: 0
  },
  mutations: {
    increment: state => state.count++,
    decrement: state => state.count--
  }
})

new Vue({
  el: '#app',
  computed: {
    count () {
      return store.state.count
    }
  },
  methods: {
    increment () {
      store.state.count++
    },
    decrement () {
      store.state.count--
    }
  }
})

附上 codepen 範例 Vuex: Strict

Vuex 核心概念

Quick View:

  • state: 類似 vue 的 data。
  • getter: 類似 vue 的 computed。
  • mutations: 唯一更改 state 的方式。
  • actions: 透過 dispatch 執行 action,提交 mutation,並處理非同步操作。
  • modules: 當應用程式過於複雜時,可將 store 分割為 module。

state

Vuex 使用單一狀態樹,用一個物件包含所有應用的狀態,因此 Vuex 以一個「唯一數據源(SSOT)」而存在。也意味著,每個 Vue Instance 僅包含一個 store 實例。

注意:

使用 Vuex 並不意味著你需要將所有的狀態放入 Vuex。雖然將所有的狀態放到 Vuex 會使狀態變化更顯式和易調試,但也會使代碼變得冗長和不直觀。如果有些狀態嚴格屬於單個組件,最好還是作為組件的局部狀態。你應該根據你的應用開發需要進行權衡和確定。

另外,當一個 Component 需要獲取許多狀態的時候,可以透過 mapState 簡化程式碼,這個部分就不多做解釋,可以直接去官網看。

getter

可以把 Vuex 的 getter 想像成 Vue 的 computed。getter接受state作為第一個參數,且getter回傳的值會根據他依賴的state被更改時重新計算。而getter也可以接受其他getter作為第二個參數。

例如:

在 Vuex 實例中定義 getters:

getters: {
  doneTodos: state => {
    return state.todos.filter(todo => todo.done)
  },
  doneTodosCount: (state, getters) => {
    return getters.doneTodos.length
  }
}

在 Vue 實例中使用 getters:

computed: {
  doneTodosCount () {
    return this.$store.getters.doneTodosCount
  }
}

也可以透過讓getter回傳一個函式,實現給getters傳入參數的功能,例如:

getters: {
  // ...
  getTodoById: (state) => (id) => {
    return state.todos.find(todo => todo.id === id)
  }
}
store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false }

另外,當一個 Component 需要獲取許getters的時候,可以透過 mapGetters 簡化程式碼,這個部分就不多做解釋,可以直接去官網看。

mutations

更改 Vuex 的 store 中的狀態的唯一方法是提交 mutation。

const store = new Vuex.Store({
  state: {
    count: 1
  },
  mutations: {
    increment (state) {
      state.count++
    }
  }
})

可以透過store.commit('increment')執行 mutation。

另外,也可以對store.commit傳入參數,而這個參數通常為一個物件包含所需的值。

例如:

mutations: {
  increment (state, payload) {
    state.count += payload.amount
  }
}

store.commit('increment', {
  amount: 10
})

actions

actions 類似於 mutations,不同在於:

  • Action 提交的是 mutation,而不是直接變更狀態。
  • Action 可以包含任意異步操作。

Action 函數接受一個與store實例具有相同方法和屬性的context對象,因此可以利用context.commit提交一個mutation。

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    increment (context) {
      context.commit('increment')
    }
  }
})

可以利用 ES6 的解構來簡化以上程式碼:

actions: {
  increment ({commit}) {
    commit('increment')
  }
}

可以透過store.dispatch('increment')執行 action。

modules

由於 Vuex 使用單一狀態樹,因此當應用變得非常複雜時,store物件就會變得非常肥大難以管理,因此可以將 store 分割成多個 module,每個 module 各自用有各自的 state, getters, mutations, actions,甚至可以再模塊中再引入子模塊。

例如:

const moduleA = {
  state: { ... },
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const moduleB = {
  state: { ... },
  mutations: { ... },
  actions: { ... }
}

const store = new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB
  }
})

store.state.a // -> moduleA.state
store.state.b // -> moduleB.state

個人還沒使用過 module,就不多加贅述,有興趣自己到官網看看。

Vuex in Vuetify template WebpackSSR

在對於 Vuex 有基本的了解後,可以來解釋一下在 Vuetify 的模組 WebpackSSR 中,如何使用 Vuex。

首先回顧一下 WebpackSSR 的目錄結構,方便對照:

  • projectName
    • assets
    • build
    • components
    • mixins
    • pages
    • router
    • static
    • store

在 Vuetify WebpackSSR(以下簡稱VW)的目錄結構中,所有關於 vuex 的東西都放在 store 資料夾。透過store中的 index.js 建立 createStore,並在assets中的app.js引入,並且將實體化的Store物件作為屬性傳入 Vue instance 中。

store/index.js

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export function createStore () {
  return new Vuex.Store({
    state: {},

    actions: {},

    mutations: {},

    getters: {}
  })
}

VW 這個模板中只有初始化 Vuex,而其中的內容都交由開發者自定義,因此 state, actions, mutations, getters 都暫時為空物件。

assets/app.js

import Vue from 'vue'
import Vuetify from 'vuetify'
import 'vuetify/dist/vuetify.css'
import App from './App.vue'
import Components from 'components/_index'

import { createStore } from 'store/index'
import { createRouter } from 'router/index'
import { sync } from 'vuex-router-sync'

Vue.use(Vuetify)

Object.keys(Components).forEach(key => {
  Vue.component(key, Components[key])
})

export function createApp (ssrContext) {
  const store = createStore() // 實體化 vuex
  const router = createRouter() // 實體化 router = new Router({options})

  sync(store, router)
  // 透過 vuex-router-sync 同步 vuex 和 vue-router,會同步的屬性有 {
  //   path: '',
  //   query: null,
  //   params: null
  // }
  // 主要是將 vue-router 的狀態放進 vuex 的 state 中,透過改變 state 進行路由的操作。
  // 這使我們可以輕鬆透過以下方法取得或操作路由設定:
  // store.state.route.path   // current path (string)
  // store.state.route.params // current params (object)
  // store.state.route.query  // current query (object)

  const app = new Vue({
    router,
    store,
    ssrContext, // 之後篇幅會補充
    render: h => h(App)
    // h => h(App)中的App為資料夾assets中的App.vue,App.vue是這整個SPA最核心基礎的頁面組成。

  })
  // 將 router, store, ssrContext, render 傳入選項物件中

  return { app, router, store }
}

參考資料


上一篇
用範例理解 Vue.js #20:vue-router in Vuetify
下一篇
用範例理解 Vue.js #22:vue-server-renderer in Vuetify
系列文
用範例理解 Vue.js30

1 則留言

1
Kuro Hsu
iT邦新手 4 級 ‧ 2018-01-05 11:46:21

在 Vuex 裡面有個東西叫「嚴格模式

const store = new Vuex.Store({
  // ...
  strict: true
});

開啟嚴格模式後,當你在 methods 直接對 store.state 操作而不是透過 mutation 處理的時候就會報錯。

/images/emoticon/emoticon82.gif
感謝 Kuro 大神專業打臉,馬上來更正內文。

我要留言

立即登入留言