制作一個(gè)輕量級(jí)的狀態(tài)管理插件:Vue-data-state

Vuex 是不是有點(diǎn)繁瑣?

Vuex 是針對(duì) Vue2 來設(shè)計(jì)的,因?yàn)?option API 本身有很多缺點(diǎn),所以 Vuex 只好做各種補(bǔ)丁彌補(bǔ)這些缺點(diǎn),于是變得比較“復(fù)雜”。

現(xiàn)在 Vue3 推出了Composition API,功能更強(qiáng)大也彌補(bǔ)了之前的缺點(diǎn),但是 Vuex 4.0 只是兼容了 Vue3,使用風(fēng)格上似乎沒啥變化。

于是乎怎么看怎么別扭,不是說 Vuex 不夠強(qiáng)大,Vuex 的 state 也使用了 reactive ,而且也是用 provide/inject 實(shí)現(xiàn)注入的,但是沒有后續(xù)了,Composition API 的其他特性呢?感覺好浪費(fèi)呀。

雖然也可以基于 Vuex 用 compositionAPI 的方式實(shí)現(xiàn)功能,但是總感覺有點(diǎn)大炮打蚊子的感覺。

做一個(gè)輕量級(jí)狀態(tài)管理。

按照“自己動(dòng)手豐衣足食”的原則,我們自己來做一個(gè)輕量級(jí)的狀態(tài)管理。

模仿 Vuex 試著實(shí)現(xiàn)了一下基本功能,有點(diǎn)理解為啥 Vuex 弄得那么繞了,因?yàn)橐巡僮骱瘮?shù)也給包含進(jìn)去確實(shí)有點(diǎn)難度。
那么就輕量到底吧,只包含狀態(tài),不包括 mutations、action 這些操作函數(shù)。

于是功能變成這個(gè)樣子:


vue-data-state功能設(shè)計(jì).png

狀態(tài):全局狀態(tài)、局部狀態(tài)
功能:初始化狀態(tài)、指定的組件里注入局部狀態(tài)、子組件里加載局部狀態(tài)

緩存功能,就是可以把狀態(tài)存入localstorage里面保存,以及初始化的時(shí)候從localstorage里面加載狀態(tài)。
緩存功能暫時(shí)沒有實(shí)現(xiàn),還沒想好局部狀態(tài)的緩存方案。

功能簡單,寫起來也就容易多了,然后順便做成插件的形式,便于使用。

// 模仿Vuex寫一個(gè)簡單的數(shù)據(jù)、狀態(tài)、緩存管理

import { reactive, provide, inject } from 'vue'   

export default {
  // 狀態(tài)容器
  store: {
    state: {}, // 全局狀態(tài)
    init: () => {}, // 初始化全局狀態(tài)
    reg: {}, // 注冊(cè)局部狀態(tài)
    get: {} // 獲取局部狀態(tài)
  },

  // 用 symbol 做個(gè)標(biāo)識(shí),避免重名
  storeFlag: Symbol('VuexDataState'),

  // 創(chuàng)建安裝插件的實(shí)例
  createStore(info) {
    /* info 的結(jié)構(gòu)示例
    const _info = {
      // 全局狀態(tài),在main.js里面注入
      global: {
        blogState: { // 每個(gè)狀態(tài)都必須是對(duì)象,不支持基礎(chǔ)類型
          aaa: '狀態(tài)演示'
        }
      },
      // 局部狀態(tài),需要手動(dòng)注入
      local: {
        dataList() {
          return {}
        }
      },
      // 初始化函數(shù),可以從后端、前端等獲取數(shù)據(jù)加入狀態(tài)
      // 注入后被動(dòng)調(diào)用,僅限于全局狀態(tài)
      init(state) {}
    }
    */

    for (const key in info.global) {
      // 把全局狀態(tài)存入state
      this.store.state[key] = reactive(info.global[key])
    }
    
    for (const key in info.local) {
      const localKey = Symbol(key)
      // 加上注冊(cè)函數(shù)
      this.store.reg[key] = () => {
        // 把局部狀態(tài)變成 reactive 的形式
        const state = reactive(info.local[key]())
        // 注入
        provide(localKey, state)
        // 返回狀態(tài),
        return state
      }
      this.store.get[key] = () => {
        // 把局部狀態(tài)變成 reactive 的形式
        const state = inject(localKey)
        // 返回狀態(tài), 
        return state
      }
    }

    // 加上初始化函數(shù)
    if (typeof info.init === 'function') {
      this.store.init = info.init
    }
    
    const _store = this.store
    const _storeFlag = this.storeFlag
    return {
      // 安裝插件
      install (app, options) {
        // console.log('install--我的狀態(tài)', _store)
        // 注入狀態(tài),用 symbol 作為標(biāo)記,避免重名,避免外部直接用 inject 獲取
        app.provide(_storeFlag, _store)
        // 設(shè)置模板使用狀態(tài)
        app.config.globalProperties.$state = _store.state
        // 調(diào)用初始化,給全局狀態(tài)賦值
        _store.init(_store.state)
      }
    }
  },

  // 代碼里面調(diào)用
  useStore() {
    // 獲取全局狀態(tài)
    const { state, reg, get } = inject(this.storeFlag)

    return {
      state, // 返回全局狀態(tài)
      reg, // 注冊(cè)局部狀態(tài)的函數(shù),并且返回對(duì)應(yīng)的局部狀態(tài)
      get // 子組件里面獲取狀態(tài)
    }
  }
}

