结合 Vuex 和 Pinia 做一个适合自己的状态管理 nf-state
一开始学习了一下 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的更多相关文章
- Mobx-React : 当前适合React的状态管理工具
MobX 简单.可扩展的状态管理 MobX 是由 Mendix.Coinbase.Facebook 开源和众多个人赞助商所赞助的. 安装 安装: npm install mobx ...
- vuex介绍--一篇看懂vuejs的状态管理神器
原文,请点击此链接http://www.ituring.com.cn/article/273487
- 一文解析Pinia和Vuex,带你全面理解这两个Vue状态管理模式
Pinia和Vuex一样都是是vue的全局状态管理器.其实Pinia就是Vuex5,只不过为了尊重原作者的贡献就沿用了这个看起来很甜的名字Pinia. 本文将通过Vue3的形式对两者的不同实现方式进行 ...
- 又见angular----步一步做一个angular4小项目
这两天看了看angular4的文档,发现他和angular1.X的差别真的是太大了,官方给出的那个管理英雄的Demo是一个非常好的入门项目,这里给出一个管理个人计划的小项目,从头至尾一步一步讲解如何去 ...
- 了解Vuex状态管理模式的理解强化指南
1 Vuex是什么呢?它是Vue的状态管理模式,在使用vue的时候,需要在vue中各个组件之间传递值是很痛苦的,在vue中我们可以使用vuex来保存我们需要管理的状态值,值一旦被改变,所有引用该值的地 ...
- Vue状态管理vuex
前面的话 由于多个状态分散的跨越在许多组件和交互间各个角落,大型应用复杂度也经常逐渐增长.为了解决这个问题,Vue提供了vuex.本文将详细介绍Vue状态管理vuex 引入 当访问数据对象时,一个 V ...
- Vue之状态管理(vuex)与接口调用
Vue之状态管理(vuex)与接口调用 一,介绍与需求 1.1,介绍 1,状态管理(vuex) Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式.它采用集中式存储管理应用的所有组件的状态 ...
- Vuex状态管理详解
什么是Vuex 专门为vue应用程序开发的状态管理模式,采用集中式存储管理应用的所有组件的状态(数据),以相应的规则保证状态以一种可预测的方式发生改变 Vuex的作用(什么样的情况下使用Vuex) 多 ...
- 前端MVC Vue2学习总结(九)——Vuex状态管理插件
一.概要 1.1.Vuex定义与注意事项 Vuex是为vue.js框架更好的管理状态而设计一个插件.Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式.它采用集中式存储管理应用的所有组件的 ...
随机推荐
- SpringDataJpa使用审计(Auditing)功能
SpringBoot项目使用SpringDataJpa提供的审计功能的使用流程 SpringDataJpa提供审计注解:@CreatedBy,@LastModifiedBy,@CreatedDate, ...
- 客户端回调 Watcher ?
客户端 SendThread 线程接收事件通知,交由 EventThread 线程回调 Watcher. 客户端的 Watcher 机制同样是一次性的,一旦被触发后,该 Watcher 就失效了.
- 为什么需要域驱动设计DDD?
我们需要 DDD 的因素 – 微服务面试问题
- 学习Redis(二)
1.Redis应用场景 1.缓存(键过期时间) 1) 缓存session会话 2) 缓存用户信息,找不到再去mysql查,查到然后回写到redis 3) 商城优惠卷过期时间 2.排行榜(列表& ...
- 体验javascript之美6:如果你觉得什么都会了或者不知道js学什么了看这里-面向对象编程
概述 当大家已经把js的语言基础理解了,然后能够写出一些简单的例子了,这个时候基本上达到了一年工作经验的水平,而自己能够独立的写一些小功能,完成一些小效果,或者临摹修改一些比较复杂的插件的时候差不多就 ...
- 抽象方法不能为private,final或者static,为什么?
4)抽象方法不能为private,final或者static, native, synchrozied为什么?[新手可忽略不影响继续学习]马克-to-win:抽象方法的最实质的意义在于被未来的子类覆盖 ...
- 将本地代码上传到gitLab
1. 在远程gitLab仓库创建项目, 执行下列命令 git init git remote add origin git@10.10.xxx.git (gitLab刚刚创建的工程地址) git ...
- Linux上安装RePlace
RePlAce: Advancing Solution Quality and Routability Validation in Global Placement 项目地址 https://gith ...
- Docker 核心知识回顾
Docker 核心知识回顾 最近公司为了提高项目治理能力.提升开发效率,将之前的CICD项目扩展成devops进行项目管理.开发人员需要对自己的负责的项目进行流水线的部署,包括写Dockerfile ...
- Linux内核--链表结构(一)
一.前言 Linux内核链表结构是一种双向循环链表结构,与传统的链表结构不同,Linux内核链表结构仅包含前驱和后继指针,不包含数据域.使用链表结构,仅需在结构体成员中包含list_head*成员就行 ...