Pinia进阶setup函数式写法封装到企业项目
作者:南山种子外卖跑手
开场
Hello大家好,相信在座各位假如使用Vue生态开发项目情况下,对Pinia
状态管理库应该有所听闻或正在使用,假如还没接触到Pinia,这篇文章可以帮你快速入门,并如何在企业项目中更优雅封装使用。
本文先给大家阐述如何去理解、使用Pinia,最后讲怎样把Pinia集成到工程中,适合大多数读者,至于研读Pinia的源码等进阶科普,会另外开一篇文章细述。另外,本文的所有demo,都专门开了个GitHub项目来保存,有需要的同学可以拿下来实操一下。🌹🌹
认识Pinia
Pinia
读音:/piːnjʌ/,是Vue官方团队推荐代替Vuex
的一款轻量级状态管理库。 它最初的设计理念是让Vue Store拥有一款Composition API方式的状态管理库,并同时能支持 Vue2.x版本的Option API 和 Vue3版本的setup Composition API开发模式,并完整兼容Typescript写法(这也是优于Vuex的重要因素之一),适用于所有的vue项目。
比起Vuex,Pinia具备以下优点:
- 完整的 TypeScript 支持:与在 Vuex 中添加 TypeScript 相比,添加 TypeScript 更容易
- 极其轻巧(体积约 1KB)
- store 的 action 被调度为常规的函数调用,而不是使用 dispatch 方法或 MapAction 辅助函数,这在 Vuex 中很常见
- 支持多个Store
- 支持 Vue devtools、SSR 和 webpack 代码拆分
Pinia与Vuex代码分割机制
上述的Pinia轻量有一部分体现在它的代码分割机制中。
举个例子:某项目有3个store「user、job、pay」,另外有2个路由页面「首页、个人中心页」,首页用到job store,个人中心页用到了user store,分别用Pinia和Vuex对其状态管理。
先看Vuex的代码分割: 打包时,vuex会把3个store合并打包,当首页用到Vuex时,这个包会引入到首页一起打包,最后输出1个js chunk。这样的问题是,其实首页只需要其中1个store,但其他2个无关的store也被打包进来,造成资源浪费。
Pinia的代码分割: 打包时,Pinia会检查引用依赖,当首页用到job store,打包只会把用到的store和页面合并输出1个js chunk,其他2个store不耦合在其中。Pinia能做到这点,是因为它的设计就是store分离的,解决了项目的耦合问题。
Pinia的常规用法
事不宜迟,直接开始使用Pinia
「本文默认使用Vue3的setup Composition API开发模式」。
假如你对Pinia使用熟悉,可以略过这part
1. 安装
yarn add pinia # or with npm npm install pinia
2. 挂载全局实例
import { createPinia } from 'pinia' app.use(createPinia())
3. 创建第一个store
在src/store/counterForOptions.ts
创建你的store。定义store模式有2种:
使用options API模式定义,这种方式和vue2的组件模型形式类似,也是对vue2技术栈开发者较为友好的编程模式。
import { defineStore } from 'pinia'; // 使用options API模式定义 export const useCounterStoreForOption = defineStore('counterForOptions', { // 定义state state: () => { return { count1: 1 }; }, // 定义action actions: { increment() { this.count1++; } }, // 定义getters getters: { doubleCount(state) { return state.count1 * 2; } } });
使用setup模式定义,符合Vue3 setup的编程模式,让结构更加扁平化,个人推荐推荐使用这种方式。
import { ref } from 'vue'; import { defineStore } from 'pinia'; // 使用setup模式定义 export const useCounterStoreForSetup = defineStore('counterForSetup', () => { const count = ref<number>(1); function increment() { count.value++; } function doubleCount() { return count.value * 2; } return { count, increment, doubleCount }; });
上面2种定义方式效果都是一样的,我们用defineStore
方法定义一个store,里面分别定义1个count
的state,1个increment
action 和1个doubleCount
的getters。其中state是要共享的全局状态,而action则是让业务方调用来改变state的入口,getters是获取state的计算结果。
之所以用第一种方式定义是还要额外写getters
、action
关键字来区分,是因为在options API模式下可以通过mapState()、mapActions()等方法获取对应项;但第二种方式就可以直接获取了(下面会细述)。
4. 业务组件对store的调用
在src/components/PiniaBasicSetup.vue
目录下创建个组件。
<script setup lang="ts" name="component-PiniaBasicSetup"> import { storeToRefs } from 'pinia'; import { useCounterStoreForSetup } from '@/store/counterForSetup'; // setup composition API模式 const counterStoreForSetup = useCounterStoreForSetup(); const { count } = storeToRefs(counterStoreForSetup); const { increment, doubleCount } = counterStoreForSetup; </script> <template> <div class="box-styl"> <h2>Setup模式</h2> <p class="section-box"> Pinia的state: count = <b>{{ count }}</b> </p> <p class="section-box"> Pinia的getters: doubleCount() = <b>{{ doubleCount() }}</b> </p> <div class="section-box"> <p>Pinia的action: increment()</p> <button @click="increment">点我</button> </div> </div> </template> <style lang="less" scoped> .box-styl { margin: 10px; .section-box { margin: 20px auto; width: 300px; background-color: #d7ffed; border: 1px solid #000; } } </style>
- Pinia在setup模式下的调用机制是先install再调用。
- install这样写:
const counterStoreForSetup = useCounterStoreForSetup();
,其中useCounterStoreForSetup
就是你定义store的变量; - 调用就直接用
counterStoreForSetup.xxx
(xxx包括:state、getters、action)就好。 - 代码中获取state是用了解构赋值,为了保持state的响应式特性,需要用
storeToRefs
进行包裹。
兼容Vue2的Options API调用方式
<script lang="ts"> import { mapState, mapActions } from 'pinia'; import { useCounterStoreForOption } from '@/store/counterForOptions'; // setup composition API模式 export default { name: 'component-PiniaBasicOptions', computed: { // 获取state和getters ...mapState(useCounterStoreForOption, ['count', 'doubleCount']) }, methods: { // 获取action ...mapActions(useCounterStoreForOption, { increment: 'increment', reset: 'reset' }) } }; </script> <template> <div class="box-styl"> <h2>Options模式</h2> <p class="section-box"> Pinia的state: count = <b>{{ this.count }}</b> </p> <p class="section-box"> Pinia的getters: doubleCount() = <b>{{ this.doubleCount }}</b> </p> <div class="section-box"> <p>Pinia的action: increment()</p> <button @click="this.increment">点我</button> </div> <div class="section-box"> <p>Pinia的action: reset()</p> <button @click="this.reset">点我</button> </div> </div> </template> <style lang="less" scoped> .box-styl { margin: 10px; .section-box { margin: 20px auto; width: 300px; background-color: #d7ffed; border: 1px solid #000; } } </style>
5. 良好的编程习惯
state的改变交给action去处理: 上面例子,counterStoreForSetup
有个pinia实例属性叫$state
是可以直接改变state的值,但不建议怎么做。一是难维护,在组件繁多情况下,一处隐蔽state更改,整个开发组帮你排查;二是破坏store封装,难以移植到其他地方。所以,为了你的声誉和安全着想,请停止游离之外的coding😇😇。
用hook代替pinia实例属性: install后的counterStoreForSetup
对象里面,带有不少$
开头的方法,其实这些方法大多数都能通过hook引入代替。
其他的想到再补充...
企业项目封装攻略
1. 全局注册机
重复打包问题
在上面的例子我们可以知道,使用store时要先把store的定义import进来,再执行定义函数使得实例化。但是,在项目逐渐庞大起来后,每个组件要使用时候都要实例化吗?在文中开头讲过,pinia的代码分割机制是把引用它的页面合并打包,那像下面的例子就会有问题,user被多个页面引用,最后user store被重复打包。
为了解决这个问题,我们可以引入 ”全局注册“ 的概念。做法如下:
创建总入口
在src/store
目录下创建一个入口index.ts
,其中包含一个注册函数registerStore()
,其作用是把整个项目的store都提前注册好,最后把所有的store实例挂到appStore
透传出去。这样以后,只要我们在项目任何组件要使用pinia时,只要import appStore进来,取对应的store实例就行。
// src/store/index.ts import { roleStore } from './roleStore'; import { useCounterStoreForSetup } from '@/store/counterForSetup'; import { useCounterStoreForOption } from '@/store/counterForOptions'; export interface IAppStore { roleStore: ReturnType<typeof roleStore>; useCounterStoreForSetup: ReturnType<typeof useCounterStoreForSetup>; useCounterStoreForOption: ReturnType<typeof useCounterStoreForOption>; } const appStore: IAppStore = {} as IAppStore; /** * 注册app状态库 */ export const registerStore = () => { appStore.roleStore = roleStore(); appStore.useCounterStoreForSetup = useCounterStoreForSetup(); appStore.useCounterStoreForOption = useCounterStoreForOption(); }; export default appStore;
总线注册
在src/main.ts
项目总线执行注册操作:
import { createApp } from 'vue'; import App from './App.vue'; import { createPinia } from 'pinia'; import { registerStore } from '@/store'; const app = createApp(App); app.use(createPinia()); // 注册pinia状态管理库 registerStore(); app.mount('#app');
业务组件内直接使用
// src/components/PiniaBasicSetup.vue <script setup lang="ts" name="component-PiniaBasicSetup"> import { storeToRefs } from 'pinia'; import appStore from '@/store'; // setup composition API模式 const { count } = storeToRefs(appStore.useCounterStoreForSetup); const { increment, doubleCount } = appStore.useCounterStoreForSetup; </script> <template> <div class="box-styl"> <h2>Setup模式</h2> <p class="section-box"> Pinia的state: count = <b>{{ count }}</b> </p> <p class="section-box"> Pinia的getters: doubleCount() = <b>{{ doubleCount() }}</b> </p> <div class="section-box"> <p>Pinia的action: increment()</p> <button @click="increment">点我</button> </div> </div> </template>
打包解耦
到这里还不行,为了让appStore
实例与项目解耦,在构建时要把appStore
抽取到公共chunk,在vite.config.ts
做如下配置
export default defineConfig(({ command }: ConfigEnv) => { return { // ...其他配置 build: { // ...其他配置 rollupOptions: { output: { manualChunks(id) { // 将pinia的全局库实例打包进vendor,避免和页面一起打包造成资源重复引入 if (id.includes(path.resolve(__dirname, '/src/store/index.ts'))) { return 'vendor'; } } } } } }; });
经过这样封装后,pinia状态库得到解耦,最终的项目结构图是这样的:
2. Store组管理
场景分析
大家在项目中是否经常遇到某个方法要更新多个store的情况呢?例如:你要做个游戏,有3种职业「战士、法师、道士」,另外,玩家角色有3个store来控制「人物属性、装备、技能」,页面有个”转职“按钮,可以转其他职业。当玩家改变职业时,3个store的state都要改变,怎么做呢?
- 方法1:在业务组件创建个函数,单点击”转职“时,获取3个store并且更新它们的值。
- 方法2:抽象一个新pinia store,store里有个”转职“的action,当玩家转职时,响应这个action,在action更新3个store的值。
对比起来,无论从封装还是业务解耦,明显方法2更好。要做到这样,这也得益于pinia的store独立管理特性,我们只需要把抽象的store作为父store,「人物属性、装备、技能」3个store作为单元store,让父store的action去管理自己的单元store。
组级Store创建
继续上才艺,父store:src/store/roleStore/index.ts
import { defineStore } from 'pinia'; import { roleBasic } from './basic'; import { roleEquipment } from './equipment'; import { roleSkill } from './skill'; import { ROLE_INIT_INFO } from './constants'; type TProfession = 'warrior' | 'mage' | 'warlock'; // 角色组,汇聚「人物属性、装备、技能」3个store统一管理 export const roleStore = defineStore('roleStore', () => { // 注册组内store const basic = roleBasic(); const equipment = roleEquipment(); const skill = roleSkill(); // 转职业 function changeProfession(profession: TProfession) { basic.setItem(ROLE_INIT_INFO[profession].basic); equipment.setItem(ROLE_INIT_INFO[profession].equipment); skill.setItem(ROLE_INIT_INFO[profession].skill); } return { basic, equipment, skill, changeProfession }; });
单元Store
3个单元store:
业务组件调用
<script setup lang="ts" name="component-StoreGroup"> import appStore from '@/store'; </script> <template> <div class="box-styl"> <h2>Store组管理</h2> <div class="section-box"> <p> 当前职业: <b>{{ appStore.roleStore.basic.basic.profession }}</b> </p> <p> 名字: <b>{{ appStore.roleStore.basic.basic.name }}</b> </p> <p> 性别: <b>{{ appStore.roleStore.basic.basic.sex }}</b> </p> <p> 装备: <b>{{ appStore.roleStore.equipment.equipment }}</b> </p> <p> 技能: <b>{{ appStore.roleStore.skill.skill }}</b> </p> <span>转职:</span> <button @click="appStore.roleStore.changeProfession('warrior')"> 战士 </button> <button @click="appStore.roleStore.changeProfession('mage')">法师</button> <button @click="appStore.roleStore.changeProfession('warlock')"> 道士 </button> </div> </div> </template> <style lang="less" scoped> .box-styl { margin: 10px; .section-box { margin: 20px auto; width: 300px; background-color: #d7ffed; border: 1px solid #000; } } </style>
效果
落幕
磨刀不误砍柴工,对于一个项目来讲,好的状态管理方案在当中发挥重要的作用,不仅能让项目思路清晰,而且便于项目日后维护和迭代。
项目demo https://github.com/JohnnyZhangQiao/pinia-use
以上就是Pinia进阶setup函数式写法封装到企业项目的详细内容,更多关于Pinia setup函数式封装的资料请关注脚本之家其它相关文章!