怎么樣,夠輕吧,不超過一百行代碼,如果去掉注釋空行的話,大概也就三十多行吧。

  • reactive, provide, inject
    狀態(tài)要實(shí)現(xiàn)響應(yīng)性,那當(dāng)然要做成 reactive 形式的。
    provide、inject 實(shí)現(xiàn)注入功能。

  • store
    內(nèi)部狀態(tài)容器。
    state:狀態(tài)
    init:全局狀態(tài)的初始化的函數(shù)
    reg:局部狀態(tài)的注入函數(shù)
    get:獲取局部狀態(tài)的函數(shù)

  • storeFlag
    用 symbol 做全局狀態(tài)的標(biāo)記,避免重名。是不是有一種高大上的感覺?[狗頭]

  • useStore
    是不是眼熟,在代碼里面獲取全局狀態(tài)的。
    除了返回全局狀態(tài)外,還可以返回局部狀態(tài)的注入函數(shù)和獲取函數(shù)。
    因?yàn)槭褂?symbol 作為key,外部無法獲取,所以需要內(nèi)部提供一個(gè)函數(shù)。(我不會(huì)告訴你我是故意的)

如果想把狀態(tài)變成只讀(readonlyReactive)的形式然后在返回,那么可以在這里操作。

  • 組件里的使用方法
import VueDS from 'vue-data-state' 

const { state, reg, get } = VueDS.useStore()

state // 全局狀態(tài)

// 父組件注入狀態(tài),并且返回局部狀態(tài),以便父組件使用
const 局部狀態(tài) = reg.局部狀態(tài)名稱()

// 子組件獲取局部狀態(tài)
const 局部狀態(tài) = get.局部狀態(tài)名稱()

  • createStore
    看著是不是眼熟,功能和 Vuex 的 createStore 是一樣的,接收參數(shù)創(chuàng)建 store 然后通過插件注入到 vue 的app上面。
    函數(shù)返回 install,用于安裝插件。

  • _info
    這個(gè)沒啥用,就是介紹一下參數(shù)的屬性格式,實(shí)現(xiàn)代碼的時(shí)候看著方便。另外去掉注釋就可以做測試用。

  • 第一個(gè)for
    遍歷全局狀態(tài),變成 reactive 掛到 store 里面。

  • 第二個(gè) for
    遍歷局部狀態(tài),變成注入和獲取的函數(shù),掛到 reg 和 get 里面。
    這個(gè) provide 的 key 也采用 symbol 的形式,避免重名。

  • init
    把初始化函數(shù)掛上。

  • install
    安裝插件,按照 Vue 官網(wǎng)示例,寫了這個(gè)install。
    對(duì)了,我只是把全局狀態(tài)掛到模板上面了,局部狀態(tài)沒有掛呢。
    局部狀態(tài)似乎掛不上,還需要再考慮考慮。

先安裝資源包

yarn add vue-data-state 

定義狀態(tài)

// /store-ds/index.js
import VuexDataState from 'vue-data-state'

