大綱 本文內容更多的是講講使用 vuex 的一些心得想法,所以大概會講述下麵這些點: Q1:我為什麼會想使用 vuex 來管理數據狀態交互? Q2:使用 vuex 框架有哪些缺點或者說副作用? Q3:我是如何在項目里使用 vuex 的? 初識 vuex 對於 vuex,有人喜歡,有人反感 喜歡的人覺 ...
大綱
本文內容更多的是講講使用 vuex 的一些心得想法,所以大概會講述下麵這些點:
Q1:我為什麼會想使用 vuex 來管理數據狀態交互?
Q2:使用 vuex 框架有哪些缺點或者說副作用?
Q3:我是如何在項目里使用 vuex 的?
初識 vuex
對於 vuex,有人喜歡,有人反感
喜歡的人覺得它可以很好的解決複雜的數據交互場景
反感的人覺得它有各種缺點:繁瑣冗餘的代碼編寫、維護性差的字元串形式變數註入、過於依賴 vue 框架導致非同步擴展場景差
這其中,有一個很模糊的點,複雜的數據交互場景並沒有一個衡量標準,每個人都有自己的見解
再加上不同人有著不同的項目經歷,這就造成了經常會出現有趣的現象:你體會不到我為什麼非要使用 vuex,他理解不了這種場景何須使用 vuex,我也講不明白選擇 vuex 的緣由
借用官網文檔一句話:
您自會知道什麼時候需要它
很玄乎,更通俗來講就是,多踩點坑,多遭遇些痛點,當你的最後一根稻草被壓垮時,自然就會去尋找更好的方案解決
我一直都不喜歡 vuex,因為我覺得它的 mapMutations
或者 mapState
註入到 vue 里的變數和方法都是字元串,極大的破壞了代碼的可讀性和維護性,沒辦法通過 idea 快速的跳轉到變數定義的地方
當然,你也可以定義一些靜態變數來替換這些字元串就可以解決跳轉問題,但代價就是代碼更繁瑣了,本來使用 vuex 時就需要寫一堆繁瑣的代碼,這下更麻煩
還有一個不想使用 vuex 的原因是因為我的項目業務邏輯挺複雜,除了 vue 單文件外,項目里還劃分了來負責業務邏輯或非同步任務的 js 層代碼,而 vuex 是為 vue 框架而設計的,存放在 vuex 數據中心的變數可以通過它的一些工具方法來註入到 vue 組件的 computed 計算屬性里方便直接使用,比如
import { mapState } from 'vuex'
export default {
// 映射 this.count 為 store.state.count
computed: mapState({
count: state => state.count
})
}
但如果想在 js 文件里使用 vuex 里的數據,就會比較繁瑣:
import store from 'store'
console.log(store.state.count);
基於以上種種原因,我遲遲未在項目里使用 vuex
那麼,我最後為什麼又選擇使用了 vuex 呢?
一,項目的一些數據交互場景使用 vue 原生的輸入輸出方案已經忍不下去了
二,我想辦法解決了我沒法忍受的 vuex 的幾個缺點了
三,這是個新項目,並沒有複雜的業務場景,項目基本由 vue 單文件來書寫即可
簡單來說,就是有個新項目符合適用 vuex 的場景,而且一些組件交互場景使用原生方案過於繁瑣,vuex 剛好能夠解決這個問題,雖然 vuex 有一定的使用成本,但這些缺點恰好又被我想了一些法子解決或簡化掉
這樣一來,引入 vuex 既能解決我的訴求,又不會引入太多我無法接受的缺點,那自然可以來玩一玩
背景
vue 框架是基於組件機制來組裝成頁面,所以頁面數據是分散到各個組件間,而組件間的數據交互使用的是 vue 自帶的輸入(props)輸出($emit)機制
這種數據交互方案有個特點,數據對象都存儲在組件內部,交互需要通過輸入和輸出
而輸入輸出機制有個缺點:頁面複雜時,經常需要層層傳遞數據,因為非父子組件間的交互,只能尋找最近的祖先組件來做中轉
同時,輸入輸出機制還有一個局限性:當不同頁面的組件需要進行數據交互時,它就無能為力了
平常開發,這種輸入輸出的方案也基本滿足了
但如果有需要跨頁面的數據交互或者說,有需要將數據做持久化處理的場景時;以及如果頁面組件層次複雜,存在props 和 $emit 層層傳遞時,那這時候如果忍不了輸入輸出方案的用法,那麼就可以研究新方案了
解決這種場景的方案很多,但從本質上來講,都可以統歸為:數據中心方案
這種方案思路就是將數據對象從組件內部移出到外部存儲維護,需要使用哪個數據變數的組件自行去數據中心取
vue 其實也有機制可以達到這種效果,如:依賴註入,但慎用,太破壞數據流的走向了
我們也可以自己用 js 來實現一個數據中心,專門建個 js 文件來存儲維護數據模型,需要數據變數的組件引入 js 文件來讀取
但每個數據中心都必須解決兩個問題:數據復用和數據污染,通俗來講就是數據初始化和重置,也就是數據的生命周期
數據復用是為了確保不同組件間從數據中心裡讀取時,是同一份數據副本,這才能達到數據交互目的
而數據污染是指不同模塊間使用同個數據中心時,數據模型是否可以達到相互獨立,互不影響的效果,這通常是某個功能在不同模塊間被覆用時會出現的場景;如果這種場景不好理解,那麼也可以想想這種場景:再次載入該頁面,組件再次被創建後,從數據中心裡讀取的數據副本是否是相互獨立的
如果數據存儲在 vue 組件內部,那數據的生命周期就是跟隨著組件的創建和銷毀,這也是為什麼 data 是一個返回對象的函數,因為這樣可以藉助 js 的函數作用域機制來解決數據的復用和污染問題
但數據從 vue 組件內部移出,存儲到數據中心,那麼這些處理就需要自己來實現
所以,數據中心並不是簡單建個 js 類,裡面聲明下數據對象就完事了的
基於此,我選擇使用 vuex
vuex 副作用
先看個使用 vuex 的簡單例子:
// 聲明
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
increment (state) {
// 變更狀態
state.count++
}
}
})
// vue 里使用
import { mapMutations } from 'vuex'
import { mapState } from 'vuex'
export default {
// ...
computed: {
...mapState({
// 將 `this.count` 映射為 `this.$store.state.count`
count: state => state.count
})
},
methods: {
...mapMutations([
'increment', // 將 `this.increment()` 映射為 `this.$store.commit('increment')`
])
}
}
僅僅簡單定義個數據對象,就需要聲明對象模型 state,聲明對象操作方法 mutation,然後在相應的 vue 組件內先通過 mapState 註入變數的讀方法,再通過 mapMutations 註入變數的寫方法
而以上這麼多繁瑣的代碼,在原本的 vue 機制里,就是簡單的在 data 里聲明下變數就完事,這一對比,vuex 的使用上,複雜度和繁瑣度很大,有一定的使用成本
所以很多人不喜歡用它,官方也說簡單的頁面也沒有必要去使用它
這是我覺得 vuex 的第一個缺點,或者說副作用:繁瑣冗餘的代碼編寫
第二個我覺得 vuex 的缺點就是,mapState 或 mapMutation 註入的變數,都是字元串的
字元串就意味著,你在 vue 單文件內其他地方通過 this.xxx 使用這些變數時,當你想查看變數的聲明時,idea 無法識別!
這是我特別無法接受的一點,降低我的維護、開發效率
不過這點因人而異,有人覺得它不是個問題,或者使用個靜態變數來替換字元串也可以解決,但這些我個人是沒辦法接受
然而 vue 原生輸入輸出的數據交互又不足夠支撐我的一些需求場景,自己用 js 實現個數據中心吧,又擔心沒強制規範,沒處理好,後期跑偏掉更難維護,那就想想辦法搞定 vuex 的這兩個缺點吧
如何更簡易的使用 vuex
先說下,我雖然用了些方法,讓我使用 vuex 可以達到我的預期,既滿足我的需求場景,又不至於引入太多副作用
但實際上,這種方式也許就偏離了 vuex 官方的推薦方式了,別人不一定能接受我的這種用法
所以,這篇更多的是分享我的一些思路和想法,有一說一,並不通用,歡迎拍磚
就我個人對於 vuex 的缺點,我所不能接受的就兩點:
- 繁瑣冗餘的代碼編寫
- 維護性差、可讀性差的字元串變數註入
那麼,就是得想辦法解決這兩個問題,先來說第一個
封裝自動生成代碼解決 vuex 使用繁瑣問題
用 vuex 需要編寫很多繁瑣的代碼,這些代碼是少不了的,既然少不了,那換個思路,不用我來編寫不就好了
想辦法提取共性,封裝個工具方法,讓它來生成每次使用 vuex 的那些繁瑣代碼,這樣一來,使用就方便了
state 里聲明的數據對象模型,這些代碼是沒辦法自動生成的,畢竟數據模型都不大一樣
而修改數據變數的 mutation 代碼就可以想辦法來自動生成了
/**
* 根據 state 對象屬性自動生成 mutations 更新屬性的方法,如:
* state: {
* projectId: '',
* searchParams: {
* batchId: ''
* }
* }
*
* ===>
*
* {
* updateProjectId: (state, payload) => { state.projectId = payLoad }
* updateSearchParams: (state, payload) => { state.searchParams = {...state.searchParams, ...payload} }
* updateBatchId: (state, payload) => { state.searchParams.batchId = payload }
* }
*
* 非對像類型的屬性直接生成賦值操作,對象類型屬性會通過擴展運算符重新生成對象
* 且會遞歸處理內部對象的屬性,扁平化的生成 updateXXX 方法掛載到 mutations 對象上
* @param {Object} stateTemplate
*/
export function generateMutationsByState(stateTemplate) {
let handleInnerObjState = (parentKeyPath, innerState, obj) => {
Object.keys(innerState).forEach(key => {
let value = innerState[key];
let updateKey = `update${key[0].toUpperCase()}${key.substr(1)}`;
if (typeof value === 'object' && value != null && !Array.isArray(value)) {
obj[updateKey] = (state, payload) => {
let target = state;
for (let i = 0; i < parentKeyPath.length; i++) {
target = target[parentKeyPath[i]];
}
target[key] = { ...target[key], ...payload };
};
handleInnerObjState([...parentKeyPath, key], value, obj);
} else {
obj[updateKey] = (state, payload) => {
let target = state;
for (let i = 0; i < parentKeyPath.length; i++) {
target = target[parentKeyPath[i]];
}
target[key] = payload;
};
}
});
};
let mutations = {};
Object.keys(stateTemplate).forEach(key => {
let obj = {};
let value = stateTemplate[key];
let updateKey = `update${key[0].toUpperCase()}${key.substr(1)}`;
if (typeof value === 'object' && value != null && !Array.isArray(value)) {
obj[updateKey] = (state, payload) => {
state[key] = { ...state[key], ...payload };
};
handleInnerObjState([key], value, obj);
} else {
obj[updateKey] = (state, payload) => {
state[key] = payload;
};
}
Object.assign(mutations, obj);
});
return mutations;
}
然後是 mapState 和 mapMutation 註入到 vue 組件的這些代碼也可以通過 computed 計算屬性的特性來自動生成,這樣使用上更加方便,畢竟使用 computed 計算屬性的方式就跟使用 data 里聲明的變數一樣,沒有什麼區別
import store from './index';
/**
* 將 store 里指定的 state 轉成計算屬性 computed 的 set() get()
* vue 里就可以直接類似操作 data 屬性一樣使用 state
*
* @param {String} moduleName state 所屬的 store 的 module 名
* @param {Array} states 待處理的 states e.g: ['project', 'searchParams.projectName'] 其中,
* 掛載在 computed 上的屬性名,預設等於 state,當 state 結構多層時,取最後一層的屬性名
*
* ps: state 對應的 mutation 必須以 updateXXX 方式命名
*/
export function storeToComputed(moduleName, states) {
if (!store) {
throw new TypeError('store is null');
}
if (!moduleName) {
throw new TypeError("state's module name is null");
}
if (!states || !Array.isArray(states) || states.length === 0) {
throw new TypeError('states is null or not array');
}
let computed = {};
states.forEach(state => {
if (state.indexOf('.') !== -1) {
let _states = state.split('.');
let _key = _states[_states.length - 1];
computed[_key] = {
get() {
let res = store.state[moduleName];
for (let i = 0; i < _states.length; i++) {
res = res[_states[i]];
}
return res;
},
set(value) {
store.commit(
`${moduleName}/update${_key[0].toUpperCase()}${_key.substr(1)}`,
value
);
},
};
} else {
computed[state] = {
get() {
return store.state[moduleName][state];
},
set(value) {
store.commit(
`${moduleName}/update${state[0].toUpperCase()}${state.substr(1)}`,
value
);
},
};
}
});
return computed;
}
那麼最終可以達到的效果就是:
- 只需在 store 文件里聲明 state 數據變數
- 然後再需要註入的 vue 組件里註入即可
// 聲明
import { generateMutationsByState } from './helper';
const global = {
state: {
count: 0
}
}
global.mutations = generateMutationsByState(global.state);
const store = new Vuex.Store({
modules: {
global
}
})
// vue里使用
import { storeToComputed } from '@/store/storeToComputed';
export default {
// ...
computed: {
// 將 this.$store.state.global.count 映射成 this.count
...storeToComputed('global', ['count'])
},
}
我的這種用法,其實就只是單純將 vuex 拿來作為數據中心使用而已,在 store 文件里不編寫邏輯代碼,也不使用 action
這種用法的好處,我是覺得,會跟原本在 vue 的 data 里聲明變數後的用法比較類似。因為就是將原本定義在 data 里的變數換成定義在專門的 store 文件里,然後再多一步將變數通過工具方法註入到 vue 的 computed 里,接下去的使用變數的任何場景,在哪賦值,在哪取值,哪裡處理非同步請求等等的代碼,原本怎麼寫,現在還是怎麼寫,完全不影響
這就意味著,這種方案後續如果有缺陷,或者用不習慣,那麼想切換到 vue 原生的輸入輸出方案非常方便,影響點、改動點都會比較少,就是將 storeToComputed 註入到 computed 的變數換到 data 就完事了
甚至說,後續想換掉 vuex 也會比較方便,畢竟只是單純用它當做數據中心而已
然後再配合上 vuex 的動態掛載和卸載的用法,這個數據中心就可以像 angular 框架那樣做到精確控制數據對象的作用域和生命周期,全局共用、模塊間共用、頁面內共用、組件內共用等都可以很方便做到,這樣一來,數據交互就不怕複雜場景了
這是我之所以會這麼使用 vuex 的考慮
自定義 vscode 插件解決字元串變數的跳轉問題
繁瑣的代碼編寫問題搞定了,接下去就是看看怎麼解決字元串變數註入的跳轉問題了
先來說說,我為什麼會在意變數支不支持利用 idea 直接跳轉到聲明的地方
這是因為,有些頁面比較複雜,數據變數比較多,或者時間久了,很容易忘記一些變數的命名、含義
而我們通常都只會在聲明的地方加上一些註釋
所以利用 idea 直接快速跳轉到聲明的地方,第一,有註釋可以快速幫助回憶、理清變數含義;第二,忘記變數命名全稱可以快速複製使用;第三,方便我查看其它數據變數
那麼,怎麼解決這個問題呢?
自然就是自己擴展開發個 vscode 插件來支持了,面向百度的話,vscode 插件開發並不困難,看幾篇教程,清楚插件的生命周期和一些 API 就可以上手了
關鍵是,如何識別 vuex 註入的這些變數?如何跳轉到 store 文件里聲明數據變數的 state 位置?
如果想做成通用的插件,那可能需要多花點精力
但如果只是基於自己當前的項目來解決這個問題,那就簡單多了,因為項目有一定的規範,比如 vuex 的 store 文件存放的目錄地址,比如註入到 vue 組件里的使用方式,這些都是有規範和規律的
簡單說下我的思路:
- 先掃描項目 store 目錄下文件,識別出有數據模型 (state) 的文件,解析並存儲數據模型各個變數名和位置
- 註冊 vscode 的變數操作響應,當按住 ctrl 並將滑鼠移到變數上時,響應我們的插件程式
- 判斷當前聚焦操作的變數是否是通過在 computed 里註入的變數,是則繼續下一步尋找變數聲明的文件位置
- 通過變數名和模塊名到 store 里匹配變數,匹配到後,記錄變數的聲明信息和文件位置,當點擊左鍵時,響應跳轉行為
總結
最後簡單總結下,項目里並不是必須要使用 vuex,vuex 所解決的場景,用 vue 原生的輸入輸出機制想想辦法也能解決,區別可能就是代碼的可讀性、維護性上的區別,數據流走向是否清晰等
vuex 作為三方庫,自然就是一個可選的服務,用不用,怎麼用,都因人而異;考慮好自己的訴求,對比好引入前後的影響點,權衡好自己能接受的點就好
比如我,使用 vuex 的方式上說得難聽點,也有點不倫不類,畢竟並沒有按照官方示例來使用,反而是自己搞了套使用規範,這也增加了別人的上手成本
所以寫這篇,不在於強推使用 vuex,只是從自己的一些經歷分享自己使用一些三方庫的心路歷程,所思所想
很多時候,當你開始吐槽某某方案、當你開始無法接受某某用法時,這其實意味著,這是一次絕佳的探索機會
吐槽完就想辦法去優化、去尋找新方案;接受不了時,就想辦法去研究看能否解決這些痛點
人嘛,總是在一次次的踩進坑裡,再爬出來