從概念到實際專案__vuex指北

shotCat發表於2019-03-03

前言

這篇文章總結了vuex實際開發涉及到的大部分概念,並加上了很多tips是自己實際開發工程中的經驗,最後再加上自己實際專案的vuex構建。總之文字很多,程式碼也很多,建議大家先收藏(滑稽臉~)再結合官方文件慢慢看。

食用方法: 邊看邊敲! 建議初學者先看“1,核心概念”,再根據“5,實際專案構建本地”進行本地構建。或者偷懶直接在官方例項上進行驗證。如果是在實際開發遇到問題的可以根據目錄找到相關的tips,也許就能解決你遇到的問題。總之就是先收藏啦!((^▽^))

1,核心概念

1.1 State: 用於資料的儲存,是store中的唯一資料來源,類似vue中data物件.

  • 單一狀態樹:用一個物件就包含了所有應用層級狀態.每個應用就只包含一個store例項.
  • 計算屬性:由於Vuex的狀態儲存是響應式的,從store例項中讀取狀態最簡單的方法就是在計算屬性中返回某個狀態(例如token).
  • 使用方法:
// 定義
new Vuex.Store({
    state: {
        bilibili: {
				acFun:"我還想再活五百年"
			}
    }
    //...
})
// 元件中獲取
this.$store.state.bilibili.acFun
複製程式碼

Tips:如果某個state是作為公共狀態給多個元件使用,且不想被修改後汙染其他元件.這時可以將state寫成return形式,類似vue中data一樣.(僅2.30+以上支援)

    state(){
		return{
			bilibili: {
				acFun:"我還想再活五百年"
			}
		}
    }
複製程式碼

1.2 Module: 將store分割成不同的模組,方便管理維護

  • 可以將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 的狀態
複製程式碼

1.3 Getter: 對共享資料進行過濾獲取

  • 當需要對 store 中的資料進行處理,或者資料被多個元件複用,就可以使用 Getters 來處理,Getters 也可以理解為 Vue 中的計算屬性 (computed),其實就是基於state資料的再包裝
  • getter的返回值會根據它的依賴被快取起來.
  • 使用方法:
// 定義,第一個引數為該模組下的state;第二個引數getters為store裡的getters.注意getters是沒有按模組進行區分的;第三個引數rootState顧名思義就是根state
getters: {
    cartProducts(state, getters, rootState) 
        => (getters.allProducts.filter(p => p.quantity)),

	dateFormat(state, getters) {
            let date = state.nowDate;
            return `${date.getFullYear()}-${date.getMonth()+1}-${date.getDate()} / ${date.getHours()}:${date.getMinutes()}`;
        }
}
// 元件中獲取
this.$store.getters.cartProducts

//補充:由於getters會快取結果.如果你不想快取,或者想對getters進行傳參,此時則需要用函式形式寫getter.
getters:{
//...
	test:(state)=>(param)=>{
		return state.todos.find(todo=>todo.id===id)
	}
}

複製程式碼

1.4 Mutation: 改變state的唯一方法

  • 每個mutation都有一個字串的事件型別(type) 和一個 回撥函式
  • mutation必須是同步函式
  • mutation不能包含非同步操作
  • 由於store中狀態是響應式的,那麼在修改時,mutation也應該滿足vue響應式的一些注意事項:
    • 最好提前在你的 store 中初始化好所有所需屬性。
    • 當需要在物件上新增新屬性時,你應該
      • 使用 Vue.set(obj, ‘newProp’, 123),或者
      • 以新物件替換老物件.例如,利用 stage-3 的物件展開運算子我們可以這樣寫:
	state.obj = { ...state.obj, newProp: 123 }
複製程式碼
  • mutation是修改state的唯一方法,並且mutations不能直接呼叫,而要通過相應type呼叫store.commit.
  • 使用方法:
