結(jié)合 Vuex 和 Pinia 做一個(gè)適合自己的狀態(tài)管理 nf-state

一開(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)如下:

狀態(tài)管理 nf-state

源碼

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 的 state、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 的功能痰娱。

pinia的狀態(tài)結(jié)構(gòu).png

我還是喜歡那種層次分明的形式,比如這樣:

class+reactive的方式

這樣設(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)這種能力。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末寂呛,一起剝皮案震驚了整個(gè)濱河市怎诫,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌贷痪,老刑警劉巖幻妓,帶你破解...
    沈念sama閱讀 219,366評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異劫拢,居然都是意外死亡涌哲,警方通過(guò)查閱死者的電腦和手機(jī)胖缤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)阀圾,“玉大人,你說(shuō)我怎么就攤上這事狗唉〕鹾妫” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,689評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵分俯,是天一觀的道長(zhǎng)肾筐。 經(jīng)常有香客問(wèn)我,道長(zhǎng)缸剪,這世上最難降的妖魔是什么吗铐? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,925評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮杏节,結(jié)果婚禮上唬渗,老公的妹妹穿的比我還像新娘。我一直安慰自己奋渔,他們只是感情好镊逝,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,942評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著嫉鲸,像睡著了一般撑蒜。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上玄渗,一...
    開(kāi)封第一講書(shū)人閱讀 51,727評(píng)論 1 305
  • 那天座菠,我揣著相機(jī)與錄音,去河邊找鬼藤树。 笑死浴滴,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的也榄。 我是一名探鬼主播巡莹,決...
    沈念sama閱讀 40,447評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼甜紫!你這毒婦竟也來(lái)了降宅?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,349評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤囚霸,失蹤者是張志新(化名)和其女友劉穎腰根,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體拓型,經(jīng)...
    沈念sama閱讀 45,820評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡额嘿,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,990評(píng)論 3 337
  • 正文 我和宋清朗相戀三年瘸恼,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片册养。...
    茶點(diǎn)故事閱讀 40,127評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡东帅,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出球拦,到底是詐尸還是另有隱情靠闭,我是刑警寧澤,帶...
    沈念sama閱讀 35,812評(píng)論 5 346
  • 正文 年R本政府宣布坎炼,位于F島的核電站愧膀,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏谣光。R本人自食惡果不足惜檩淋,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,471評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望萄金。 院中可真熱鬧蟀悦,春花似錦、人聲如沸捡絮。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,017評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)福稳。三九已至涎拉,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間的圆,已是汗流浹背鼓拧。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,142評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留越妈,地道東北人季俩。 一個(gè)月前我還...
    沈念sama閱讀 48,388評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像梅掠,于是被迫代替她去往敵國(guó)和親酌住。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,066評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容