草庐IT

Pinia进阶:优雅的setup(函数式)写法+封装

老鼠AI大米_Java全栈 2023-03-28 原文

相信在座各位假如使用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对其状态管理。

image.png

先看Vuex的代码分割: 打包时,vuex会把3个store合并打包,当首页用到Vuex时,这个包会引入到首页一起打包,最后输出1个js chunk。这样的问题是,其实首页只需要其中1个store,但其他2个无关的store也被打包进来,造成资源浪费。

image.png

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的计算结果。

之所以用第一种方式定义是还要额外写gettersaction关键字来区分,是因为在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">
    <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在setup模式下的调用机制是先install再调用

  • install这样写: const counterStoreForSetup = useCounterStoreForSetup();,其中 useCounterStoreForSetup就是你定义store的变量;

  • 调用就直接用 counterStoreForSetup.xxx(xxx包括:state、getters、action)就好。

  • 代码中获取state是用了解构赋值,为了保持state的响应式特性,需要用storeToRefs进行包裹。

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

5. 良好的编程习惯

state的改变交给action去处理: 上面例子,counterStoreForSetup有个pinia实例属性叫$state是可以直接改变state的值,但不建议怎么做。一是难维护,在组件繁多情况下,一处隐蔽state更改,整个开发组帮你排查;二是破坏store封装,难以移植到其他地方。所以,为了你的声誉和安全着想,请停止游离之外的coding??。

用hook代替pinia实例属性: install后的counterStoreForSetup对象里面,带有不少$开头的方法,其实这些方法大多数都能通过hook引入代替。

其他的想到再补充...

企业项目封装攻略

1. 全局注册机

重复打包问题

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

image.png

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

创建总入口

在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">
    <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做如下配置

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

image.png

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">
    <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>
复制代码

效果

image.png

落幕

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

