一開(kāi)始學(xué)習(xí)了一下 Vuex调窍,感覺(jué)比較冗余嗜愈,就自己做了一個(gè)輕量級(jí)的狀態(tài)管理旧蛾。
后來(lái)又學(xué)習(xí)了 Pinia莽龟,于是參考 Pinia 改進(jìn)了一下自己的狀態(tài)管理。
結(jié)合 Vuex 和 Pinia锨天, 保留需要的功能毯盈,去掉不需要的功能,修改一下看著不習(xí)慣的使用方法病袄,最后得到了一個(gè)滿足自己需要的輕量級(jí)狀態(tài)管理 —— nf - state奶镶。
設(shè)計(jì)思路
還是喜歡 MVC設(shè)計(jì)模式,狀態(tài)可以看做 M陪拘,組件是V厂镇,可以用 controller 做調(diào)度,需要訪問(wèn)后端的話左刽,可以做一個(gè) services捺信。這樣整體結(jié)構(gòu)比較清晰明了。
當(dāng)然簡(jiǎn)單的狀態(tài)不需要 controller欠痴,直接使用 getters迄靠、actions 即可。整體結(jié)構(gòu)如下:
源碼
https://gitee.com/naturefw-code/nf-rollup-state
在線演示
https://naturefw-code.gitee.io/nf-rollup-state/
在線文檔
https://nfpress.gitee.io/doc-nf-state
優(yōu)點(diǎn)
- 支持全局狀態(tài)和局部狀態(tài)喇辽;
- 可以像 Vuex 那樣掌挚,用 createStore 統(tǒng)一注冊(cè)全局狀態(tài) ;
- 也可以像 Pinia 那樣菩咨,用 defineStore 分散定義全局狀態(tài)和局部狀態(tài)吠式;
- 根據(jù)不同的場(chǎng)景需求,選擇適合的狀態(tài)變更方式(安全等級(jí))抽米;
- 可以和 Vuex特占、Pinia 共存;
- 數(shù)據(jù)部分和操作部分“分級(jí)”存放云茸,便于遍歷是目;
- 狀態(tài)采用 reactive 形式,可以直接使用 watch标捺、toRefs 等懊纳;
- 更輕、更小亡容、更簡(jiǎn)潔嗤疯;
- 可以記錄變化日志,也可以不記錄萍倡;
- 封裝了對(duì)象身弊、數(shù)組的一些方法,使用 reactive 的時(shí)候可以“直接”賦值。
缺點(diǎn)
- 不支持 option API阱佛、vue2帖汞;
- 暫時(shí)不支持 TypeScript;
- 暫時(shí)不支持 vue-devtool凑术;
- 不支持SSR翩蘸;
- 只有一個(gè)簡(jiǎn)單的狀態(tài)變化記錄(默認(rèn)不記錄)。
nf-state 的結(jié)構(gòu)
- state:支持對(duì)象淮逊、函數(shù)的形式催首。
- getters:會(huì)變成 computed,不支持異步(其實(shí)也可以用異步)泄鹏。
- actions:變更狀態(tài)郎任,支持異步。
- 內(nèi)置函數(shù):
- $state:整體賦值备籽。
- $patch:修改部分屬性舶治,支持深層。
- $reset:重置车猬。
本來(lái)想只保留 state 即可霉猛,但是看看 Pinia,感覺(jué)加上 getter珠闰、action 也不是不行惜浅,另外也參考 Pinia 設(shè)置了幾個(gè)內(nèi)置函數(shù)。
內(nèi)置函數(shù)
reactive 哪都好伏嗜,就是不能直接賦值坛悉,否則就會(huì)失去響應(yīng)性,雖然有辦法解決阅仔,但是需要多寫幾行代碼吹散,所以我們可以封裝一下。好吧八酒,是看到 Pinia 的 patch 后想到的刃唐。
$state
可以直接整體賦值羞迷,支持 object 和 數(shù)組。直接賦值即可画饥,這樣用起來(lái)就方便多了衔瓮。
this.dataList.$state = {xxx}
$patch
修改部分屬性。我們可以直接改狀態(tài)的屬性值抖甘,但是如果一次改多個(gè)的話热鞍,就有一點(diǎn)點(diǎn)麻煩,用$patch可以整潔一點(diǎn)。
// 依次設(shè)置屬性值:
this.pagerInfo.count = list.allCount === 0 ? 1 : list.allCount
this.pagerInfo.pagerIndex = 1
// 使用 $patch 設(shè)置屬性值:
this.pagerInfo.$patch({
count: list.allCount === 0 ? 1 : list.allCount,
pagerIndex: 1
})
支持深層屬性薇宠。
全局狀態(tài)的使用方式
全局狀態(tài)有兩種定義方式:
- 像 Vuex 那樣偷办,在 main.js 里面統(tǒng)一注冊(cè);
- 像 Pinia 那樣澄港,在組件里面定義椒涯。
在 main.js 里面統(tǒng)一注冊(cè)全局狀態(tài)
nf-state 的全局狀態(tài)的使用方法和 Vuex 差不多,先創(chuàng)建一個(gè) js文件回梧,定義一個(gè)或者多個(gè)狀態(tài)废岂,然后在main.js里面掛載。
優(yōu)點(diǎn):可以統(tǒng)一注冊(cè)狱意、便于管理湖苞,一個(gè)項(xiàng)目里有哪些全局狀態(tài),可以一目了然详囤。
- /store/index.js
// 定義全局狀態(tài)
import { createStore } from '@naturefw/nf-state'
/* 模擬異步操作 */
const testPromie = () => {
return new Promise((resolve) => {
setTimeout(() => {
const re = {
name: '異步的方式設(shè)置name'
}
resolve(re)
}, 500)
})
}
/**
* 統(tǒng)一注冊(cè)全局狀態(tài)财骨。key 相當(dāng)于 defineStore 的第一個(gè)參數(shù)(id)
*/
export default createStore({
// 定義狀態(tài),會(huì)變成 reactive 的形式纬纪。store 里面是各種狀態(tài)
store: {
// 如果只有 state蚓再,那么可以簡(jiǎn)化為一個(gè)對(duì)象的方式。
user: {
isLogin: false,
name: 'jyk', //
age: 19,
roles: []
},
// 有 getters包各、actions
userCenter: {
state: {
name: '',
age: 12,
list: []
},
getters: {
userName () {
return this.name + '---- 測(cè)試 getter'
}
},
actions: {
async loadData(val, state) {
const foo = await testPromie()
state.name = foo.name
this.name = foo.name
this.$state = foo
this.$patch(foo)
}
},
options: {
isLocal: false, // true:局部狀態(tài)摘仅;false:全局狀態(tài)(默認(rèn)屬性);
isLog: true, // true:做記錄问畅;false:不用做記錄(默認(rèn)屬性)娃属;
/**
* 1:寬松,可以各種方式改變屬性护姆,適合彈窗矾端、抽屜、多tab切換等卵皂。
* 2:一般秩铆,不能通過(guò)屬性直接改狀態(tài),只能通過(guò)內(nèi)置函數(shù)灯变、action 改變狀態(tài)
* 3:嚴(yán)格殴玛,不能通過(guò)屬性、內(nèi)置函數(shù)改狀態(tài)添祸,只能通過(guò) action 改變狀態(tài)
* 4:超嚴(yán)滚粟,只能在指定組件內(nèi)改變狀態(tài),比如當(dāng)前用戶的狀態(tài)刃泌,只能在登錄組件改凡壤,其他組件完全只讀署尤!
*/
level: 1
}
},
// 數(shù)組的情況
dataList: [123]
},
// 狀態(tài)初始化,可以給全局狀態(tài)設(shè)置初始狀態(tài)亚侠,支持異步曹体。
init (store) {
// 可以從后端API、indexedDB盖奈、webSQL等混坞,設(shè)置狀態(tài)的初始值。
}
})
- main.js
import { createApp } from 'vue'
import App from './App.vue'
import store from './store'
createApp(App)
.use(store)
.mount('#app')
在組件里獲取統(tǒng)一注冊(cè)的全局狀態(tài)
使用方法和 Vuex 類似钢坦,直接獲取全局狀態(tài):
import { store } from '@naturefw/nf-state'
const { user, userCenter } = store
在組件里注冊(cè)全局狀態(tài)
這種方式究孕,借鑒了Pinia的方式,我們可以建立一個(gè) js 文件爹凹,然后定義一個(gè)狀態(tài)厨诸,可以用Symbol 作為標(biāo)志,這樣可以更方便的避免重名禾酱。(當(dāng)然也可以用 string)
import { defineStore } from '@naturefw/nf-state'
const flag = Symbol('UserInfo')
// const flag = 'UserInfo'
const getUserInfo = () => defineStore(flag, {
state: {
name: '客戶管理',
info: {}
},
getters: {
},
actions: {
updateName(val) {
this.name = val
}
}
})
export {
flag,
getUserInfo
}
雖然使用 Symbol 可以方便的避免重名微酬,但是獲取狀態(tài)的時(shí)候有點(diǎn)小麻煩。
ID(狀態(tài)標(biāo)識(shí))支持 string 和 Symbol 颤陶,大家可以根據(jù)自己的情況選擇適合的方式颗管。
在組件里面引入 這個(gè)js文件,然后可以通過(guò) getUserInfo 函數(shù)獲取狀態(tài)滓走,可以用統(tǒng)一注冊(cè)的全局狀態(tài)的方式獲取垦江。
使用局部狀態(tài)
基于 provide/inject 設(shè)置了局部狀態(tài)。
有時(shí)候搅方,一個(gè)狀態(tài)并不是整個(gè)項(xiàng)目都需要訪問(wèn)比吭,這時(shí)候可以采用局部狀態(tài),比如列表頁(yè)面里的狀態(tài)姨涡。
定義一個(gè)局部狀態(tài)
我們可以建立一個(gè)js文件衩藤,定義狀態(tài):
- state-list.js
import { watch } from 'vue'
import { defineStore, useStore, store } from '@naturefw/nf-state'
const flag = Symbol('pager001')
// const flag = 'pager001'
/**
* 注冊(cè)局部狀態(tài),父組件使用 provide
* * 數(shù)據(jù)列表用
* @returns
*/
const regListState = () => {
// 定義 列表用的狀態(tài)
const state = defineStore(flag, {
state: () => {
return {
moduleId: 0, // 模塊ID
dataList: [], // 數(shù)據(jù)列表
findValue: {}, // 查詢條件的精簡(jiǎn)形式
findArray: [], // 查詢條件的對(duì)象形式
pagerInfo: { // 分頁(yè)信息
pagerSize: 5,
count: 20, // 總數(shù)
pagerIndex: 1 // 當(dāng)前頁(yè)號(hào)
},
selection: { // 列表里選擇的記錄
dataId: '', // 單選ID number 涛漂、string
row: {}, // 單選的數(shù)據(jù)對(duì)象 {}
dataIds: [], // 多選ID []
rows: [] // 多選的數(shù)據(jù)對(duì)象 []
},
query: {} // 查詢條件
}
},
actions: {
/**
* 加載數(shù)據(jù)赏表,
* @param {*} isReset true:需要設(shè)置總數(shù),頁(yè)號(hào)設(shè)置為1匈仗;false:僅翻頁(yè)
*/
async loadData (isReset = false) {
// 獲取列表數(shù)據(jù)
const list = await xxx
// 使用 $state 直接賦值
this.dataList.$state = list.dataList
if (isReset) {
this.pagerInfo.$patch({
count: list.allCount === 0 ? 1 : list.allCount,
pagerIndex: 1
})
}
}
}
},
{ isLocal: true } // 設(shè)置為局部狀態(tài)底哗,沒(méi)有設(shè)置的話,就是全局狀態(tài)了锚沸。
)
// 初始化
state.loadData(true)
// 監(jiān)聽(tīng)頁(yè)號(hào),實(shí)現(xiàn)翻頁(yè)功能
watch(() => state.pagerInfo.pagerIndex, (index) => {
state.loadData()
})
// 監(jiān)聽(tīng)查詢條件涕癣,實(shí)現(xiàn)查詢功能哗蜈。
watch(state.findValue, () => {
state.loadData(true)
})
return state
}
/**
* 子組件用 inject 獲取狀態(tài)
* @returns
*/
const getListState = () => {
return useStore(flag)
}
export {
getListState,
regListState
}
是不是應(yīng)該把 watch 也內(nèi)置了前标?
在父組件引入局部狀態(tài)
建立父組件,使用 getListState 引入局部狀態(tài):
- data-list.vue
// 引入
import { regListState } from './controller/state-list.js'
// 注冊(cè)狀態(tài)
const state = regListState()
調(diào)用 getListState() 會(huì)用 provide 設(shè)置一個(gè)狀態(tài)距潘。
在子組件里獲取局部狀態(tài)
建立子組件炼列,獲取局部狀態(tài):
- pager.vue
// 局部狀態(tài)
import { getListState } from '../controller/state-list.js'
// 獲取父組件提供的局部狀態(tài)
const state = getListState()
調(diào)用 getListState(), 內(nèi)部會(huì)用 inject (注入)獲取父組件的局部狀態(tài)音比。這樣使用起來(lái)就比較明確俭尖,也比較簡(jiǎn)單。
子組件也可以調(diào)用 regListState 洞翩,這樣可以注冊(cè)一個(gè)子組件的狀態(tài)稽犁,子子組件只能獲取子組件的狀態(tài)。
子子組件如果想獲取父組件的狀態(tài)骚亿,那么需要設(shè)置不同的ID已亥。
安全等級(jí)
變更狀態(tài)可以有四個(gè)安全級(jí)別:寬松、一般来屠、嚴(yán)格虑椎、超嚴(yán)。
安全級(jí)別 | state類型 | 直接改屬性 | 內(nèi)置函數(shù) | action | 范圍 | 舉例 |
---|---|---|---|---|---|---|
寬松 | reactive | ? | ? | ? | 所有組件 | 彈窗俱笛、抽屜的狀態(tài) |
一般 | readonly | ? | ? | ? | 所有組件 | |
嚴(yán)格 | readonly | ? | ? | ? | 所有組件 | |
超嚴(yán) | readonly | ? | ? | ? | 特定組件才可更改 | 當(dāng)前用戶狀態(tài) |
寬松:任何組件里都可以通過(guò)屬性捆姜、內(nèi)置函數(shù)和 action 來(lái)更改狀態(tài)。
比如彈窗狀態(tài)(是否打開(kāi))迎膜、抽屜狀態(tài)(是否打開(kāi))泥技、tab標(biāo)簽的切換等。
這些場(chǎng)景里星虹,如果可以直接修改屬性的話零抬,那么可以讓代碼更簡(jiǎn)潔。一般和嚴(yán)格:二者主要區(qū)別是宽涌,內(nèi)置函數(shù)是否可以使用的問(wèn)題平夜,其實(shí)一開(kāi)始不想?yún)^(qū)分的,但是想想還是先分開(kāi)的話卸亮,畢竟多提供了一個(gè)選擇忽妒。
超嚴(yán):只能在特定的組件里改變狀態(tài),其他組件只能讀取狀態(tài)兼贸。
比如當(dāng)前訪問(wèn)者的狀態(tài)段直,只有在登錄組件、退出組件里改變溶诞,其他組件不能更改鸯檬。
這樣可以更好的適應(yīng)不同的場(chǎng)景需求。
和 Pinia 的區(qū)別
nf-state 看起來(lái)和 Pnina 挺像的螺垢,那么有哪些區(qū)別呢喧务?
局部狀態(tài)
Pinia 都是 全局狀態(tài)赖歌,沒(méi)有局部狀態(tài),或者說(shuō)功茴,局部狀態(tài)比較簡(jiǎn)單庐冯,似乎不用特殊處理,只是坎穿,既然都封裝了展父,那么就做全套吧,統(tǒng)一封裝玲昧,統(tǒng)一使用風(fēng)格栖茉。
狀態(tài)的結(jié)構(gòu)
雖然都是 reactive 的形式,但是內(nèi)部結(jié)構(gòu)的層次不一樣酌呆。
pinia 的狀態(tài)衡载,數(shù)據(jù)部分和操作部分都在一個(gè)層級(jí)里面,感覺(jué)有點(diǎn)分布清楚隙袁,所以 pinia 提供了 來(lái)實(shí)現(xiàn) toRefs 的功能痰娱。
我還是喜歡那種層次分明的形式,比如這樣:
這樣設(shè)計(jì)層次很清晰菩收,可以直接使用 toRefs 實(shí)現(xiàn)解構(gòu)梨睁,而不會(huì)解構(gòu)出來(lái)“不需要”的方法。
支持的功能
官方提供的狀態(tài)管理需要滿足各種需求娜饵,所以要支持 option API坡贺、vue2、TypeScript等箱舞。
而我自己做的狀態(tài)管理遍坟,滿足自己的需求即可,所以可以更簡(jiǎn)潔晴股,當(dāng)然可能無(wú)法滿足你的需求愿伴。
可以不重復(fù)制造輪子,但是要擁有制造輪子的能力电湘。做一個(gè)狀態(tài)管理隔节,可以培養(yǎng)這種能力。