前言
這篇文章總結了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的資料流向:
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.$router
和this.$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是全域性儲存的,只有重新整理頁面資料才會重置.所以有時候可以用來儲存元件銷燬之前儲存的部分資訊,而不用重複請求資料.
最後
如果大家發現了哪裡有不對,或者異議的地方,歡迎留言拍磚!謝謝!