// 定義 第一個引數state為該模組下的state;第二個引數products為呼叫時的傳參
mutations: {
    setProducts (state, products) {
        state.allProducts = products
    }
}

// 元件中提交方式可以分為三種,其實前兩種都是載荷(payload),第三種是物件形式

//第一種 直接在後面加上要傳入的引數
this.$store.commit('setProducts', 'GodOfWar4')

//第二種 直接在後面加上要傳入的引數
this.$store.commit('setProducts', {
	name:'GodOfWar4',
	comment:"我TM射爆!"
	})
//注意:此時mutation,setProducts 就要修改為下面形式
setProducts (state, products) {
        state.allProducts = products.name //要改為這種寫法
    }

//第三種 將state類別作為物件的屬性,和引數一起提交
this.$store.commit({
	type:'setProducts',
	name:'GodOfWar4',
	comment:"我TM射爆!"
	})
//此時mutation的寫法和第二種情況一樣.

複製程式碼

1.5 Action: 可以使用非同步操作提交mutation

  • action提交的是mutation,而不是直接變更狀態.action可以包含非同步操作
  • action通過store.dispatch觸發(非同步)
  • action返回的是promise
  • 如果是state的資料就使用actions請求資料,建議資料處理也放在actions中,或者放在getter中進行資料處理.mutations只做state的修改.
  • 使用方法:
      state: {
         count: 0
             },
      mutations: {                
         increment (state) {
          state.count++
         }
          },
      actions: {         //只是提交`commit`了`mutations`裡面的方法。
         increment (context,payload) {
          context.commit('increment')
   		}
 	 }


 // 一般我們會通過解構簡寫成這樣
  actions: {
   increment ({ commit },payload) {
         commit('increment')
      }
         }

// 在元件中使用,同mutation,只是由commit變為dispatch
this.$store.dispatch('increment', {//..payload})


//這裡需要說明的是第一個引數context就是上下文,context是一個store物件,你也可以用解構的方式寫出來,第二個引數payload是我們的傳參,同mutation一樣.
//那麼context究竟包含了哪些?通過原始碼可以清楚看到:

let res = handler({ 
   dispatch,
   commit,
   getters: store.getters,
   state: getNestedState(store.state, path),
   rootState: store.state
  }, payload, cb)

//可以看出context包括5個屬性:dispatch,commit,getters,state,rootState.
//故我們可以通過解構的方式{commit,dispatch,state}只取我們需要的屬性.
複製程式碼
  • 組合 Action (完全照搬官網說明)

Action 通常是非同步的,那麼如何知道 action 什麼時候結束呢?更重要的是,我們如何才能組合多個 action,以處理更加複雜的非同步流程?

首先,你需要明白 store.dispatch 可以處理被觸發的 action 的處理函式返回的 Promise,並且 store.dispatch仍舊返回 Promise

actions: {
  actionA ({ commit }) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        commit('someMutation')
        resolve()
      }, 1000)
    })
  }
}

複製程式碼

現在你可以:

store.dispatch('actionA').then(() => {
  // ...
})

複製程式碼

在另外一個 action 中也可以:

actions: {
  // ...
  actionB ({ dispatch, commit }) {
    return dispatch('actionA').then(() => {
      commit('someOtherMutation')
    })
  }
}

複製程式碼

最後,如果我們利用 async / await,我們可以如下組合 action:

// 假設 getData() 和 getOtherData() 返回的是 Promise

actions: {
  async actionA ({ commit }) {
    commit('gotData', await getData())
  },
  async actionB ({ dispatch, commit }) {
    await dispatch('actionA') // 等待 actionA 完成
    commit('gotOtherData', await getOtherData())
  }
}

複製程式碼

一個 store.dispatch 在不同模組中可以觸發多個 action 函式。在這種情況下,只有當所有觸發函式完成後,返回的 Promise 才會執行。

