先来说说为什么
-
使用
Vuex
会使代码变得繁琐冗余(代码太多了)“被忽略的官方说明”,而大多时候我们并没有用到所谓的状态追踪,只是简单的进行getter
、setter
、mutations
;当状态多的时候,Vuex
真的可以有利于我们对代码便捷的阅读吗? -
不利于编辑器和
typescript
中代码查找,每次要找某个变量的时候,要在store
文件全局搜,各种代码片段去反复跳;现在这种状态管理方式解决了这个痛点:像一些标准的多语言编辑器vscode
、atom
等是可以检查到静态代码的,Ctrl
+点击可以跳到对应位置、鼠标放上去某个字段时,会得到定义时声明的类型提示和代码片段(js中需要用jsDoc注释声明),一开始我也觉得这东西没用,后面写过typescript
才发现,原来javascript
也一样可以通过jsDoc注释
来提供typescript
一样的类型提示,真香。
设计模式
这里我推荐自行定义class
作为分模块数据管理会更加好,理由就是数据庞大的时候可以拆分为各class
来便于对状态的管理,为什么不使用普通object
单例对象?因为普通object
没有继承
、没有自身调用的构造函数,在ts
中类型定义会比较麻烦,而且没有class
中好用的private
、protected
和class
可以作为interface
使用等;一句话总结就是:“没有class
灵活”,这个我也是从游戏编程那边借鉴来的习惯。
写这篇文章的时候,我早就在2017年开始实际项目中使用class
替代vuex
了,所以并不用担心有其他问题,而且Vue 3.x
中也可以复用,而且这只是一个编程的设计模式,并不是我个人发明的方言
先来看下以下代码
const a = {
data: {
value: 10
}
}
const b = {
data: a.data
};
b.data.value = 20;
console.log(a, b); // 输出 {data: { value: 20 }}, {data: { value: 20 }}
先来说下原理:因为javascript
变量对等赋值的时候,指针指向同一个内存,所以依赖这个特性和Vue
中赋值是直接对等,就可以自行定义一些全局的状态属性,依次注入到需要同步更新的组件中。废话不多说,直接看代码结构:
step 1 src/store/index.js
// 自行定义一个class作为数据管理
class ModuleStore {
/** 订单信息 */
orderInfo = {
/** 订单名 */
name: "订单" + Math.random().toString(36).substr(2),
/** 订单日期 */
date: "2018/12/12 12:12:12",
/**
* 订单状态
* @type {"ok"|"fail"|"invalid"|"wait"} 完成 | 失败 | 无效 | 待支付
*/
state: "ok"
}
}
/** 状态管理模块 */
const store = new ModuleStore;
export default store;
step 2 src/goods.vue
<script>
import store from "../store";
export default {
data () {
return {
// 当前组件响应式用到的数据,这里对等之后,修改 store.orderInfo 的值就是响应式了
pageData: store.orderInfo
}
}
}
</script>
step 3 src/list.vue
<script>
import store from "../store";
export default {
data () {
return {
// 当前组件响应式用到的数据,这里对等之后,修改 store.orderInfo 的值就是响应式了
listData: store.orderInfo
}
},
methods: {
modifyState() {
// 这里修改了,其他引用到 store.orderInfo 所有组件都会同步修改
this.listData.state = "wait";
// 或者
store.orderInfo.state = "wait";
}
}
}
</script>
注意事项
<script>
import store from "../store";
export default {
...more,
methods: {
modifyState() {
// 注意不要这样去修改整个属性,因为会导致对象内所有的属性都失去了指针(参照一开始那个段代码片段)
this.listData = {
name: "修改订单名",
date: new Date().toLocaleString()
}
// 但是我不想
// this.listData.name = "修改订单名";
// this.listData.date = new Date().toLocaleString()
// 咋整?
}
}
}
</script>
进阶一下状态模块设计
- 创建一个
ModifyObject.js
,可以分代码多方式去使用:默认导出给其他模块继承使用
、作为普通单例工具使用
;在typescript
中这两个方法我会用泛型去约束传参,那么就确保了状态字段的可靠性。
// 因为可能存在多个状态模块,所以定义一个基类,导出让其他模块继承使用
// 这里 export 是给其他模块继承用
export class ModuleModifyObject {
/**
* 修改属性值-只修改之前存在的值
* @param {object} target 修改的目标
* @param {object} value 修改的内容
*/
modifyData(target, value) {
for (const key in value) {
if (Object.prototype.hasOwnProperty.call(target, key)) {
target[key] = value[key];
}
}
}
/**
* 设置属性值-之前不存在的值也根据传入的`value`值去设置
* @param {object} target 设置的目标
* @param {object} value 设置的内容
*/
setData(target, value) {
for (const key in object) {
target[key] = value[key];
}
}
}
const modifyObject = new ModuleModifyObject();
// 这里 export default 是作为单例用
export default modifyObject;
- 再来改造下
src/store/index.js
import {
ModuleModifyObject
} from "./utils/ModifyObject"
class ModuleStore extends ModuleModifyObject {
...more
}
...more
- 现在回到
src/list.vue
中
<script>
import store from "../store";
export default {
...more,
methods: {
modifyState() {
const obj = {
name: "修改订单名",
date: new Date().toLocaleString(),
// 下面是不在 store.orderInfo 的属性,上面的基类中我已经做了处理,所以本身不存在的属性是不会更改到的
a: "php",
b: "java"
}
// 写法一
store.modifyData(this.listData, obj);
// 写法二
store.modifyData(store.orderInfo, obj);
// 这样就不需要 this.listData.name = "修改订单名"; this.listData.date = new Date().toLocaleString() 这种逐个属性去修改了
}
}
}
</script>
真正意义上做到不能直接修改store.xxx
,而是通过mutations
去修改状态
1. 开始改用typescript
,首先要定义两个核心类型工具,一会用到
/** 深层递归所有属性为可选 */
export type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
}
/** 深层递归所有属性为只读 */
export type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
}
2. 改造一下store
模块;注意这里不是要使用vuex
的mutations
,而是通过对应方法去约束使用
interface UrderInfoType {
/** 用户名 */
name: string
/** 用户出生日期 */
date: string
/** 性别 */
gender: "man" | "woman"| ""
/** 登录信息 */
api: {
/** 登录凭据 */
token: string
/** 登录`id` */
id: string
}
}
class ModuleStore extends ModuleModifyObject {
/** 用户信息 */
readonly userInfo: DeepReadonly<UrderInfoType> = {
name: "",
date: "",
gender: "",
api: {
token: "",
id: "",
}
}
/**
* 更新用户信息,并缓存到`sessionStorage`中
* @param value 要更新的值
*/
updateUserInfo(value: DeepPartial<UserInfoType>) {
// `modifyData`方法在里面做了递归处理,可以看下面的代码仓库有写,这里不做解释
this.modifyData(this.userInfo, value);
// or
// this.modifyData(this.userInfo as DeepPartial<UserInfoType>, value);
sessionStorage.setItem("user-info", JSON.stringify(this.userInfo));
}
}
3. 回到使用场景
<script>
import store from "../store";
export default {
data () {
return {
// 挂载响应式状态
userInfo: store.userInfo
}
},
methods: {
setToken() {
// 现在不能直接修改了,编辑器会报错提示:无法分配到 "token" ,因为它是只读属性。
// this.userInfo.api.token = ""
// 现在只能通过`updateUserInfo`去更改要更改的属性,而且有类型约束和代码提示,再也不会担心状态出错,
// 反正我是不会去记住每一个属性的,这些就应该交给机器去记住然后提示出来
store.updateUserInfo({
api: {
token: "adfsdf2sd4f5s4d"
}
})
}
}
}
</script>
4. 对比下vuex
中的mutations
const store = new Vuex.Store({
state: {
userInfo: {
...more
}
},
mutations: {
updateUserInfo(state, value) {
state.userInfo = value;
sessionStorage.setItem("user-info", JSON.stringify(state.userInfo));
}
}
})
// 使用时,官方给出的例子是:如果不想覆盖旧属性的值去改某个属性,那么这样写
this.$store.commit(updateUserInfo, {
...this.$store.state.userInfo,
...{
api: {
token: "asfdf45s4d54s5d4f5",
id: vuexStore.state.userInfo.api.id
}
}
})
// 实在太丑陋了,而且属性多的时候(对象属性很深层),不一定能保证你可以百分百没写错,懂我要说什么吧?
// 或者说,你在外面设置好修改之后的数据再传进去不就好了吗?那来看看是怎么样的
const value = JSON.parse(JSON.stringify(this.$store.state.userInfo)); // 这里必须要深拷贝,不然直接修改到状态了
value.api.token = "asfdf45s4d54s5d4f5"
this.$store.commit(updateUserInfo, value);
// 通过对比,应该能看出来有什么区别了
以上这个就是基本的状态管理实现,说下我在项目中用到的状态监听处理:在ModuleStore
这个类里面使用Object.defineProperty
或者new Proxy
作为更复杂的操作,自定义的class
作为状态管理更容易理解,而且扩展性也高。
最后对比优缺点,优点:代码编辑器(以vscode为例)静态代码追踪提示非常友好(Vuex
无法实现,而且代码多的时候找某个属性太费力了,所以我才放弃使用),数据庞大时尤其明显,如果你是使用ts
,配合 readonly
、private
、enum
等关键字使用,可维护、阅读性简直再舒服不过。缺点:无法使用浏览器的vuex插件,不过有了静态代码分析检测,也不需要调试插件了。
Vue 3.x 同样可以复用
稍作改动,在store
模块中把对应的属性加上reactive()
即可,下面代码中,取消了上面使用的继承操作,而是把modifyData
、setData
单独抽离了出来;
新建一个状态模块/store/Order.ts
import { reactive } from "vue";
import { jsonParse, modifyData } from "@/utils";
interface OrderInfo {
id: number | ""
/** 超时判断的时间 */
time: string | number
/** 商品属性对象 */
goods: {
id: number | ""
name: string
/**
* 商品价格
* - 前端展示时需要除以`100`
*/
price: number
}
/**
* 生成订单时间
* - 当`new Date(time).getTime() > new Date(createTime).getTime()`时,当前订单则为超时,执行清空操作
*/
createTime: string | number
}
function useOrderInfo(): DeepReadonly<OrderInfo> {
return {
id: "",
time: "",
goods: {
id: "",
name: "",
price: 0
},
createTime: ""
}
}
const key = "ModuleOrder";
export default class ModuleOrder {
constructor() {
this.init();
}
/** 订单信息 */
readonly info = reactive(useOrderInfo())
/** 初始化缓存信息 */
private init() {
const value = jsonParse(sessionStorage.getItem(key));
modifyData(this.info, value);
}
/**
* 更新 & 设置状态数据
* @param value
*/
update(value: DeepPartial<OrderInfo>) {
modifyData(this.info, value);
sessionStorage.setItem(key, JSON.stringify(this.info));
}
/** 重置 */
reset() {
modifyData(this.info, useOrderInfo());
sessionStorage.removeItem(key);
}
}
导入到整体store模块下 /store/index.ts
import ModuleUser from "./User"; // 与 Order 一样的操作文件
import ModuleOrder from "./Order";
class ModuleStore {
/** 用户状态模块 */
readonly user = new ModuleUser();
/** 订单状态 */
readonly order = new ModuleOrder();
}
const store = new ModuleStore();
export default store;
页面应用 /views/demo.vue
<template>
<div>
<div>{{ userInfo.name }}</div>
<span class="the-tag cyan">商品名称:{{ orderInfo.goods.name }}</span>
<span class="the-tag red">${{ orderInfo.goods.price / 100 }}</span>
<button @click="onEditPrice()">修改价格</button>
<button @click="onSubmit()">发起支付</button>
</div>
</template>
<script lang="ts" setup>
import store from "@/store";
const userInfo = store.user.info;
const orderInfo = store.order.info;
function onEditPrice() {
store.order.update({
goods: {
price: 2199
}
})
}
function onSubmit() {
if (new Date(orderInfo.time).getTime() > new Date(orderInfo.createTime).getTime()) {
alert(`当前用户【${userInfo.name}】未在${orderInfo.time}之前下单,请重新操作`);
store.order.reset();
return;
}
}
</script>
可以看到,vue 3.x
之后提供了reactive()
这个api
使得响应式变量可以直接提取到其他地方去声明,相比vue 2.x
的操作简单了一些,代码更直观一点;以上这个设计模式在两者中,实现的功能都是相同的。
最后附上使用到的项目:vue-admin-template、uni-app-template
有问题欢迎提出~