export default VuexDataState.createStore({
  global: { // 全局狀態(tài)
    userInfo: {
      name:'當(dāng)前登錄人'
    }
  },
  local: { // 局部狀態(tài)
    // 數(shù)據(jù)列表,使用前需要先注冊(cè)
    dataListState() { // 顯示博文列表用的狀態(tài)
      return {
        findKind: {}, // 查詢方式
        find: {}, // 查詢關(guān)鍵字
        page: { // 分頁參數(shù)
          pageTotal: 100,
          pageSize: 2,
          pageIndex: 1,
          orderBy: { id: false }
        },
        _query: {}, // 緩存的查詢條件
        isReload: false // 重新加載數(shù)據(jù),需要統(tǒng)計(jì)總數(shù)
      }
    }
  },
  // 可以給全局狀態(tài)設(shè)置初始狀態(tài),可以是異步操作
  init(state) {
    setTimeout(() => {
      state.blogState.name = 'int里面設(shè)置的數(shù)據(jù),可以異步'
    },3000)
  }
}) 
  • global
    全局狀態(tài),每一個(gè)狀態(tài)都必須是對(duì)象(包含數(shù)組)的形式,不能是基礎(chǔ)類型。
    全局狀態(tài),會(huì)默認(rèn)注入到根 app 里面。
    狀態(tài)名稱、屬性名稱可以隨意,這里只是舉個(gè)例子。

  • local
    局部狀態(tài),每個(gè)狀態(tài)也必須是對(duì)象形式,不會(huì)默認(rèn)注入,需要在父組件里面使用 reg 調(diào)用函數(shù)才能注入。
    需要使用 return 的形式,原理和 data 一樣。Vuex 模塊里的 state 也是需要用 return 形式的。
    狀態(tài)名稱、屬性名稱可以隨意,這里只是舉個(gè)例子。

  • init
    初始化全局狀態(tài)的函數(shù),可以不設(shè)置。
    在main.js里面安裝插件時(shí),注入全局狀態(tài)后 init會(huì)被調(diào)用,這時(shí)候可以給全局狀態(tài)賦值,支持異步操作。

在main.js 里面使用

這個(gè)就和 Vuex 一樣了:
main.js

import { createApp } from 'vue'
import store from './store-ds' // 輕量級(jí)狀態(tài)

createApp(App)
  .use(store) // 輕量級(jí)狀態(tài)

后續(xù)會(huì)在個(gè)人博客里面試用一下,具體使用的時(shí)候,才會(huì)發(fā)現(xiàn)有沒有問題,以及如何改進(jìn)。

FAQ

  • 傳說中的跟蹤呢?
    關(guān)于跟蹤的問題,一直理解的不深刻,因?yàn)閐ev-tool總是安裝不上,后來好不容易安裝上了,卻不工作。所以暫時(shí)跳過這個(gè)功能。

  • Vuex支持插件,你的這個(gè)呢?
    這個(gè)說起來有點(diǎn)復(fù)雜,簡單的說,目前還沒有這樣的需求,所以就先跳過了,以后需要的話,可以再加嘛。
    要做插件的話也簡單,用 watch 對(duì)狀態(tài)做深度監(jiān)聽,然后調(diào)用插件鉤子就行。
    要不然我為啥要把狀態(tài)拆開做reactive呢?

  • 不是說不讓直接修改狀態(tài)嗎?
    關(guān)于這一點(diǎn)也是比較復(fù)雜。
    我可以把狀態(tài)做成只讀的,readonlyReactive一下就行,然后再設(shè)計(jì) 類似 mutations 的方法 來修改狀態(tài)。
    但是這么做的意義到底是什么呢?
    沒有實(shí)現(xiàn)跟蹤功能,也沒用插件,也不知道怎么弄到dev-tool里面去。
    這些都是配套工程,如果沒有這些配套工程,只是做一個(gè)只讀的話,總是感覺怪怪的。

  • 為啥要弄個(gè)局部狀態(tài)?
    這個(gè)要從一次討論說起。
    某天和知乎大神聊天,他說要做一個(gè)模塊內(nèi)的共享狀態(tài),一開始我還不理解,討論了半天,感覺大神說的確實(shí)在理。
    于是慢慢開始嘗試,最后發(fā)現(xiàn)確實(shí)挺香的。具體的會(huì)在后面的博客項(xiàng)目里面介紹。

  • 支持 option API嗎?
    一開始忘記這個(gè)事了,后來才想起來,因?yàn)槭菍iT針對(duì)composition API來設(shè)計(jì)的,所以應(yīng)該是不支持的吧。

源碼

https://gitee.com/naturefw/vue-data-state

在線演示

https://naturefw.gitee.io/vue-data-state/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容