基于Vue3做一套适合自己的状态管理(六)在哪里创建状态?需要局部状态吗?
计划章节
- 基类:实现辅助功能
- 继承:充血实体类
- 继承:OptionApi 风格的状态
- Model:正确的打开方式
- 组合:setup 风格,更灵活
- 注册状态的方法、以及局部状态和全局状态
- 实践:当前登录用户的状态
- 实践:列表页面需要的状态
全局状态还是局部状态?
不知道大家想过这个问题没,或者觉得这根本不是问题。。。我们先分析一下 Vuex 和 Pinia 的状态的创建方式和有效范围。
Vuex
Vuex 需要在 main 里面创建状态,然后在组件里面才能使用状态,状态在整个项目里共享且唯一。
这种方式的优点就是统一注册、便于查看,一个项目里有多少状态,到 main 里面看看就知道了。
缺点就是,感觉状态距离组件有点远,灵活性稍微差了一点点。(Vuex自身的就不说了)
Pinia
Pinia 不必在 mian 里面创建状态了,可以直接在组件里面创建,或者在单独的js文件里面创建,然后直接在组件里面引入。
这种方式给人一种错觉,这是局部状态吧,其实它还是全局状态。也很好验证,在兄弟组件里面引入同一个状态,就会发现两个兄弟组件可以共享这个状态。
那么是否可以结合一下
我感觉一个状态管理方案,应该有全局状态和局部状态两种情况,应该有明确的区分方式。 可能你会觉得,局部状态很简单,直接使用 provide/inject 即可,不需要放在一个状态管理方案里面。
这样也挺方便的,只是我感觉还是希望有一个明确的统一的规范,这样代码写起来不容易乱,看别人的代码也不会有陌生感,或者别扭感。
-
全局状态 建议在 main 里面统一创建,组件里面获取状态。 当然也可以在组件里面创建,这样可以更灵活一些。
-
局部状态 在组件(含单独的js、ts文件)里面创建,通过 provide/inject 注入。 有效范围是:自己、子组件、子子组件等。
实现方式
计划目标就是上面那样,然后我们看看具体的实现方式。
局部状态:defineStore 创建一个状态
defineStore,山寨一下 Pinia 的命名方式,其实我想起名“regState” 的。 defineStore,一般情况用来创建一个局部状态,特殊情况也可以创建全局状态。
/**
* 定义状态,一般是局部状态,也可以是全局状态
* @param id 标识(string | symbol),局部状态可以重名,全局状态不能重名
* @param info 状态信息,四种情况:
* * info:
* * 一:函数:setup 风格
* * 二:reactive、readonly,直接存入状态
* * 三:对象:含有 state 属性 -- option 风格
* * 四:对象:无 state 属性 -- 直接视为 state,option 风格
* @param isLocal Boolean 默认是局部状态
*/
export default function defineStore<T extends IObjectOrArray> (
id: IStateKey,
info: IStateCreateOption | IAnyFunctionObject | IObjectOrArray,
isLocal = true
): T & IState {
// 判断ID是否重复
if (isLocal) {
// 局部状态,可以重复
} else {
// 全局状态,ID 如果重复 返回ID对应的状态
if (store[id]) {
return store[id] as T & IState
}
}
// 创建状态:
/**
* 1. 函数——setup;
* 2. reactive —— 自定义;
* 3. 对象(state)—— option;
* 3.1. 对象 —— 全是state
*/
// setup 风格,执行函数获得结果
if (typeof info === 'function') {
return save<T>(id, isLocal, info())
}
// 自定义,直接存入
if (isReactive(info)) {
return save<T>(id, isLocal, info as T)
}
// 对象, option 风格,有 state 属性
if ((info as IStateCreateOption).state) {
return save<T>(id, isLocal, OptionState(id, info as IStateCreateOption))
}
// 没有 state 属性,info 视为 state
return save<T>(id, isLocal, OptionState(id, { state: info }))
}
info 可以是三种情况:
- 函数:对应的是 setup 风格;
- reactive:自定义类型,如果传入一个reactive,说明外部已经做好了一个状态,那么直接“保存”即可;
- 对象:对应的是 option 风格。
一般是局部状态,当然也可以是全局状态。
/**
* 全局状态存入 store;局部状态存入 provide,返回状态
* @param id 状态标识
* @param isLocal 是否局部状态
* @param state 状态,对象或者数组
* @returns 返回状态
*/
function save<T extends IObjectOrArray>(id: IStateKey, isLocal: boolean, state: T): T & IState {
// 判断是否全局状态
if (isLocal) {
// 局部状态,使用 provide 注入
provide<T>(id, state)
} else {
// 全局状态,存入容器
store[id] = state
}
return state as T & IState
}
如果是局部状态,那么使用 provide 注入;如果是全局状态,存入一个全局变量。(不支持SSR)
获取局部状态
封装一下 inject,组合一下类型。
/**
* 获取局部状态
* @param id 状态的ID
* @returns 局部状态
*/
export default function useStoreLocal<T> (id: IStoreKey): T & IState {
const re = inject<T>(id)
return re as T & IState
}
全局状态:createStore 批量建立全局状态
如果使用全局状态的话,感觉还是在main里面统一创建的好。
这里实现两个功能,一个是遍历集合,使用 defineStore 创建状态,另一个功能就是用Vue的“插件”方式挂载全局状态。
cn.vuejs.org/guide/reusa… Vue 的插件
/**
* 开局时创建一批全局状态。在main里面。
* @param info 状态列表,多个状态,和回调函数
* * store
* * * state 的类型
* * * * function:setup 风格,不记录日志,全局状态
* * * * 对象:
* * * * * 没有 state 属性:整个对象作为 state,无 getter、action
* * * * * 有 state:option 风格
* * init 创建完毕后的回调函数
*/
export default function createStore(info: IStateCreateListOption) {
// 获取状态列表
const tmpStore = info.store
// 遍历,调用 defineStore 注册状态
Object.keys(tmpStore).forEach(key => {
// 创建全局状态
defineStore(key, tmpStore[key], false)
})
// 创建完毕,调用回调
if (typeof info.init === 'function') {
info.init(store)
}
// 安装插件
return (app: any) => {
// 设置模板直接使用状态
app.config.globalProperties.$state = store
// 发现个问题,这是个object,不用注入到 provide
app.provide(_storeFlag, store)
}
}
获取全局状态
从全局容器里面获取全局状态:
/**
* 获取全局状态。
* @param id 全局状态 的 ID,string | symbol
* @returns 指定的状态
*/
export default function useStore<T> (id: IStateKey): T & IState {
if (store[id]) {
return store[id] as T & IState
}
console.error('没有找到这个状态:', id)
return {} as T & IState
}
在 main 里面创建状态
我们可以把状态写在一个或者多个文件里面,然后在main里面加载。
描述一个状态
stateTest.ts
//
export const stateTest = {
state: () => {
return {
name: '全局状态的测试',
age: 20
}
},
getters: {
getAge: (state: any) => {
return state.age + ' + 箭头函数'
}
},
actions: {
addAge2: (state: any, n = 1 ) => {
state.age += 10 * n
}
}
}
创建状态
store/index.ts
import { stateTest } from './stateTest'
// 可以继续加载其他状态
/**
* 统一注册全局状态
*/
export default createStore({
// 定义状态,直接使用 reactive
store: {
stateTest,
...
},
// 可以给全局状态设置初始状态,同步数据可以直接在上面设置,如果是异步数据,可以在这里设置。
init (store: IStore) {
console.log('初始化完成:', store)
}
})
在 main 里面挂载
import { createApp } from 'vue'
import App from './App.vue'
import store from './store'
createApp(App)
.use(store) // 挂载状态
.mount('#app')
在组件里面创建局部状态
还是先在文件里面创建状态,然后在组件里面引入,这是要注意,是创建一个局部状态,还是获取一个局部状态。
state-person.ts
export default () => {
return defineStore('Person', {
state: () => {
return {
name: '基础设置',
age: 20
}
},
getters: {
getAge: (state: any) => {
return state.age + ' + 箭头函数'
}
},
actions: {
addAge2: (state: any, n = 1 ) => {
state.age += 10 * n
}
},
options: {
isLog: false
}
})
}
父组件
// 引入状态
import usePerson from './state-person'
// 创建状态
const person = usePerson()
子组件
// 使用 useStoreLocal 获取局部状态
const person = useStoreLocal('Person')
这里和 Pinia 的设定不一致,如果在子组件像父组件那样引入的话,得到的不是父组件的状态,而是在子组件又创建了一个新的状态。
这样设定是考虑到,组件在嵌套的情况下,可以有自己的状态。
源码
在线演示
转载自:https://juejin.cn/post/7237697495110189116