有关Pinia进阶:优雅的setup(函数式)写法+封装的更多相关文章

  1. ruby - 在没有 sass 引擎的情况下使用 sass 颜色函数 - 2

    我想在一个没有Sass引擎的类中使用Sass颜色函数。我已经在项目中使用了sassgem,所以我认为搭载会像以下一样简单:classRectangleincludeSass::Script::FunctionsdefcolorSass::Script::Color.new([0x82,0x39,0x06])enddefrender#hamlengineexecutedwithcontextofself#sothatwithintemlateicouldcall#%stop{offset:'0%',stop:{color:lighten(color)}}endend更新:参见上面的#re

  2. ruby-on-rails - 在 ruby​​ 中使用 gsub 函数替换单词 - 2

    我正在尝试用ruby​​中的gsub函数替换字符串中的某些单词,但有时效果很好,在某些情况下会出现此错误?这种格式有什么问题吗NoMethodError(undefinedmethod`gsub!'fornil:NilClass):模型.rbclassTest"replacethisID1",WAY=>"replacethisID2andID3",DELTA=>"replacethisID4"}end另一个模型.rbclassCheck 最佳答案 啊,我找到了!gsub!是一个非常奇怪的方法。首先,它替换了字符串,所以它实际上修改了

  3. ruby - 在 Ruby 中有条件地定义函数 - 2

    我有一些代码在几个不同的位置之一运行:作为具有调试输出的命令行工具,作为不接受任何输出的更大程序的一部分,以及在Rails环境中。有时我需要根据代码的位置对代码进行细微的更改,我意识到以下样式似乎可行:print"Testingnestedfunctionsdefined\n"CLI=trueifCLIdeftest_printprint"CommandLineVersion\n"endelsedeftest_printprint"ReleaseVersion\n"endendtest_print()这导致:TestingnestedfunctionsdefinedCommandLin

  4. ruby - 在 Ruby 中按名称传递函数 - 2

    如何在Ruby中按名称传递函数?(我使用Ruby才几个小时,所以我还在想办法。)nums=[1,2,3,4]#Thisworks,butismoreverbosethanI'dlikenums.eachdo|i|putsiend#InJS,Icouldjustdosomethinglike:#nums.forEach(console.log)#InF#,itwouldbesomethinglike:#List.iternums(printf"%A")#InRuby,IwishIcoulddosomethinglike:nums.eachputs在Ruby中能不能做到类似的简洁?我可以只

  5. C51单片机——实现用独立按键控制LED亮灭(调用函数篇) - 2

    说在前面这部分我本来是合为一篇来写的,因为目的是一样的,都是通过独立按键来控制LED闪灭本质上是起到开关的作用,即调用函数和中断函数。但是写一篇太累了,我还是决定分为两篇写,这篇是调用函数篇。在本篇中你主要看到这些东西!!!1.调用函数的方法(主要讲语法和格式)2.独立按键如何控制LED亮灭3.程序中的一些细节(软件消抖等)1.调用函数的方法思路还是比较清晰地,就是通过按下按键来控制LED闪灭,即每按下一次,LED取反一次。重要的是,把按键与LED联系在一起。我打算用K1来作为开关,看了一下开发板原理图,K1连接的是单片机的P31口,当按下K1时,P31是与GND相连的,也就是说,当我按下去时

  6. ruby-on-rails - Rails 优雅地处理超时 session ? - 2

    使用rails4,ruby2。我在rails配置中为我的cookiesession设置了30分钟的超时时间。问题是,如果我转到表单,让session超时,然后提交表单,我会收到此ActionController::InvalidAuthenticityToken错误。如何在Rails中优雅地处理这个错误?比如说,重定向到登录屏幕? 最佳答案 在您的ApplicationController:rescue_fromActionController::InvalidAuthenticityTokendoredirect_tosome_p

  7. ruby - 获取数组中的值并最小化某个类属性的最优雅的方法是什么? - 2

    假设我有以下类(class):classPersondefinitialize(name,age)@name=name@age=ageenddefget_agereturn@ageendend我有一组Person对象。是否有一种简洁的、类似于Ruby的方法来获取最小(或最大)年龄的人?如何根据它对它们进行排序? 最佳答案 这样做会:people_array.min_by(&:get_age)people_array.max_by(&:get_age)people_array.sort_by(&:get_age)

  8. ruby-on-rails - 将字符串转换为 ruby​​-on-rails 中的函数 - 2

    我需要一个通过输入字符串进行计算的方法,像这样function="(a/b)*100"a=25b=50function.something>>50有什么方法吗? 最佳答案 您可以使用instance_eval:function="(a/b)*100"a=25.0b=50instance_evalfunction#=>50.0请注意,使用eval本质上是不安全的,尤其是当您使用外部输入时,因为它可能包含注入(inject)的恶意代码。另请注意,a设置为25.0而不是25,因为如果它是整数a/b将导致0(整数)。

  9. ruby-on-rails - 优雅的 Rails : multiple routes, 相同的 Controller Action - 2

    让多条路线去同一条路的最优雅的方式是什么ControllerAction?我有:get'dashboard',to:'dashboard#index'get'dashboard/pending',to:'dashboard#index'get'dashboard/live',to:'dashboard#index'get'dashboard/sold',to:'dashboard#index'这很丑陋。有什么“更优雅”的建议吗?一个类轮的奖励积分。 最佳答案 为什么不只有一个路由和一个Controller操作,并根据传递给它的参数来

  10. ruby - 鸭子输入字符串、符号和数组的优雅方式? - 2

    这是针对我无法破坏的现有公共(public)API,但我确实希望对其进行扩展。目前,该方法采用字符串或符号或任何其他在作为第一个参数传递给send时有意义的内容我想添加发送字符串、符号等列表的功能。我可以只使用is_a吗?数组,但还有其他发送列表的方法,这不是很像ruby​​。我将调用列表中的map,所以第一个倾向是使用respond_to?:map。但是字符串也会响应:map,所以这行不通。 最佳答案 如何将它们全部视为数组?String的行为与仅包含String的Array相同:deffoo(obj,arg)[*arg].eac

随机推荐