基于Vue3做一套适合自己的状态管理(六)在哪里创建状态?需要局部状态吗?

计划章节

  1. 基类:实现辅助功能
  2. 继承:充血实体类
  3. 继承:OptionApi 风格的状态
  4. Model:正确的打开方式
  5. 组合:setup 风格,更灵活
  6. 注册状态的方法、以及局部状态和全局状态
  7. 当前登录用户的状态
  8. 列表页面需要的状态

全局状态还是局部状态?

不知道大家想过这个问题没,或者觉得这根本不是问题。。。我们先分析一下 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 的设定不一致,如果在子组件像父组件那样引入的话,得到的不是父组件的状态,而是在子组件又创建了一个新的状态。

这样设定是考虑到,组件在嵌套的情况下,可以有自己的状态。

源码

gitee.com/naturefw-co…

在线演示

naturefw-code.gitee.io/nf-rollup-s…

原文链接:https://juejin.cn/post/7237697495110189116 作者:金色海洋

(0)
上一篇 2023年5月28日 上午10:36
下一篇 2023年5月28日 上午10:46

相关推荐

发表回复

登录后才能评论