前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Vue项目进阶:再谈Pinia函数式(composition API)用法

Vue项目进阶:再谈Pinia函数式(composition API)用法

作者头像
南山种子外卖跑手
发布2022-04-24 21:31:22
1.3K0
发布2022-04-24 21:31:22
举报
文章被收录于专栏:南山种子外卖跑手的专栏

本文为原创文章,引用请注明出处,欢迎大家收藏和分享💐💐

背景

Hello大家好,前段时间写了一篇关于Pinia的composition API用法的文章《Pinia进阶:优雅的setup(函数式)写法+封装到你的企业项目》,收到了不少朋友的反馈和建议。笔者也结合最近项目情况和网友们的建议做一次优化,也算是一个比较完整的版本了,这包括:

  • options APIcomposition API写法的完整映射示例
  • 补充composition API模式的ts类型提示补充
  • 关于Store实例的内置函数一些思考
  • 工程项目下pinia相关模块封装

本文的所有demo专门开了个GitHub项目来保存,有需要的同学可以拿下来实践。🌹🌹

认识Pinia

相信在座各位假如使用Vue生态开发项目情况下,对Pinia状态管理库应该有所听闻或正在使用,假如还没接触到Pinia,这篇文章可以帮你快速入门,并如何在企业项目中更优雅封装使用。

Pinia**读音:/piːnjʌ/,是Vue官方团队推荐代替**Vuex**的一款轻量级状态管理库。** 它最初的设计理念是让Vue Store拥有一款Composition API方式的状态管理库,并同时能支持 Vue2.x版本的Option API 和 Vue3版本的setup Composition API开发模式,并完整兼容Typescript写法(这也是优于Vuex的重要因素之一),适用于所有的vue项目。前段时间尤雨溪也说过Pinia其实就是Vuex5,其诞生是更好服务于Vue3的。

比起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的 Composition API 写法

事不宜迟,直接开始使用Pinia

1. 安装

代码语言:javascript
复制
yarn add pinia
# or with npm
npm install pinia

2. 项目引入

代码语言:javascript
复制
import { createPinia } from 'pinia'

app.use(createPinia())

3. 创建第一个store

定义store模式有2种:options API 和 composition API

前者不做细述了,社区文章一大堆;使用composition API模式定义store,符合Vue3 setup的编程模式,让结构更加扁平化,个人推荐推荐使用这种方式。

src/store/counterForSetup.ts创建第一个的store:

代码语言:javascript
复制
import { ref, computed } from 'vue';
import { defineStore } from 'pinia';

// 使用composition API模式定义store
export const useCounterStoreForSetup = defineStore('counterForSetup', () => {
  const count = ref<number>(1);
  const doubleCount = computed(() => count.value * 2);
  function increment() {
    count.value++;
  }

  return { count, doubleCount, increment };
});

这与下面定义方式是完全等价的「至少在实例化之前是🐶🐶,实例化后有点不一样下面会讲到,但不影响使用」:

代码语言:javascript
复制
import { defineStore } from 'pinia';

// 使用options API模式定义
export const useCounterStoreForOption = defineStore('counterForOptions', {
  // 定义state
  state: () => {
    return { count: 1 };
  },
  // 定义action
  actions: {
    increment() {
      this.count++;
    }
  },
  // 定义getters
  getters: {
    doubleCount(state) {
      return state.count * 2;
    }
  }
});

上述可以看出来:

  1. composition API中的**ref**、**reactive**定义的变量等价于options API中的**state**;
  2. composition API中的**computed**属性等价于options API中的**getters**;
  3. composition API中的导出函数等价于options API中的**actions**;

最后两者在devtools的表现完全一致:

在Typescript类型提醒上是这样的:

4. 业务组件对store的调用

src/components/PiniaBasicSetup.vue目录下创建个组件,在组件内对store实例化后可直接调用。

代码语言:javascript
复制
<script setup lang="ts" name="component-PiniaBasicSetup">
import { storeToRefs } from 'pinia';
import { useCounterStoreForSetup } from '@/store/counterForSetup';

// composition API模式
const counterStoreForSetup = useCounterStoreForSetup();
// 确保解构确保后的state具有响应式,要使用storeToRefs方法
const { count, doubleCount } = storeToRefs(counterStoreForSetup);
const { increment } = counterStoreForSetup;
</script>

<template>
  <div class="box-styl">
    <h1>Setup模式</h1>
    <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的调用机制是先install再调用
  • install这样写: const counterStoreForSetup = useCounterStoreForSetup();,其中 useCounterStoreForSetup就是你定义store的变量;
  • 调用就直接用 counterStoreForSetup.xxx(xxx包括:state、getters、action)就好。
  • 代码中获取state是用了解构赋值,为了保持state的响应式特性,需要用storeToRefs进行包裹。

兼容Vue2的Options API调用方式可以到 这里

5. 两种写法的差异

虽然options API 和 composition API写法都能呈现同样的状态库,但是他们有没有差异?实际上还是有的,举个例子:composition API模式下不支持某些内置方法(例如$reset())。