Tips:

  • 有一點要注意的是,將 store 中的 state 繫結到 Vue 元件中的 computed 計算屬性後,對 state 進行更改需要通過 mutation 或者 action,在 Vue 元件中直接進行賦值 (this.myState = ‘ABC’) 是不會生效的。

  • 在 Vuex 模組化中,state 是唯一會根據組合時模組的別名來新增層級的,後面的 getters、mutations 以及 actions 都是直接合並在 store 下。

  • 由於vuex是單向資料流,vue中v-model是雙向繫結.所以當v-model繫結的資料時vuex時,需要監聽實時修改vuex中的資料.

2,圖例分析

  • 資料夾結構:

    資料夾結構

  • vuex組織結構:

    vuex組織結構

  • vuex操作:

    vuex操作

  • vuex的資料流向:

    vuex的資料流向

3,Vuex安裝

此部分參考了# Vue元件通訊深入Vuex,在此表示感謝!

  • 3.1 在專案中安裝Vuex:
npm install vuex --save
複製程式碼
  • 3.2 在src目錄下新建store/index.js,其中程式碼如下:
import Vue from 'vue'
import Vuex from 'vuex'
// 修改state時在console列印,便於除錯
import createLogger from 'vuex/dist/logger'

Vue.use(Vuex)

const debug = process.env.NODE_ENV !== 'production'

const state = {}
const getters = {}
const mutataions = {}
const actions = {}

export default new Vuex.Store({
    state,
    getters,
    mutataions,
    actions,
    // 嚴格模式,非法修改state時報錯
    strict: debug,
    plugins: debug ? [createLogger()] : []
})
複製程式碼
  • 3.3 在入口檔案main.js中新增:
// ...
import router from './router'
import store from './store'

new Vue({
    el: '#app',
    router,
    store,
    // ...
})
複製程式碼

可以對比vue-router和vuex的安裝方式:它們均為vue外掛,並在例項化元件時引入,在該例項下的所有元件均可由this.$routerthis.$store的方式查詢到對應的外掛例項

4,輔助函式用法

  • 再次強調在 Vuex 模組化中(即使用module寫法),state 是唯一會根據組合時模組的別名來新增層級的,後面的 getters、mutations 以及 actions 都是直接合並在 store 下。所以輔助函式也是同樣符合上述規定

  • 注意: 這裡為了簡便,都使用了...擴充運算子.並且我們 所有使用的輔助函式都是寫在device模組(module)內!

4.1 mapState

寫在computed的情況

// 首先引入mapState
import {mapState} from 'vuex'

export default {
//1,寫在computed的情況
  computed: {
 ...mapState({
  //這裡需要注意的是,由於我們是使用module的,所以需要寫成加上device,並且是這種箭頭函式的形式.
  test1: state => state.device.test

//test1:'test'  這種寫成字串形式,等價於state=>state.test
//test1(state){
//	return state.device.test + this.message
//} 如果返回值中需要用到元件this,則需要寫成這種函式形式

  }), 
  },
}

//這種情況可以直接通過this.test1來得到state.device.test

複製程式碼

寫在methods的情況

這中寫法最大的不同就是,務必要寫成this.test1()才能得到state.device.test

// 首先引入mapState
import {mapState} from 'vuex'

export default {
//2,寫在methods的情況,寫法和在computed中基本是一樣的.但注意的是:mapState是不能直接寫在某個函式體內的!只能像這樣寫在methods內.
  methods: {
 ...mapState({
  //這裡一樣也是支援三種寫法的,看實際情況選擇
  test1: state => state.device.test

//test1:'test'  
//test1(state){
//	return state.device.test + this.message
//} 

  }), 
  },
}

//這中寫法最大的不同就是,務必要寫成this.test1()才能得到state.device.test直接寫成this.test得到的只是獲取返回state的函式

複製程式碼

注意: 常用的做法是將state中資料使用getter包裝後輸出,因此,mapState在專案中較少遇到. 其實,個人感覺這兩種情況的寫法都不是太好,都比較麻煩.個人建議還是直接使用let test1=this.$store.state.device.test這種寫法最簡單直接明瞭,複雜的資料就使用getters.

