• 结合 Vuex 和 Pinia 做一个适合自己的状态管理 nfstate


    一开始学习了一下 Vuex,感觉比较冗余,就自己做了一个轻量级的状态管理。
    后来又学习了 Pinia,于是参考 Pinia 改进了一下自己的状态管理。

    结合 Vuex 和 Pinia, 保留需要的功能,去掉不需要的功能,修改一下看着不习惯的使用方法,最后得到了一个满足自己需要的轻量级状态管理 —— nf - state

    设计思路

    还是喜欢 MVC设计模式,状态可以看做 M,组件是V,可以用 controller 做调度,需要访问后端的话,可以做一个 services。这样整体结构比较清晰明了。

    当然简单的状态不需要 controller,直接使用 getters、actions 即可。整体结构如下:

    状态管理 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

    优点

    • 支持全局状态局部状态
    • 可以像 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 的功能。

    pinia的状态结构.png

    我还是喜欢那种层次分明的形式,比如这样:

    class+reactive的方式

    这样设计层次很清晰,可以直接使用 toRefs 实现解构,而不会解构出来“不需要”的方法。

    支持的功能

    官方提供的状态管理需要满足各种需求,所以要支持 option API、vue2、TypeScript等。

    而我自己做的状态管理,满足自己的需求即可,所以可以更简洁,当然可能无法满足你的需求。

    可以不重复制造轮子,但是要拥有制造轮子的能力。做一个状态管理,可以培养这种能力。

  • 相关阅读:
    [代码]比较XML文件差异[cl_proxy_ui_utils=>show_xml_diff]
    查看日期属性(休息日、节假日、星期几)[DAY_ATTRIBUTES_GET]
    ABAP中字符串处理方法小结(一)
    ABAP中字符串处理方法小结(二)
    [问题解决]ALV可输入状态下输入金额/数量字段小数位数提前的问题
    自适应背景图
    js原型继承
    仿留言功能
    鼠标画图效果
    照片切换,24小块分散效果
  • 原文地址:https://www.cnblogs.com/jyk/p/16257632.html
Copyright © 2020-2023  润新知