Vue現(xiàn)代化使用方法(四)--Vuex
在組件內(nèi)可以通過data屬性共享數(shù)據(jù),父子組件也可以通過props進(jìn)行數(shù)據(jù)共享,但如果是兄弟跨組件之間的數(shù)據(jù)共享,就要借助Vuex,Vuex類似大樹的主干,各個組件類似一個個獨(dú)立的樹枝,組件之間通過Vuex形成一種聯(lián)系,只要把需要共享的數(shù)據(jù)放到Vuex中,所有相關(guān)組件都可以訪問引用,這樣就能形成跨組件的數(shù)據(jù)共享。
通過npm進(jìn)行安裝
npm install --save vuex
基本組成
按Vuex的設(shè)計(jì)思路,其核心(Store)是包括下面四部分:
- state
- getters
- mutations
- actions
state
// 在index.js頁面做如下改造
import Vuex from 'vuex'; // 引入Vuex
Vue.use(Vuex); // 啟用Vuex
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
}
}); // 設(shè)定一個公共倉庫
let vm = new Vue({
el: "#app",
store, // 引入建立的倉庫
render: h => h(app)
});
...
// 在index.vue的計(jì)算屬性中添加下面代碼
computed: {
count () {
return this.$store.state.count
}
}
...
// 在index.vue的模板中添加如下內(nèi)容,引入之前創(chuàng)建的header.vue組件
<p>index.vue中的值:{{count}}</p>
<cus-header></cus-header>
...
// 在header.vue組件也一樣添加相關(guān)計(jì)算屬性,并修改模板中的內(nèi)容
<div>
<p>header.vue中的count值:{{count}}</p>
</div>
...
// 這時頁面就渲染為
<p>index.vue中的值:0</p>
<div>
<p>header.vue中的count值:0</p>
</div>
通過上例我們可以看到在index.js設(shè)定的公共倉庫(store)中的state內(nèi)容,在index.vue和header.vue中共享,通過調(diào)用this.$store.state.count來引用,這樣就做到了跨組件數(shù)據(jù)共享
把數(shù)據(jù)放到計(jì)算屬性中,
在上例中,我們是通過this.$store.state.count對倉庫中值進(jìn)行訪問,這種寫法是顯得有些冗余,Vuex在這里提供了mapState函數(shù)來優(yōu)化了這個問題
// 利用mapState,可以簡化對倉庫中值的調(diào)用,計(jì)算屬性中的其他值,正常使用不受影響,添加了這個方法只影響了對倉庫中值的取用
// 先引入這個方法
import { mapState } from 'vuex';
...
// 在計(jì)算屬性中用這個方法包裹
computed: mapState({
count (state) {
return state.count;
},
fullName () {
return `${this.firstName}-${this.lastName}`;
}
}),
...
// 利用箭頭函數(shù)還可以在簡化下當(dāng)前代碼
count: state => state.count,
fullName () {
return `${this.firstName}-${this.lastName}`;
}
...
// 借助mapState可以使用字符串?dāng)?shù)組來再簡化代碼,調(diào)用this.count時 映射 store.sate.count
computed: mapState(['count']),
...
// 如果名詞不一致可以使用對象形式,在模板中使用{{co}}
...mapState({
co: 'count'
})
...
// 不過這時我們不好添加其他的計(jì)算屬性了,在這里可以簡化使用對象擴(kuò)展來解決這個問題
computed: {
fullName () {
return `${this.firstName}-${this.lastName}`;
},
...mapState(['count'])
},
getters
getters就是針對state的計(jì)算屬性
// index.js中進(jìn)行下面內(nèi)容添加
const store = new Vuex.Store({
state: {
count: 0,
name: 'lin ken',
address: '深圳XX區(qū)XX街道',
},
getters: {
userInfo (state) {
return `姓名:${state.name}; 住址:${state.address}`;
}
}
});
...
// index.vue計(jì)算屬性添加下面內(nèi)容
userInfo () {
return this.$store.getters.userInfo;
},
...
// index.vue模板添加如下內(nèi)容
<p>{{userInfo}}</p>
與mapState類似,getters也有mapGetters使用方式與mapState類似
// 引入mapGetters
import { mapState,mapGetters } from 'vuex';
...
// 在計(jì)算屬性中使用擴(kuò)展對象寫法把相關(guān)getters寫到計(jì)算屬性中
fullName () {
return `${this.firstName}-${this.lastName}`;
},
...mapGetters(['userInfo']),
...mapState(['count']),
...
// 如果不希望使用userInfo這個名詞,mapGetters可以使用對象形式,替換相關(guān)名詞
...mapGetters({
cusInfo: 'userInfo'
})
...
// 模板中可以使用
<p>{{cusInfo}}</p>
mutation
mutation是Vuex設(shè)定唯一可以進(jìn)行對state值進(jìn)行修改的地方,Vuex應(yīng)該在這里對這些值的變化做了相關(guān)監(jiān)聽跟蹤
// index.js中的公共倉庫添加如下代碼
mutations: {
increment (state) {
state.count++
}
}
...
// index.vue的模板頁做如下調(diào)整
<p>index.vue中的值:{{co}}</p>
<cus-header></cus-header>
<button @click="addCount">addCount</button>
...
// index.vue中添加methods
addCount () {
this.$store.commit('increment'); // 通過commit調(diào)用mutations中的increment方法
}
這時點(diǎn)擊頁面就會發(fā)現(xiàn)在index.vue和header.vue中引用的count值一起發(fā)生了更改
// 通過commit調(diào)用mutations時,還可以傳遞參數(shù)
// 在index.js中的方法接收傳入?yún)?shù)
increment (state, payload) {
state.count += payload.addNum;
}
...
// 在index.vue中的調(diào)用方法傳遞相關(guān)值
addCount () {
this.$store.commit('increment', {addNum:100}); // 調(diào)用mutations
}
...
// 如果使用對象形式進(jìn)行傳值,還可以使用下面的簡寫方式
// commit方法自動識別type類型,所以也就不要使用type來傳相關(guān)值
this.$store.commit({
type: 'increment',
addNum:100
})
在組件內(nèi)通過commit觸發(fā)mutations時,我們是使用字符串increment,這就隱含會有一些問題當(dāng)項(xiàng)目比較大時,同時協(xié)助的人比較多時,頁面中隨便調(diào)用一個mutations可能是沒人知道做什么用的
// 在項(xiàng)目目錄建立mutation-types.js
export const INCREMENT = 'INCREMENT'; // 在index.js的公共倉庫中針對count增加的mutations
...
// 在index.js引入這個文件
import { INCREMENT } from './mutation-types';
...
// 在store中的mutations做如下修改
mutations: {
[INCREMENT] (state, payload) {
state.count += payload.addNum;
}
}
...
// 在index.vue中引入mutation-types.js,調(diào)用時也做相關(guān)修改
addCount () {
this.$store.commit({
type: INCREMENT,
addNum:100
})
}
這樣在一個公共的地方管理mutations,如果注釋寫的清晰,項(xiàng)目人員可以很明晰知道各個mutations的作用。
同mapState,mapGetters,針對mutations,也有一個mapMutations
import { mapState, mapGetters, mapMutations } from 'vuex';
...
// 在methods中添加相關(guān)內(nèi)容,mapMutations要用在methods中
// 如果不需要傳值,直接使用this.INCREMENT();
methods: {
...mapMutations([INCREMENT]),
addCount () {
this.INCREMENT({addNum: 100});
}
mutations必須是同步函數(shù)
看官方的解釋似乎是因?yàn)槭褂没卣{(diào)函數(shù)后無法明確的監(jiān)聽到state的變化,所以針對mutations的函數(shù)只能是同步函數(shù),針對這個問題Vuex設(shè)定了actions
actions
- actions提交的是mutations,通過dispatch觸發(fā)
- actions支持異步函數(shù)
// 在index.js中的公共倉庫添加如下代碼
// context是一個與實(shí)例對象一致的對象,可以通過 context.commit 提交一個 mutation,或者通過 context.state 和 context.getters 來獲取 state 和 getters
actions: {
increment (context, payload) {
context.commit(INCREMENT, payload);
}
}
...
// 借用參數(shù)解構(gòu),代碼可以做如下優(yōu)化
// 獲取state,getters可以一樣使用參數(shù)解構(gòu)
increment ({commit}, payload) {
commit(INCREMENT, payload);
}
...
// 在index.vue的頁面方法做如下改造,actionst通過dispatch觸發(fā)
addCount () {
this.$store.dispatch('increment', {addNum: 100});
}
...
// 與commit類似,也可以使用下面的簡寫方式
this.$store.dispatch({type: 'increment', addNum: 100});
...
// 可以使用mapActions簡化代碼
// 引入mapActions函數(shù)
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex';
...
// 在methods下做下面修改
...mapActions(['increment']),
addCount () {
this.increment({addNum: 100});
}
在我的理解中,我認(rèn)為actions是對mutations的增強(qiáng),所以在實(shí)際使用中,更加習(xí)慣在actions中添加相關(guān)邏輯,并觸發(fā)mutations來更新state
Modules
上面例子中我們都是在index.js的公共倉庫中store來進(jìn)行相關(guān)操作,可以預(yù)期到,當(dāng)項(xiàng)目非常大時,把所有組件用到的state都放在一個stroe上會造成這個store過大,為了解決這個問題,可以把stroe按功能進(jìn)行拆分多個小的stroe然后在組合到一起進(jìn)行使用。
// 在index.js,新建兩個對象userStore和schoolStroe用來假定是用來存放用戶信息和學(xué)校信息
const userStore = {
state: {
name: 'lin ken from userStore',
address: '深圳XX區(qū)XX街道',
}
};
const schoolStore = {
state: {
department: '軟件學(xué)院',
classroom: 'C509'
}
};
// 在Vuex.Store的modules下,使用modules添加剛剛新建的子倉庫
const store = new Vuex.Store({
modules: {
userStore,
schoolStore
},
state: {
count: 0,
},
getters: {
userInfo (state) {
return `姓名:${state.name}; 住址:${state.address}`;
}
},
mutations: {
[INCREMENT] (state, payload) {
state.count += payload.addNum;
}
},
actions: {
increment ({commit}, payload) {
commit(INCREMENT, payload);
}
}
});
...
// 在index.vue的計(jì)算屬性中做如下改造,引入新建的子倉庫
userStoreInfo () {
return `姓名:${this.userStore.name};地址:${this.userStore.address}`;
},
schoolStoreInfo () {
return `學(xué)院:${this.schoolStore.department};班級:${this.schoolStore.classroom}`;
},
...mapState(['count', 'schoolStore', 'userStore'])
在實(shí)際項(xiàng)目中,我們可能會把子倉庫放到不同的文件下進(jìn)行管理
// 在項(xiàng)目目錄下新建兩個文件userStore.js和schoolStore.js
// userStore.js內(nèi)容如下
export const userStore = {
state: {
name: 'lin ken from userStore',
address: '深圳XX區(qū)XX街道',
}
};
...
// schoolStore.js內(nèi)容如下
export const schoolStore = {
state: {
department: '軟件學(xué)院',
classroom: 'C509'
}
};
...
// 在index.js目錄下引入剛剛新建的子倉庫
import {userStore} from './userStore';
import {schoolStore} from './schoolStore';
通過這種拆分管理,可以很方便的把各個子倉庫組合在一起,易于維護(hù)和使用。Vuex在實(shí)現(xiàn)子倉庫時只會把state按模塊名詞進(jìn)行劃分,其他的getters、actions是會合并在一起使用不會按模塊劃分。
// userStore.js做如下改造
export const userStore = {
state: {
name: 'lin ken from userStore',
address: '深圳XX區(qū)XX街道',
},
getters: {
rootCount (state, getters, rootState) {
return rootState.count;
}
},
mutations: {
changeName (state, payload) {
state.name = payload.name;
}
},
actions: {
changeName ({commit}, payload) {
commit('changeName', payload);
}
}
};
...
// index.vue的計(jì)算屬性中改造如下
...mapGetters(['rootCount']),
userStoreInfo () {
return `姓名:${this.userStore.name};地址:${this.userStore.address};from rootState:${this.rootCount}`;
}
...
// index.vue的方法改造如下
...mapActions(['increment', 'changeName']),
addCount () {
this.changeName({name: 'Rede'});
this.increment({addNum: 100});
}
通過上例子可以看出如果我們要調(diào)用子倉庫的相關(guān)state需要添加倉庫名:this.userStore.name/this.schoolStore.classroom,但是如果我們要調(diào)用子倉庫的getters、mutations、actions時無須添加子倉庫名,這時信息會合并在一起:
this.rootCount/this.changeName,這樣會造成子倉庫的getters,mutations,actions重名的情況
// 在schoolStore.js做如下修改
export const schoolStore = {
state: {
department: '軟件學(xué)院',
classroom: 'C509'
},
getters: {
rootCount (state) {
return state.classroom;
}
},
mutations: {
changeName (state, payload) {
state.department = payload.name;
}
},
actions: {
changeName ({commit}, payload) {
console.log('school store');
commit('changeName', payload);
}
}
};
...
// 在userStore.js做如下修改
actions: {
changeName ({commit}, payload) {
console.log('user store');
commit('changeName', payload);
}
}
這時頁面會提示rootCount是一個重復(fù)的getters關(guān)鍵字,但是針對重復(fù)的mutations和actions卻無法檢測出來,這個時候你點(diǎn)擊頁面的按鈕會發(fā)現(xiàn),我們在index.vue設(shè)定的要執(zhí)行changeName的方法,本意上你可能想要執(zhí)行userStore.js中actions,但因?yàn)闊o意間在schoolStore.js中也聲明了一個同名actions,這樣就造成了兩個actions同時觸發(fā)(通過控制臺我們能驗(yàn)證這一點(diǎn)),類似這種沒有明確報(bào)錯,完全因?yàn)槿藶樵蛟斐傻臒o意錯誤,在開發(fā)過程中是極其難維護(hù)的,尤其是在多人協(xié)作的項(xiàng)目中,所以總是建議針對mutations和actions定義的名詞(尤其是mutations,因?yàn)橐话闶窃诮M件內(nèi)直接觸發(fā)actions,actions重名的可能性要比mutations小)要存放在一個獨(dú)立的常量列表中,類似mutations-type.js。
除了把相關(guān)信息名獨(dú)立聲明外,還可以利用命名空間來解決這個問題,因?yàn)槟J(rèn)getters,mutations和actions這些值是綁定在全局聲明中,這樣可以方便調(diào)用。
// 在userStore.js中添加namespced為true,開啟這個命名空間
export const userStore = {
namespaced: true,
...
// 在index.vue頁面要做如下調(diào)整
// 這樣頁面使用的this.rootCount就指向userStore下的rootCount
...mapGetters({
'rootCount': 'userStore/rootCount'
}),
...
// 如果不像使用對象的形式還是保持使用字符串?dāng)?shù)組,可以如此引用
...mapActions(['increment', 'userStore/changeName']),
addCount () {
this['userStore/changeName']({name: 'Rede'});
this.increment({addNum: 100});
}
雖然this['userStore/changeName']使用起來并不是很方便,但是這樣的用法可以明確的指出這個方法的所屬,如果不想使用這么麻煩的寫法,還可以指定命名空間:
// 把所有綁定都綁定到userStore這個子倉庫下
...mapActions('userStore/', ['increment', 'changeName']),
addCount () {
this.changeName({name: 'Rede'});
this.increment({addNum: 100});
}
這樣對changeName的調(diào)用就可以直接調(diào)用userStore下的內(nèi)容,還可以借助createNamespacedHelpers來在全局自動添加
import { mapState, mapGetters, mapMutations } from 'vuex';
import { createNamespacedHelpers } from 'vuex';
const { mapActions } = createNamespacedHelpers('userStore/');
...
// 下面的代碼和最開始沒有加入命名空間時一致,看起來是改動量最小的
...mapActions(['increment', 'changeName']),
addCount () {
this.changeName({name: 'Rede'});
this.increment({addNum: 100});
}
不過因?yàn)檫@種添加是針對所有的綁定信息,所以increment也會指定到userStore這個子倉庫中,如果該子倉庫沒有這個方法就會報(bào)錯,如何使用這個內(nèi)容需要好好考慮下,不過如果想一起都要,也可以借助對象的形式來指定擴(kuò)展信息。
// 把頭部信息的引用改回去
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex';
...
// 相關(guān)內(nèi)容做如下改造
// 這樣使用即保證了不會影響全局命名,又能指定特定方法調(diào)用指定子倉庫,而且也方便隨時切換
...mapActions({
increment: 'increment',
changeName: 'userStore/changeName'
}),
addCount () {
this.changeName({name: 'Rede'});
this.increment({addNum: 100});
}
另外也可以針對這個同名的changeName,我們可以根據(jù)文件名的不同,分別定義為
USER_CHANGENAME,
SCHOOL_CHANGENAME
如何使用,就看你怎么考慮了。
在子倉庫中,如果要獲取主倉庫中的state,getters可以傳rootState,rootGetters這個參數(shù)
// getters下,rootState是第三個參數(shù),rootGetters是第四個參數(shù)
rootCount (state, getters, rootState, rootGetters)
// actions中,可以直接使用參數(shù)解構(gòu)的形式,設(shè)定這個值
changeName ({commit, rootState, rootGetters}, payload)
動態(tài)創(chuàng)建子倉庫
除了直接聲明好使用的子倉庫,還可以動態(tài)的生成
// 在index.vue通過registerModule創(chuàng)建一個子倉庫
mounted () {
this.$store.registerModule('myModule', {
state: {
info: 'from new module'
}
});
},
...
// 在相關(guān)方法中可以直接調(diào)用
addCount () {
this.changeName({name: 'Rede'});
this.increment({addNum: 100});
this.info = this.$store.state.myModule.info;
this.$store.unregisterModule('myModule');
console.log(this.$store.state.myModule);
}
這種臨時的子倉庫在子組件之間用來臨時通信是非常有用的,使用完成還可以調(diào)用unregisterModule直接刪除,為了避免臨時創(chuàng)建的子倉庫影響到靜態(tài)倉庫,還可以在指定子倉庫創(chuàng)建下一級倉庫
mounted () {
this.$store.registerModule('nested', {
state: {
info: 'nested'
}
});
this.$store.registerModule(['nested', 'myModule'], {
state: {
info: 'from new module'
}
});
},
...
// 直接調(diào)用
console.log(this.$store.state.nested.info);
console.log(this.$store.state.nested.myModule.info);
動態(tài)倉庫是臨時創(chuàng)建的,碰到同名的會直接覆蓋。
// 第二個myModule會覆蓋第一個,myModule中的state只包含一個name值
mounted () {
this.$store.registerModule('nested', {
state: {
info: 'nested'
}
});
this.$store.registerModule(['nested', 'myModule'], {
state: {
info: 'from new module'
}
});
this.$store.registerModule(['nested', 'myModule'], {
state: {
name: 'myModule'
}
});
},
如果希望這些動態(tài)創(chuàng)建的子倉庫可以疊加,可以添加preserveState參數(shù)
// 這時創(chuàng)建的myModule的state就包含info和name兩個值
this.$store.registerModule(['nested', 'myModule'], {
state: {
info: 'from new module'
}
});
this.$store.registerModule(['nested', 'myModule'], {
state: {
name: 'myModule'
}
}, { preserveState: true });
倉庫的復(fù)用
如果我們對一個組件進(jìn)行復(fù)用,
插件
Vuex支持自定義插件,通過插件可以監(jiān)聽狀態(tài)的變化。
// 在index.js中新建一個插件
// store是Vuex插件的唯一參數(shù)
// subscribe是在mutations發(fā)生變化時調(diào)用
// subscribeAction是action發(fā)生變化時調(diào)用
const myPlugin = store => {
// 當(dāng) store 初始化后調(diào)用
store.subscribe((mutation, state) => {
console.log('from myPlugin');
console.log(mutation);
});
store.subscribeAction((action, state) => {
console.log('from myPlugin');
console.log(action);
})
};
...
// 在初始化方法中通過plugins直接引用
const store = new Vuex.Store({
...
plugins: [myPlugin]
});
針對input的優(yōu)化
Vuex中會強(qiáng)制要求只通過mutations去修改state的值,如果我們把某個input的value值綁定為一個針對state的計(jì)算屬性,那如何去修改對應(yīng)在state中的值?
// 在index.vue的模板中輸入如下內(nèi)容
<input v-model="userStoreInfo"/>
...
userStoreInfo () {
return `${this.userStore.name}`;
},
這時如果我們嘗試修改input中的內(nèi)容,控制臺會直接報(bào)錯提示我們沒有設(shè)置setter,解決方案就是設(shè)置set方法,并通過actions去修改這個對應(yīng)的state
userStoreInfo: {
get () {
return `${this.userStore.name}`;
},
set (value) {
this.changeName({name: value});
}
},
官網(wǎng)上還提到另外一種綁定value去監(jiān)聽input方法,再更新的方案,感覺沒這樣直接
d