4.2 mapGetters

寫在computed的情況

// 首先引入mapGetters
import {mapGetters} from 'vuex'

export default {
//1,寫在computed的情況
  computed: {
 ...mapGetters({
  //這裡需要注意的是,與mapState不同的是,我們雖然使用了module,但並不能加上device,這裡vuex是把所有的getters合在了一起,裡面並沒有device進行模組劃分.所以只能寫成字串形式.並且注意:如果device裡的getters屬性名與根getters的屬性名一樣.根getters的屬性名則會就進行覆蓋.
  test1: 'test'


  }), 
  },
}

//同樣這種情況可以直接通過this.test1來得到getters.test

複製程式碼

寫在methods的情況

這中寫法最大的不同就是,務必要寫成this.test1()才能得到state.device.test

// 首先引入mapGetters
import {mapGetters} from 'vuex'

export default {
//2,寫在methods的情況,寫法和在computed中基本是一樣的.但注意的是:mapGetters是不能直接寫在某個函式體內的!只能像這樣寫在methods內.
  methods: {
 ...mapGetters({
  //這裡一樣也是隻支援字串寫法的
  test1:'test'
  }), 
  },
}

//這中寫法最大的不同就是,務必要寫成this.test1()才能得到getters.test

複製程式碼

再次重申:與mapState不同的是,我們雖然使用了module,但並不能加上device,這裡vuex是把所有的getters合在了一起,裡面並沒有device進行模組劃分.所以只能寫成字串形式.並且注意:如果device裡的getters屬性名與根getters的屬性名一樣.根getters的屬性名則會就進行覆蓋.並且寫法只能是{test1:'test'}或者['test']此時this.test就等於getters.test

4.3 mapMutations

mapMutations對映的是store.commit('mutation名',傳參)而不是mutation函式,並且和mapGetters一樣,是把所有的mutation合在了一起,所以無法通過模組名進行區分,只能自己在命名時進行區分.或者使用自帶的名稱空間.

// 首先引入mapMutations
import {mapMutations} from 'vuex'

