vue從入門到進階:Vuex狀態管理(十)

風雨後見彩虹發表於2018-01-24

Vuex 是一個專為 Vue.js 應用程式開發的狀態管理模式。它採用集中式儲存管理應用的所有元件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化。

在 Vue 之後引入 vuex 會進行自動安裝:

<script src="/path/to/vue.js"></script>
<script src="/path/to/vuex.js"></script>

可以通過 https://unpkg.com/vuex@2.0.0 這樣的方式指定特定的版本。

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.statecontext.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 並不限制你的程式碼結構。但是,它規定了一些需要遵守的規則:

  • 1.應用層級的狀態應該集中到單個 store 物件中。
  • 2.提交 mutation 是更改狀態的唯一方法,並且這個過程是同步的。
  • 3.非同步邏輯都應該封裝到 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》

相關文章