0%

使用 Vuex 管理專案資料傳遞

文章目的

之前在學習 vue 的資料傳遞時,是使用 Event Bus 的方式來做傳遞,但當專案較大時,Event Bus 會較難管理資料,這時我們就可以利用 Vuex 來達成資料管理與傳遞的目的,本文將介紹 Vuex 與說明使用方法。

Vuex 介紹

Vuex 和 Event Bus 不一樣的地方是它是一個儲存庫的概念,儲存庫中將會為每一個步驟做分類與分工並讓所有的元件做使用,簡單來講就是先將資料做個統整再做傳遞

Vuex 方法

  • state:如同元件中的 data,負責管理資料。
  • actions:如同元件中的 methods,負責處裡非同步事件,或是取得遠端資料等。
  • getter:如同元件中的 computed,在資料呈現之前做過濾的動作。
  • mutations:這是 Vuex 新增的方法,改變資料內容的方法,在以前我們要改變資料狀態時都會直接在 methods 做變更,但在 Vuex 不同,actions 並不會做資料的變更,而是在 mutations 才會做資料狀態的變更。

Vuex 運作方法

在 Vue 的元件中,我們透過 dispatch 來觸發 actions,actions 就會去取得遠端資料,並透過 commit 的方式呼叫 mutations,最後透過 mutations 來改變 state 中的資料狀態並反映給元件。
這邊有一點要注意,當要使用一些非同步的行為時(AJAX、Settimeout 等),請在 actions 中就完成它。

Vuex 簡單範例,透過 Vuex 改變資料狀態

現在我們就來看看 Vuex 各方法的寫法吧。

範例狀況

現在我們的專案中有讀取的動畫功能,因為很多元件在取得資料時都會需要使用這功能,因此我們打算利用 Vuex 統一管理動畫功能。
我們利用 isLoading = true or false 來控制動畫的出現與消失。

創造我們的 Vuex 儲存庫

我們先在 src 資料夾中創造一個名為 store 的資料夾,並在此資料夾中創造一個名為 index.js 的檔案,這個檔案將為我們管理 Vuex。

  • 在檔案中我們先將 vuevuex import 進來,程式碼如下:

    1
    2
    3
    4
    import Vue from 'vue'
    import Vuex from 'vuex'

    Vue.use(Vuex)

    另外在 main.js(進入點) 也要做 import 的動作,並把 store 也 import 進來,程式碼如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import Vuex from 'vuex'
    import store from './store'

    Vue.use(Vuex)

    new Vue({
    router,
    store, //記得這裡也要補上 store 喔
    render: h => h(App)
    }).$mount('#app')
  • 接著我們在 export default 中輸入我們的方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    export default new Vuex.Store({
    state: {
    isLoading: false
    },
    actions: {
    updateLoading (context, status) {
    context.commit('LOADING', status)
    }
    },
    mutations: {
    LOADING (state, status) {
    state.isLoading = status
    }
    }
    })

    這邊將為 actions 和 mutations 做解釋

actions:創建一個 function,function 中擁有兩個參數,分別是 contextpayload(載核)context 是必要的參數, payload 則是我們自定義的參數(名稱也可自訂,以此範例來說就是 status),接著使用 context.commit 觸發 mutations 並把載核傳遞給它。

mutations:一樣創建一個 function,當此 function 被觸發時就會對 state 中的資料做變更。

統整:在元件中因為某個事件透過 dispatch 觸發了 updateLoading 這個 action,觸發的同時將 status 的資料傳遞進來,接著 updateLoading 透過 context.commit 觸發名為 LOADING 的 mutations 並將 status 資料傳遞給它,最後在名為 LOADING 的 mutations 中將 state 中的資料狀態變更,資料變更的同時會反應回元件上。

元件 Vuex 設定

因為是整個專案都會用到此動畫效果,因此我們將動畫程式碼放在我們的 App.vue 中就可以反映在每個元件上。
那因為資料的變更也是反應在 App.vue(根元件),因此我們可以利用 computed 來監控 Vuex 中的這個資料,程式碼如下:

1
2
3
4
computed: {
isLoading () {
return this.$store.state.isLoading
}

接著我們需要改寫的就是 methods 中觸發效果的寫法,程式碼如下:

1
2
vm.$store.dispatch('updateLoading', true)
vm.$store.dispatch('updateLoading', false)

透過 dispatch 觸發 updateLoading 這個 actions,並將 payload(true or false) 傳入,接著系統就會執行我們剛剛說的 Vuex 內部流程。

為 Vuex 加入嚴謹模式來檢視錯誤

我們另外可以在 Vuex 中加入嚴謹模式,讓我們的程式碼撰寫都在規範中,加入方式只需在 state 之前寫入以下程式碼即可:

1
strict: true,

使用 actions 取得遠端資料

剛剛有說過若要在 Vuex 中執行非同步行為我們就需要在 actions 中執行完這個動作。
現在將要介紹如何在 actions 中執行 AJAX 的行為。
在 Vuex 中執行 AJAX 的好處是我們可以統一管理這個行為,且較大型網站若有很多元件都需要用到此 AJAX,也可以隨時調用,不需要在元件內重複撰寫。

範例狀況

現在我們要接取一支可以取得商品資料的 api, 抓取到的 api 資料皆儲存在 products 這個陣列裡,以下是程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
export default new Vuex.Store({
strict: true,
state: {
isLoading: false,
products: []
},
actions: {
updateLoading (context, status) { //前一步驟的 loading 效果
context.commit('LOADING', status)
},
getProducts (context) {
const url = `api 路徑`
context.commit('LOADING', true) //觸發loading效果
axios.get(url).then((response) => {
context.commit('PRODUCTS', response.data.products)
context.commit('LOADING', false) //取消loading效果
})
}
},
mutations: {
LOADING (state, status) {
state.isLoading = status
},
PRODUCTS (state, payload) {
state.products = payload
}
}
})

我們的目的是要把 AJAX 的資料儲存到 state 的 products 裡,因此我們先在 actions 利用 getProducts 這個 function 去接取 api 並把抓取到的資料透過 commit 傳遞給 PRODUCTSPRODUCTS 收到資料後,再將資料儲存進 state 的 products 裡。

axios 解釋

這邊使用的接取 api 方法是 Vue axios,但你會發現我們上列的程式碼,接取 api 時不是這樣寫 this.$http.get(url),原因是現在 AJAX 的行為是在 Vuex 中進行,this 是代表元件,但現在並非在元件內,因此才會改寫,那改寫步驟如以下:

  • axios import from 'axios' 寫入 Vuex 裡。
  • this.$http 改寫成 axios

我們是直接透過掛載 axios 的方法並呼叫它,而非像一開始用 Vue 呼叫。

元件改寫

我們元件中一樣要有事件觸發 AJAX 的行為,一樣是使用 dispatch 做觸發,寫法如下:

1
2
3
4
getProducts () {
const vm = this;
vm.$store.dispatch('getProducts')
},

一樣的我們要將 products 這個陣列內容反應在元件中,我們透過 computed 來調用,寫法如下:

1
2
3
products () { //寫在 computed 中
return this.$store.state.products
}

利用 actions 傳遞參數

這邊要說明的是,actions 方法中只能傳遞一個參數,當我們今天要傳遞複數個參數時,就得利用物件的方法做傳遞,程式碼如下:

1
this.$store.dispatch('actions 方法', { 參數1, 參數2 })

另外若我們在執行某個 actions 方法時,需要執行另外一個 actions 方法可以利用 commit.dispatch('actions 方法名', 參數),利用這種方法調用其他 actions。

getters 寫法

前面有提到 getters 相當於 computed 我們也可以將部分寫法改成用 getters 替代,這邊示範將前幾個範例改寫。
我們在 Vuex 中新增一個名為 getters 的物件,並將要 computed 回傳的資料寫入,程式碼如下:

1
2
3
4
5
6
7
8
getters: {
categories (state) {
return state.categories
},
products (state) {
return state.products
}
}

這邊回傳前面範例的 products 和 categories。
接著我們回到元件中改寫程式碼,我們先將 getters import 進元件內 import { mapGetters } from 'vuex',接著在 computed 做改寫,改寫成 ...mapGetters (['products', 'categories']),陣列中是我們 Vuex 中 getters 的方法。
另外,我們也可以將 actions 做改寫,只要在 import 中把 actions import 進來 import { mapGetters, mapActions } from 'vuex',並在 methods 將方法改寫成 ...mapActions (['getProducts']),一樣的陣列中是我們的 actions 方法名。
這邊有一點要注意的是用這方法取用 actions 時不適用在需要傳遞參數的 actions。

Vuex 模組化

你可以發現到在前面的範例中我們已經在 Vuex 中寫入了很多程式碼,但這只是一個小小的範例而已,若遇到更大型專案時,模組化的概念就會顯得相當重要。
我們可以把 Vuex 中管理的行為再做進一步的拆分,使我們更加方便管理。
這邊將示範將範例中取得商品的方法做拆分。

  • 我們先在 store 資料夾中再新增一個叫做 products.js 的檔案,並把 getProducts 的 actions 和相關參數從 index.js 中拆出來並寫進 products.js 中。
  • 接著我們利用 import 將 products.js 的內容載入至 index.jsimport productsModule from './products',然後在 index 中新增一個名為 modules 的物件,並把 productsModule 加入。
    1
    2
    3
    modules: {
    productsModule
    }

    加強觀念

    在 Vuex 中 state 是屬於模組區域變數,裡頭的資料只會在該模組中才能做使用,但是 actions, mutations 和 getters 屬於全域變數
    全域變數好處是在小型專案時可以節省開發時間,但在大型專案中就會建議使用區域變數,避免衝突(當有相同方法名的時候就會產生衝突)。

如何取得拆分模組中的 state 區域變數(元件中)

這方法很簡單,只需要將程式碼這樣改寫即可:
this.$store.state.productModule.products

將模組內全域變數改成區域變數

若我們要避免衝突,將全域變數變成區域變數,可以在該模組的 export default 底下寫入這行程式碼:namespaced: true
變成區域變數之後我們要如何調用它呢?延續前面的調用方法我們可以這樣寫:

1
2
3
4
5
...mapActions('productModule', ['getProducts']) // 第一種方法,在第一個參數中寫入要調用的模組名
/*****/
getProducts () {
this.$store.dispatch('productsModule/getProducts') // 第二種方法,利用 dispatch 做調用 模組名/actions名
}

讓特定行為變為全域調用

若我們要某方法可以全域調用,像是範例的讀取動畫效果,因為變數是存在 index 裡,若我們要在 products 中調用可以這樣寫:

1
2
context.commit('LOADING', true, { root: true })
context.commit('LOADING', false, { root: true })

參考資料

六角學院課程–Vue 出一個電商網站
官方文件