export default {
//此時需要寫在methods上,寫法和mapGetters基本是一樣的.可以寫成物件形式和陣列形式
  methods: {
 ...mapMutations({
  test1:'test'
  }), 
// ...mapMutations([
//  'test'  //此時this.test(param)對映為this.$store.commit('test',param)
//  ]), 
//  },
}


複製程式碼

4.4 mapActions

mapActions對映的是store.dispatch('action名',傳參)而不是action函式,並且和mapGetters一樣,是把所有的action合在了一起,所以無法通過模組名進行區分,只能自己在命名時進行區分.或者使用自帶的名稱空間.

// 首先引入mapActions
import {mapActions} from 'vuex'

export default {
//此時需要寫在methods上,寫法和mapGetters基本是一樣的.可以寫成物件形式和陣列形式
  methods: {
 ...mapActions({
  test1:'test'
  }), 
// ...mapActions([
//  'test'  //此時this.test(param)對映為this.$store.dispatch('test',param)
//  ]), 
//  },
}


複製程式碼

5,實際專案構建

5.1,檔案結構構建

首先宣告下,vuex實際專案構建有很多人是以功能進行劃分模組.例如:

store
    ├── index.js             # 匯出 store 的地方
    ├── state.js             # 根級別的 state
    ├── getters.js           # 二次包裝state資料
    ├── actions.js           # 根級別的 action
    ├── mutations.js         # 根級別的 mutation
複製程式碼

但我更傾向於以業務邏輯進行劃分模組,畢竟我們構建專案的src和構建vue也都是以業務邏輯進行區分的.所以實際專案使用的是以modules進行模組劃分的.具體可以參考上圖的vue結構組織形式.資料夾結構如下:(下面我也會以這種結構進行講解)

store
    ├── index.js             # 匯出 store 的地方
    ├── modules              # modules資料夾
    	├── home.js          # home 的模組檔案
    	├── device.js        # device 的模組檔案
    	├── event.js         # event 的模組檔案
        ├── order.js         # order 的模組檔案
    	├── user.js          # user 的模組檔案
    	├── log.js           # log 的模組檔案
        ...
複製程式碼

5.2,index.js檔案配置

import Vue from 'vue'  //引入vue
import Vuex from 'vuex' //引入vuex
//引入你的module模組
import home from './modules/home'
import device from './modules/device'
import event from './modules/event'
import order from './modules/order'
import user from './modules/user'
import log from './modules/log'

Vue.use(Vuex)
export default new Vuex.Store({
//這裡可以存放和處理一些公共的資訊,例如token,使用者資訊,初始化資訊等.
  state: {
    token: '123',
    userInfo: {
      userId: 0,
      userName: '',
      roleId: 0,
      roleName: ''
  	},
    initialInfo: {
      menuList: [],
      functionList: [],
      projectNodeList: []
    } 
  },
  mutations: {
    setToken (state, token) {
      state.token = token
  	},
    setUserInfo (state, userInfo) {
      state.userInfo = userInfo
  	} 
  },
 //這裡填寫我們引入的module模組
  modules: {
	home,
    device,
	event,
	order,
	user,
	log
  }
})
複製程式碼

5.3, module模組元件的編寫.

這裡以device模組為例進行講解:

import Vue from 'vue';

const device = {
  namespaced:true,  //這裡我使用了名稱空間

state: {
//這裡面的state結構就與你device中vue元件的結構進行對應
  //例如:leftTree對應的是device檔案下的leftTree.vue元件
  leftTree:{
	  //我一般會將元件需要存放的資料分成這三類進行分別存放,當然你也可以根據自己的需求,自行配置或者不要
	  output:{ 
		//暴露出來的公共資料
	  },
	  update:{
	      //是否更新元件的資料
	  },
	  cache:{
		//元件快取資料
	  } 
  },
},

getters: {
  leftTree_checkedNode: state=>{
    return state.leftTree.output.checkedNode
  },
  leftTree_update_tree:state=>{
    return state.leftTree.update.tree
  }
},

mutations: {
    leftTree_checkedNode(state,val){
      state.leftTree.output.checkedNode=val
  },
    leftTree_update_tree(state,val){
      state.leftTree.update.tree=val
  },
},

actions: {

  }
};

export default device;

複製程式碼

5.4, 在vue元件中的呼叫.

//注意這裡的寫法都是按module分模組並加上名稱空間的寫法
this.$store.state.device.leftTree.output.checkedNode  //這樣寫會很長
this.$store.getters['device/leftTree_checkedNode']    //這樣寫會簡短點,如果沒有名稱空間,則不寫device/

this.$store.commit('device/leftTree_update_tree',true) //執行device裡的mutation方法.同樣,沒有名稱空間,則不寫device/
複製程式碼

最後我還是想說一下:其實當你想給state設定一個複雜的物件,但mutation裡卻沒有實現方法時,你是可以使用vue.set方法來實現的.也就是說vue.set其實也是可以修改state狀態的.但一般建議還是使用mutation.方便管理維護.

6, 使用場景

最後簡單提一下vuex的使用場景:

  • 兩個以上元件共享的資料.
  • 多個兄弟元件共享的資料.
  • 方便兄弟元件間修改共享的資料.
  • 全域性共享資料,如:token.
  • 核心業務資料.

一些實際應用

  • 由於vuex是全域性儲存的,只有重新整理頁面資料才會重置.所以有時候可以用來儲存元件銷燬之前儲存的部分資訊,而不用重複請求資料.

最後

如果大家發現了哪裡有不對,或者異議的地方,歡迎留言拍磚!謝謝!

相關文章