Vuex 是一個專為 Vue.js 應用程式開發的狀態管理模式。它採用集中式存儲管理應用的所有組件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化。 在 Vue 之後引入 vuex 會進行自動安裝: 可以通過 https://unpkg.com/[email protected] 這樣的方式指定特定的版本 ...
Vuex 是一個專為 Vue.js 應用程式開發的狀態管理模式。它採用集中式存儲管理應用的所有組件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化。
在 Vue 之後引入 vuex
會進行自動安裝:
<script src="/path/to/vue.js"></script> <script src="/path/to/vuex.js"></script>
可以通過 https://unpkg.com/[email protected]
這樣的方式指定特定的版本。
NPM
npm install vuex --save
State
在 Vue 組件中獲得 Vuex 狀態
const Counter = { template: `<div>{{ count }}</div>`, computed: { count () { return this.$store.state.count } } }
mapState
輔助函數
當一個組件需要獲取多個狀態時候,將這些狀態都聲明為計算屬性會有些重覆和冗餘。為瞭解決這個問題,我們可以使用 mapState
輔助函數幫助我們生成計算屬性,讓你少按幾次鍵:
// 在單獨構建的版本中輔助函數為 Vuex.mapState import { mapState } from 'vuex' export default { // ... computed: mapState({ // 箭頭函數可使代碼更簡練 count: state => state.count, // 傳字元串參數 'count' 等同於 `state => state.count` countAlias: 'count', // 為了能夠使用 `this` 獲取局部狀態,必須使用常規函數 countPlusLocalState (state) { return state.count + this.localCount } }) }
當映射的計算屬性的名稱與 state 的子節點名稱相同時,我們也可以給 mapState
傳一個字元串數組。
computed: mapState([ // 映射 this.count 為 store.state.count 'count' ])
對象展開運算符
mapState
函數返回的是一個對象。我們如何將它與局部計算屬性混合使用呢?通常,我們需要使用一個工具函數將多個對象合併為一個,以使我們可以將最終對象傳給 computed
屬性。
computed: { localComputed () { /* ... */ }, // 使用對象展開運算符將此對象混入到外部對象中 ...mapState({ // ... }) }
Getter
有時候我們需要從 store 中的 state 中派生出一些狀態,例如對列表進行過濾並計數:
computed: { doneTodosCount () { return this.$store.state.todos.filter(todo => todo.done).length } }
如果有多個組件需要用到此屬性,我們要麼複製這個函數,或者抽取到一個共用函數然後在多處導入它——無論哪種方式都不是很理想。
Vuex 允許我們在 store 中定義“getter”(可以認為是 store 的計算屬性)。就像計算屬性一樣,getter 的返回值會根據它的依賴被緩存起來,且只有當它的依賴值發生了改變才會被重新計算。
Getter 接受 state 作為其第一個參數:
const store = new Vuex.Store({ state: { todos: [ { id: 1, text: '...', done: true }, { id: 2, text: '...', done: false } ] }, getters: { doneTodos: state => { return state.todos.filter(todo => todo.done) } } })
Getter 會暴露為 store.getters
對象:
store.getters.doneTodos // -> [{ id: 1, text: '...', done: true }]
Getter 也可以接受其他 getter 作為第二個參數:
getters: { // ... doneTodosCount: (state, getters) => { return getters.doneTodos.length } } store.getters.doneTodosCount // -> 1
我們可以很容易地在任何組件中使用它:
computed: { doneTodosCount () { return this.$store.getters.doneTodosCount } }
你也可以通過讓 getter 返回一個函數,來實現給 getter 傳參。在你對 store 里的數組進行查詢時非常有用。
getters: { // ... getTodoById: (state) => (id) => { return state.todos.find(todo => todo.id === id) } } store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false }
mapGetters
輔助函數
mapGetters
輔助函數僅僅是將 store 中的 getter 映射到局部計算屬性:
import { mapGetters } from 'vuex' export default { // ... computed: { // 使用對象展開運算符將 getter 混入 computed 對象中 ...mapGetters([ 'doneTodosCount', 'anotherGetter', // ... ]) } }
如果你想將一個 getter 屬性另取一個名字,使用對象形式:
mapGetters({ // 映射 `this.doneCount` 為 `store.getters.doneTodosCount` doneCount: 'doneTodosCount' })
Mutation
更改 Vuex 的 store 中的狀態的唯一方法是提交 mutation。Vuex 中的 mutation 非常類似於事件:每個 mutation 都有一個字元串的 事件類型 (type) 和 一個 回調函數 (handler)。這個回調函數就是我們實際進行狀態更改的地方,並且它會接受 state 作為第一個參數:
const store = new Vuex.Store({ state: { count: 1 }, mutations: { increment (state) { // 變更狀態 state.count++ } } })
store.commit('increment')
當使用對象風格的提交方式,整個對象都作為載荷傳給 mutation 函數,因此 handler 保持不變:
mutations: { increment (state, payload) { state.count += payload.amount } }
使用常量替代 Mutation 事件類型
// mutation-types.js export const SOME_MUTATION = 'SOME_MUTATION' // store.js import Vuex from 'vuex' import { SOME_MUTATION } from './mutation-types' const store = new Vuex.Store({ state: { ... }, mutations: { // 我們可以使用 ES2015 風格的計算屬性命名功能來使用一個常量作為函數名 [SOME_MUTATION] (state) { // mutate state } } })
在組件中提交 Mutation
你可以在組件中使用 this.$store.commit('xxx')
提交 mutation,或者使用 mapMutations
輔助函數將組件中的 methods 映射為 store.commit
調用(需要在根節點註入 store
)。
import { mapMutations } from 'vuex' export default { // ... methods: { ...mapMutations([ 'increment', // 將 `this.increment()` 映射為 `this.$store.commit('increment')` // `mapMutations` 也支持載荷: 'incrementBy' // 將 `this.incrementBy(amount)` 映射為 `this.$store.commit('incrementBy', amount)` ]), ...mapMutations({ add: 'increment' // 將 `this.add()` 映射為 `this.$store.commit('increment')` }) } }
Action
Action 類似於 mutation,不同在於:
- Action 提交的是 mutation,而不是直接變更狀態。
- Action 可以包含任意非同步操作。
讓我們來註冊一個簡單的 action:
const store = new Vuex.Store({ state: { count: 0 }, mutations: { increment (state) { state.count++ } }, actions: { increment (context) { context.commit('increment') } } })
Action 函數接受一個與 store 實例具有相同方法和屬性的 context 對象,因此你可以調用 context.commit
提交一個 mutation,或者通過 context.state
和 context.getters
來獲取 state 和 getters。
實踐中,我們會經常用到 ES2015 的 參數解構 來簡化代碼(特別是我們需要調用 commit
很多次的時候):
actions: { increment ({ commit }) { commit('increment') } }
分發 Action
Action 通過 store.dispatch
方法觸發:
store.dispatch('increment')
乍一眼看上去感覺多此一舉,我們直接分發 mutation 豈不更方便?實際上並非如此,還記得 mutation 必須同步執行這個限制麽?Action 就不受約束!我們可以在 action 內部執行非同步操作:
actions: { incrementAsync ({ commit }) { setTimeout(() => { commit('increment') }, 1000) } }
Actions 支持同樣的載荷方式和對象方式進行分發:
// 以載荷形式分發 store.dispatch('incrementAsync', { amount: 10 }) // 以對象形式分發 store.dispatch({ type: 'incrementAsync', amount: 10 })
來看一個更加實際的購物車示例,涉及到調用非同步 API 和分發多重 mutation:
actions: { checkout ({ commit, state }, products) { // 把當前購物車的物品備份起來 const savedCartItems = [...state.cart.added] // 發出結賬請求,然後樂觀地清空購物車 commit(types.CHECKOUT_REQUEST) // 購物 API 接受一個成功回調和一個失敗回調 shop.buyProducts( products, // 成功操作 () => commit(types.CHECKOUT_SUCCESS), // 失敗操作 () => commit(types.CHECKOUT_FAILURE, savedCartItems) ) } }
在組件中分發 Action
你在組件中使用 this.$store.dispatch('xxx')
分發 action,或者使用 mapActions
輔助函數將組件的 methods 映射為 store.dispatch
調用(需要先在根節點註入 store
):
import { mapActions } from 'vuex' export default { // ... methods: { ...mapActions([ 'increment', // 將 `this.increment()` 映射為 `this.$store.dispatch('increment')` // `mapActions` 也支持載荷: 'incrementBy' // 將 `this.incrementBy(amount)` 映射為 `this.$store.dispatch('incrementBy', amount)` ]), ...mapActions({ add: 'increment' // 將 `this.add()` 映射為 `this.$store.dispatch('increment')` }) } }
Module
由於使用單一狀態樹,應用的所有狀態會集中到一個比較大的對象。當應用變得非常複雜時,store 對象就有可能變得相當臃腫。
為瞭解決以上問題,Vuex 允許我們將 store 分割成模塊(module)。每個模塊擁有自己的 state、mutation、action、getter、甚至是嵌套子模塊——從上至下進行同樣方式的分割:
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 的狀態 store.state.b // -> moduleB 的狀態
一般項目結構
Vuex 並不限制你的代碼結構。但是,它規定了一些需要遵守的規則:
-
應用層級的狀態應該集中到單個 store 對象中。
-
提交 mutation 是更改狀態的唯一方法,並且這個過程是同步的。
-
非同步邏輯都應該封裝到 action 裡面。
只要你遵守以上規則,如何組織代碼隨你便。如果你的 store 文件太大,只需將 action、mutation 和 getter 分割到單獨的文件。
對於大型應用,我們會希望把 Vuex 相關代碼分割到模塊中。下麵是項目結構示例:
├── index.html
├── main.js
├── api
│ └── ... # 抽取出API請求
├── components
│ ├── App.vue
│ └── ...
└── store
├── index.js # 我們組裝模塊並導出 store 的地方
├── actions.js # 根級別的 action
├── mutations.js # 根級別的 mutation
└── modules
├── cart.js # 購物車模塊
└── products.js # 產品模塊
請參考購物車示例。
總結
安裝vuex
npm install --save vuex
<!--這裡假定你已經搭好vue的開發環境了-->
配置vuex
1、首先創建一個js文件,假定這裡取名為store.js
2、在main.js文件中引入上面創建的store.js
//main.js內部對store.js的配置 import store from '"@/store/store.js' //具體地址具體路徑 new Vue({ el: '#app', store, //將store暴露出來 template: '<App></App>', components: { App } });
store.js中的配置
import Vue from 'vue'; //首先引入vue import Vuex from 'vuex'; //引入vuex Vue.use(Vuex) export default new Vuex.Store({ state: { // state 類似 data //這裡面寫入數據 }, getters:{ // getters 類似 computed // 在這裡面寫個方法 }, mutations:{ // mutations 類似methods // 寫方法對數據做出更改(同步操作) }, actions:{ // actions 類似methods // 寫方法對數據做出更改(非同步操作) } })
使用vuex
我們約定store中的數據是以下形式
state:{ goods: { totalPrice: 0, totalNum:0, goodsData: [ { id: '1', title: '好吃的蘋果', price: 8.00, image: 'https://www.shangdian.com/static/pingguo.jpg', num: 0 }, { id: '2', title: '美味的香蕉', price: 5.00, image: 'https://www.shangdian.com/static/xiangjiao.jpg', num: 0 } ] } }, gettles:{ //其實這裡寫上這個主要是為了讓大家明白他是怎麼用的, totalNum(state){ let aTotalNum = 0; state.goods.goodsData.forEach((value,index) => { aTotalNum += value.num; }) return aTotalNum; }, totalPrice(state){ let aTotalPrice = 0; state.goods.goodsData.forEach( (value,index) => { aTotalPrice += value.num * value.price }) return aTotalPrice.toFixed(2); } }, mutations:{ reselt(state,msg){ console.log(msg) //我執行了一次; state.goods.totalPrice = this.getters.totalPrice; state.goods.totalNum = this.getters.totalNum; }, reduceGoods(state,index){ //第一個參數為預設參數,即上面的state,後面的參數為頁面操作傳過來的參數 state.goodsData[index].num-=1; let msg = '我執行了一次' this.commit('reselt',msg); }, addGoods(state,index){ state.goodsData[index].num+=1; let msg = '我執行了一次' this.commit('reselt',msg); /** 想要重新渲染store中的方法,一律使用commit 方法 你可以這樣寫 commit('reselt',{ state: state }) 也可以這樣寫 commit({ type: 'reselt', state: state }) 主要看你自己的風格 **/ } }, actions:{ //這裡主要是操作非同步操作的,使用起來幾乎和mutations方法一模一樣 //除了一個是同步操作,一個是非同步操作,這裡就不多介紹了, //有興趣的可以自己去試一試 //比如你可以用setTimeout去嘗試一下 }
好了,簡單的數據我們就這樣配置了,接下來看看購物車頁面吧;
第一種方式使用store.js中的數據(直接使用)
<template> <div id="goods" class="goods-box"> <ul class="goods-body"> <li v-for="(list,index) in goods.goodsData" :key="list.id"> <div class="goods-main"> <img :src="list.image"> </div> <div class="goods-info"> <h3 class="goods-title">{{ list.title }}</h3> <p class="goods-price">¥ {{ list.price }}</p> <div class="goods-compute"> <!--在dom中使用方法為:$store.commit()加上store.js中的屬性的名稱,示例如下--> <span class="goods-reduce" @click="$store.commit('reduceGoods',index)">-</span> <input readonly v-model="list.num" /> <span class="goods-add" @click="$store.commit('addGoods',index)">+</span> </div> </div> </li> </ul> <div class="goods-footer"> <div class="goods-total"> 合計:¥ {{ goods.totalPrice }} <!-- 如果你想要直接使用一些數據,但是在computed中沒有給出來怎麼辦? 可以寫成這樣 {{ $store.state.goods.totalPrice }} 或者直接獲取gettles裡面的數據 {{ $store.gettles.totalPrice }} --> </div> <button class="goods-check" :class="{activeChecke: goods.totalNum <= 0}">去結賬({{ goods.totalNum }})</button> </div> </div> </template> <script> export default { name: 'Goods', computed:{ goods(){ return this.$store.state.goods; } } } </script>
如果上面的方式寫參數讓你看的很彆扭,我們繼續看第二種方式
第一種方式使用store.js中的數據(通過輔助函數使用)
<!--goods.vue 購物車頁面--> <template> <div id="goods" class="goods-box"> <ul class="goods-body"> <li v-for="(list,index) in goods.goodsData" :key="list.id"> <div class="goods-main"> <img :src="list.image"> </div> <div class="goods-info"> <h3 class="goods-title">{{ list.title }}</h3> <p class="goods-price">¥ {{ list.price }}</p> <div class="goods-compute"> <span class="goods-reduce" @click="goodsReduce(index)">-</span> <input readonly v-model="list.num" /> <span class="goods-add" @click="goodsAdd(index)">+</span> </div> </div> </li> </ul> <div class="goods-footer"> <div class="goods-total"> 合計:¥ {{ goods.totalPrice }} <!-- gettles裡面的數據可以直接這樣寫 {{ totalPrice }} --> </div> <button class="goods-check" :class="{activeChecke: goods.totalNum <= 0}">去結賬({{ goods.totalNum }})</button> </div> </div> </template> <script> import {mapState,mapGetters,mapMutations} from 'vuex'; /** 上面大括弧裡面的三個參數,便是一一對應著store.js中的state,gettles,mutations 這三個參數必須規定這樣寫,寫成其他的單詞無效,切記 畢竟是這三個屬性的的輔助函數 **/ export default { name: 'Goods', computed:{ ...mapState(['goods']) ...mapGetters(['totalPrice','totalNum']) /** ‘...’ 為ES6中的擴展運算符,不清楚的可以百度查一下 如果使用的名稱和store.js中的一樣,直接寫成上面數組的形式就行, 如果你想改變一下名字,寫法如下 ...mapState({ goodsData: state => stata.goods }) **/ }, methods:{ ...mapMutations(['goodsReduce','goodsAdd']), /** 這裡你可以直接理解為如下形式,相當於直接調用了store.js中的方法 goodsReduce(index){ // 這樣是不是覺得很熟悉了? }, goodsAdd(index){ } 好,還是不熟悉,我們換下麵這種寫法 onReduce(index){ //我們在methods中定義了onReduce方法,相應的Dom中的click事件名要改成onReduce this.goodsReduce(index) //這相當於調用了store.js的方法,這樣是不是覺得滿意了 } **/ } } </script>
Module
const moduleA = { state: { /*data**/ }, mutations: { /**方法**/ }, actions: { /**方法**/ }, getters: { /**方法**/ } } const moduleB = { state: { /*data**/ }, mutations: { /**方法**/ }, actions: { /**方法**/ } } export default new Vuex.Store({ modules: { a: moduleA, b: moduleB } }) //那怎麼調用呢?看下麵! //在模塊內部使用 state.goods //這種使用方式和單個使用方式樣,直接使用就行 //在組件中使用 store.state.a.goods //先找到模塊的名字,再去調用屬性 store.state.b.goods //先找到模塊的名字,再去調用屬性
參考地址:《震驚!喝個茶的時間就學會了vuex》