一开始学习了一下 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等。

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

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

结合 Vuex 和 Pinia 做一个适合自己的状态管理 nf-state的更多相关文章

  1. Mobx-React : 当前适合React的状态管理工具

    MobX 简单.可扩展的状态管理        MobX 是由 Mendix.Coinbase.Facebook 开源和众多个人赞助商所赞助的.    安装 安装: npm install mobx ...

  2. vuex介绍--一篇看懂vuejs的状态管理神器

    原文,请点击此链接http://www.ituring.com.cn/article/273487

  3. 一文解析Pinia和Vuex,带你全面理解这两个Vue状态管理模式

    Pinia和Vuex一样都是是vue的全局状态管理器.其实Pinia就是Vuex5,只不过为了尊重原作者的贡献就沿用了这个看起来很甜的名字Pinia. 本文将通过Vue3的形式对两者的不同实现方式进行 ...

  4. 又见angular----步一步做一个angular4小项目

    这两天看了看angular4的文档,发现他和angular1.X的差别真的是太大了,官方给出的那个管理英雄的Demo是一个非常好的入门项目,这里给出一个管理个人计划的小项目,从头至尾一步一步讲解如何去 ...

  5. 了解Vuex状态管理模式的理解强化指南

    1 Vuex是什么呢?它是Vue的状态管理模式,在使用vue的时候,需要在vue中各个组件之间传递值是很痛苦的,在vue中我们可以使用vuex来保存我们需要管理的状态值,值一旦被改变,所有引用该值的地 ...

  6. Vue状态管理vuex

    前面的话 由于多个状态分散的跨越在许多组件和交互间各个角落,大型应用复杂度也经常逐渐增长.为了解决这个问题,Vue提供了vuex.本文将详细介绍Vue状态管理vuex 引入 当访问数据对象时,一个 V ...

  7. Vue之状态管理(vuex)与接口调用

    Vue之状态管理(vuex)与接口调用 一,介绍与需求 1.1,介绍 1,状态管理(vuex) Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式.它采用集中式存储管理应用的所有组件的状态 ...

  8. Vuex状态管理详解

    什么是Vuex 专门为vue应用程序开发的状态管理模式,采用集中式存储管理应用的所有组件的状态(数据),以相应的规则保证状态以一种可预测的方式发生改变 Vuex的作用(什么样的情况下使用Vuex) 多 ...

  9. 前端MVC Vue2学习总结(九)——Vuex状态管理插件

    一.概要 1.1.Vuex定义与注意事项 Vuex是为vue.js框架更好的管理状态而设计一个插件.Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式.它采用集中式存储管理应用的所有组件的 ...

随机推荐

  1. AQS 支持两种同步方式?

    1.独占式 2.共享式 这样方便使用者实现不同类型的同步组件,独占式如 ReentrantLock,共享式如 Semaphore,CountDownLatch,组合式的如 ReentrantReadW ...

  2. 学习Nginx(一)

    实验目的 通过nginx实现反向代理的功能,类似apache反向代理和haproxy反向代理 工作中用nginx做反向代理和负载均衡的也越来越多了 有些公司从web服务器到反向代理,都使用nginx. ...

  3. 使用css完成引导用户按照流程完成任务的进度导航条

    首先先看设计稿 图中的12345便是主角进度条. 分析需求如下:线的长度不固定,适应移动端和pc端点平均地分布在一条线上点的个数不固定,可能会改变激活的点之间线的颜色是绿色的 两种种方式 百分比宽度切 ...

  4. pydev+eclipse写python代码

    首先,下载pydev:PyDev for Eclipse - Browse /pydev at SourceForge.net (建议下载到本地,之前看其他文章时,进行了如下安装: 启动 Eclips ...

  5. JDK卸载和彻底删除

    第一步:点击"控制面板". 第二步:点击"卸载程序". 第三步:进入到"程序和功能"界面,找到jdk的两个程序:①java 8 update ...

  6. 使用element UI el-upload组件实现视频文件上传及上传进度显示方法总结

    实现效果: 上传中: 上传完成: 代码: <el-form-item label="视频上传" prop="Video"> <!-- acti ...

  7. 实现call、apply 及 bind 函数

    今日学习内容: (1)call 函数的实现步骤: 判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况. 判断传入上下文对象是否存在,如果不存在,则设置为 ...

  8. Python入门-异常处理

    异常处理 #try----else---- 会一起执行 #finally无论如何,最后都会执行 def main(): try: res = 10/2 print("开始执行计算:" ...

  9. numpy---(上)

    Numpy Numpy ndarray N维数组对象ndarray, 是一系列同类型数据的集合, 索引以0下标开始, 创建一个ndarray对象, 需调用array函数: numpy.array(ob ...

  10. Spring Boot-使用Spring Initializer快速创建Spring Boot项目

    File->project->Spring Initializer 点击next 点击下一步即可,如果是第一次可能需要下载jar包,如下图 resources文件中的目录结构如上图所示 s ...