解决方法就是重写reset函数,上面代码可改成这样:

代码语言:javascript
复制
// 使用composition API模式定义store
export const useCounterStoreForSetup = defineStore('counterForSetup', () => {
  // 初始状态
  const initState = {
    count: 1
  };

  const count = ref<number>(initState.count);
  const doubleCount = computed(() => count.value * 2);
  function increment() {
    count.value++;
  }

  // 重写reset
  function reset() {
    count.value = initState.count;
  }

  return { count, doubleCount, increment, reset };
});

6. state的改变交给action去处理

来看下实例化后的store对象,你会发现有个属性叫state,它能直接改变state的值,例如useCounterStoreForSetup. state.count = 2;就能把count的状态变成2,但不建议这么做,有2点考虑:

  1. 不利于统一管理。count名称有更改,所有.$state.count的地方都要更新代码,任何一处遗漏都会直接导致页面报错。所以我们应该声明个唯一的action去管理count的变更,当有变动时只需要更改action就ok了。
  1. 不利于状态调试。 众所周知,devtools的Timeline是可以对actions进行跟踪的,所有的state变更统一的交由action处理,开发者能清晰、容易排查state的变化流,避免出非正常流的状态变更遗漏。

企业项目封装攻略

1. 全局注册机

重复打包问题

在上面的例子我们可以知道,使用store时要先把store的定义import进来,再执行定义函数使得实例化。但是,在项目逐渐庞大起来后,每个组件要使用时候都要实例化吗?在文中开头讲过,pinia的代码分割机制是把引用它的页面合并打包,那像下面的例子就会有问题,user被多个页面引用,最后user store被重复打包。

为了解决这个问题,我们可以引入 ”全局注册“ 的概念。做法如下:

创建总入口

src/store目录下创建一个入口index.ts,其中包含一个注册函数registerStore(),其作用是把整个项目的store都提前注册好,最后把所有的store实例挂到appStore透传出去。这样以后,只要我们在项目任何组件要使用pinia时,只要import appStore进来,取对应的store实例就行。

代码语言:javascript
复制
// 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项目总线执行注册操作:

代码语言:javascript
复制
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');

业务组件内直接使用

代码语言:javascript
复制
// 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">
    <h1>Setup模式</h1>
    <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做如下配置

代码语言:javascript
复制
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状态库得到解耦,最终的项目结构图是这样的:

答疑

到这里或许你会有某些疑惑:把所有store提前实例化和vuex有什么区别,还有就是这么做的必要性?

先说下与vuex的区别:差不多但又不完全一样。。。差别在于vuex是全部store都注册了个遍,开发者根本没法选择哪些store可以懒加载。但全局注册机只会把高频常用的、项目启动完成前必须要放到初始化阶段注册的全局store提前给准备好,一来保证项目加载流程合理性,二来可以避免在业务组件反复注册。其必要性就相当于:你可以不需要,但作为项目管理层面我不能没有「逐渐卷起来了」。

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

代码语言:javascript
复制
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:

业务组件调用

代码语言:javascript
复制
<script setup lang="ts" name="component-StoreGroup">
import appStore from '@/store';
</script>

<template>
  <div class="box-styl">
    <h1>Store组管理</h1>
    <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>

效果

文末随感

磨刀不误砍柴工,对于一个项目来讲,好的状态管理方案在当中发挥重要的作用,不仅能让项目思路清晰,而且便于项目日后维护和迭代。

这也是笔者二次谈Pinia了,前面一篇有些地方解析不完全,也是在这里补充上了,希望能给大家在实际中代码生产中起到作用。

项目传送门

最后重新提下,本文的所有demo,都专门开了个GitHub项目来保存,有需要的同学可以拿下来实操。🌹🌹

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022/04/21 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 背景
  • 认识Pinia
  • Pinia与Vuex代码分割机制
  • Pinia的 Composition API 写法
    • 1. 安装
      • 2. 项目引入
        • 3. 创建第一个store
          • 4. 业务组件对store的调用
            • 5. 两种写法的差异
              • 6. state的改变交给action去处理
              • 企业项目封装攻略
                • 1. 全局注册机
                  • 重复打包问题
                  • 创建总入口
                  • 总线注册
                  • 打包解耦
                  • 答疑
                • 2. Store组管理
                  • 场景分析
                  • 组级Store创建
                  • 单元Store
                  • 业务组件调用
                  • 效果
              • 文末随感
              • 项目传送门
              相关产品与服务
              项目管理
              CODING 项目管理(CODING Project Management,CODING-PM)工具包含迭代管理、需求管理、任务管理、缺陷管理、文件/wiki 等功能,适用于研发团队进行项目管理或敏捷开发实践。结合敏捷研发理念,帮助您对产品进行迭代规划,让每个迭代中的需求、任务、缺陷无障碍沟通流转, 让项目开发过程风险可控,达到可持续性快速迭代。
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档