一開始學習了一下 Vuex,感覺比較冗餘,就自己做了一個輕量級的狀態管理。
後來又學習了 Pinia,於是參考 Pinia 改進了一下自己的狀態管理。
結合 Vuex 和 Pinia, 保留需要的功能,去掉不需要的功能,修改一下看著不習慣的使用方法,最後得到了一個滿足自己需要的輕量級狀態管理 —— nf - state。
設計思路
還是喜歡 MVC設計模式,狀態可以看做 M,元件是V,可以用 controller 做排程,需要訪問後端的話,可以做一個 services。這樣整體結構比較清晰明瞭。
當然簡單的狀態不需要 controller,直接使用 getters、actions 即可。整體結構如下:
原始碼
https://gitee.com/naturefw-code/nf-rollup-state
線上演示
https://naturefw-code.gitee.io/nf-rollup-state/
線上文件
https://nfpress.gitee.io/doc-nf-state
優點
- 支援全域性狀態和區域性狀態;
- 可以像 Vuex 那樣,用 createStore 統一註冊全域性狀態 ;
- 也可以像 Pinia 那樣,用 defineStore 分散定義全域性狀態和區域性狀態;
- 根據不同的場景需求,選擇適合的狀態變更方式(安全等級);
- 可以和 Vuex、Pinia 共存;
- 資料部分和操作部分“分級”存放,便於遍歷;
- 狀態採用 reactive 形式,可以直接使用 watch、toRefs 等;
- 更輕、更小、更簡潔;
- 可以記錄變化日誌,也可以不記錄;
- 封裝了物件、陣列的一些方法,使用 reactive 的時候可以“直接”賦值。
缺點
- 不支援 option API、vue2;
- 暫時不支援 TypeScript;
- 暫時不支援 vue-devtool;
- 不支援SSR;
- 只有一個簡單的狀態變化記錄(預設不記錄)。
nf-state 的結構
- state:支援物件、函式的形式。
- getters:會變成 computed,不支援非同步(其實也可以用非同步)。
- actions:變更狀態,支援非同步。
- 內建函式:
- $state:整體賦值。
- $patch:修改部分屬性,支援深層。
- $reset:重置。
本來想只保留 state 即可,但是看看 Pinia,感覺加上 getter、action 也不是不行,另外也參考 Pinia 設定了幾個內建函式。
內建函式
reactive 哪都好,就是不能直接賦值,否則就會失去響應性,雖然有辦法解決,但是需要多寫幾行程式碼,所以我們可以封裝一下。好吧,是看到 Pinia 的 $state、$patch 後想到的。
$state
可以直接整體賦值,支援 object 和 陣列。直接賦值即可,這樣用起來就方便多了。
this.dataList.$state = {xxx}
$patch
修改部分屬性。我們可以直接改狀態的屬性值,但是如果一次改多個的話,就有一點點麻煩,用$patch可以整潔一點。
// 依次設定屬性值:
this.pagerInfo.count = list.allCount === 0 ? 1 : list.allCount
this.pagerInfo.pagerIndex = 1
// 使用 $patch 設定屬性值:
this.pagerInfo.$patch({
count: list.allCount === 0 ? 1 : list.allCount,
pagerIndex: 1
})
支援深層屬性。
全域性狀態的使用方式
全域性狀態有兩種定義方式:
- 像 Vuex 那樣,在 main.js 裡面統一註冊;
- 像 Pinia 那樣,在元件裡面定義。
在 main.js 裡面統一註冊全域性狀態
nf-state 的全域性狀態的使用方法和 Vuex 差不多,先建立一個 js檔案,定義一個或者多個狀態,然後在main.js裡面掛載。
優點:可以統一註冊、便於管理,一個專案裡有哪些全域性狀態,可以一目瞭然。
- /store/index.js
// 定義全域性狀態
import { createStore } from '@naturefw/nf-state'
/* 模擬非同步操作 */
const testPromie = () => {
return new Promise((resolve) => {
setTimeout(() => {
const re = {
name: '非同步的方式設定name'
}
resolve(re)
}, 500)
})
}
/**
* 統一註冊全域性狀態。key 相當於 defineStore 的第一個引數(id)
*/
export default createStore({
// 定義狀態,會變成 reactive 的形式。store 裡面是各種狀態
store: {
// 如果只有 state,那麼可以簡化為一個物件的方式。
user: {
isLogin: false,
name: 'jyk', //
age: 19,
roles: []
},
// 有 getters、actions
userCenter: {
state: {
name: '',
age: 12,
list: []
},
getters: {
userName () {
return this.name + '---- 測試 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:區域性狀態;false:全域性狀態(預設屬性);
isLog: true, // true:做記錄;false:不用做記錄(預設屬性);
/**
* 1:寬鬆,可以各種方式改變屬性,適合彈窗、抽屜、多tab切換等。
* 2:一般,不能通過屬性直接改狀態,只能通過內建函式、action 改變狀態
* 3:嚴格,不能通過屬性、內建函式改狀態,只能通過 action 改變狀態
* 4:超嚴,只能在指定元件內改變狀態,比如當前使用者的狀態,只能在登入元件改,其他元件完全只讀!
*/
level: 1
}
},
// 陣列的情況
dataList: [123]
},
// 狀態初始化,可以給全域性狀態設定初始狀態,支援非同步。
init (store) {
// 可以從後端API、indexedDB、webSQL等,設定狀態的初始值。
}
})
- main.js
import { createApp } from 'vue'
import App from './App.vue'
import store from './store'
createApp(App)
.use(store)
.mount('#app')
在元件裡獲取統一註冊的全域性狀態
使用方法和 Vuex 類似,直接獲取全域性狀態:
import { store } from '@naturefw/nf-state'
const { user, userCenter } = store
在元件裡註冊全域性狀態
這種方式,借鑑了Pinia的方式,我們可以建立一個 js 檔案,然後定義一個狀態,可以用Symbol 作為標誌,這樣可以更方便的避免重名。(當然也可以用 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 可以方便的避免重名,但是獲取狀態的時候有點小麻煩。
ID(狀態標識)支援 string 和 Symbol ,大家可以根據自己的情況選擇適合的方式。
在元件裡面引入 這個js檔案,然後可以通過 getUserInfo 函式獲取狀態,可以用統一註冊的全域性狀態的方式獲取。
使用區域性狀態
基於 provide/inject 設定了區域性狀態。
有時候,一個狀態並不是整個專案都需要訪問,這時候可以採用區域性狀態,比如列表頁面裡的狀態。
定義一個區域性狀態
我們可以建立一個js檔案,定義狀態:
- state-list.js
import { watch } from 'vue'
import { defineStore, useStore, store } from '@naturefw/nf-state'
const flag = Symbol('pager001')
// const flag = 'pager001'
/**
* 註冊區域性狀態,父元件使用 provide
* * 資料列表用
* @returns
*/
const regListState = () => {
// 定義 列表用的狀態
const state = defineStore(flag, {
state: () => {
return {
moduleId: 0, // 模組ID
dataList: [], // 資料列表
findValue: {}, // 查詢條件的精簡形式
findArray: [], // 查詢條件的物件形式
pagerInfo: { // 分頁資訊
pagerSize: 5,
count: 20, // 總數
pagerIndex: 1 // 當前頁號
},
selection: { // 列表裡選擇的記錄
dataId: '', // 單選ID number 、string
row: {}, // 單選的資料物件 {}
dataIds: [], // 多選ID []
rows: [] // 多選的資料物件 []
},
query: {} // 查詢條件
}
},
actions: {
/**
* 載入資料,
* @param {*} isReset true:需要設定總數,頁號設定為1;false:僅翻頁
*/
async loadData (isReset = false) {
// 獲取列表資料
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 } // 設定為區域性狀態,沒有設定的話,就是全域性狀態了。
)
// 初始化
state.loadData(true)
// 監聽頁號,實現翻頁功能
watch(() => state.pagerInfo.pagerIndex, (index) => {
state.loadData()
})
// 監聽查詢條件,實現查詢功能。
watch(state.findValue, () => {
state.loadData(true)
})
return state
}
/**
* 子元件用 inject 獲取狀態
* @returns
*/
const getListState = () => {
return useStore(flag)
}
export {
getListState,
regListState
}
是不是應該把 watch 也內建了?
在父元件引入區域性狀態
建立父元件,使用 getListState 引入區域性狀態:
- data-list.vue
// 引入
import { regListState } from './controller/state-list.js'
// 註冊狀態
const state = regListState()
呼叫 getListState() 會用 provide 設定一個狀態。
在子元件裡獲取區域性狀態
建立子元件,獲取區域性狀態:
- pager.vue
// 區域性狀態
import { getListState } from '../controller/state-list.js'
// 獲取父元件提供的區域性狀態
const state = getListState()
呼叫 getListState(), 內部會用 inject (注入)獲取父元件的區域性狀態。這樣使用起來就比較明確,也比較簡單。
子元件也可以呼叫 regListState ,這樣可以註冊一個子元件的狀態,子子元件只能獲取子元件的狀態。
子子元件如果想獲取父元件的狀態,那麼需要設定不同的ID。
安全等級
變更狀態可以有四個安全級別:寬鬆、一般、嚴格、超嚴。
安全級別 | state型別 | 直接改屬性 | 內建函式 | action | 範圍 | 舉例 |
---|---|---|---|---|---|---|
寬鬆 | reactive | ✔ | ✔ | ✔ | 所有元件 | 彈窗、抽屜的狀態 |
一般 | readonly | ✘ | ✔ | ✔ | 所有元件 | |
嚴格 | readonly | ✘ | ✘ | ✔ | 所有元件 | |
超嚴 | readonly | ✘ | ✔ | ✔ | 特定元件才可更改 | 當前使用者狀態 |
-
寬鬆:任何元件裡都可以通過屬性、內建函式和 action 來更改狀態。
比如彈窗狀態(是否開啟)、抽屜狀態(是否開啟)、tab標籤的切換等。
這些場景裡,如果可以直接修改屬性的話,那麼可以讓程式碼更簡潔。 -
一般和嚴格:二者主要區別是,內建函式是否可以使用的問題,其實一開始不想區分的,但是想想還是先分開的話,畢竟多提供了一個選擇。
-
超嚴:只能在特定的元件裡改變狀態,其他元件只能讀取狀態。
比如當前訪問者的狀態,只有在登入元件、退出元件裡改變,其他元件不能更改。
這樣可以更好的適應不同的場景需求。
和 Pinia 的區別
nf-state 看起來和 Pnina 挺像的,那麼有哪些區別呢?
區域性狀態
Pinia 都是 全域性狀態,沒有區域性狀態,或者說,區域性狀態比較簡單,似乎不用特殊處理,只是,既然都封裝了,那麼就做全套吧,統一封裝,統一使用風格。
狀態的結構
雖然都是 reactive 的形式,但是內部結構的層次不一樣。
pinia 的狀態,資料部分和操作部分都在一個層級裡面,感覺有點分佈清楚,所以 pinia 提供了 來實現 toRefs 的功能。
我還是喜歡那種層次分明的形式,比如這樣:
這樣設計層次很清晰,可以直接使用 toRefs 實現解構,而不會解構出來“不需要”的方法。
支援的功能
官方提供的狀態管理需要滿足各種需求,所以要支援 option API、vue2、TypeScript等。
而我自己做的狀態管理,滿足自己的需求即可,所以可以更簡潔,當然可能無法滿足你的需求。
可以不重複製造輪子,但是要擁有製造輪子的能力。做一個狀態管理,可以培養這種能力。