持续创造,加快生长!这是我参加「日新方案 10 月更文挑战」的第N天,点击检查活动概况

前言

在 2020 年 9 月 Vue 3 发布正式版别之后,2021 年 2 月 Vuex 也发布了适配 Vue 3 的 4.0 版别,可是在 2021 年 8 月底,由 Vue 中心团队成员 Eduardo 首要奉献的全新 Vue 状况同享库发布 2.0 版别,并在同年 11 月,尤大正式指定 Pinia 为 Vue 的官方状况库(现在 Vue 官网也现已将 Vuex 替换为了 Pinia)。

什么是 Pinia

Pinia 与 Vuex 相同,是作为 Vue 的“状况存储库”,用来完结 跨页面/组件 方法的数据状况同享。

在平时的开发过程中,Vue 组件之间能够经过 PropsEvents 完结组件之间的音讯传递,关于跨层级的组件也能够经过 EventBus 来完结通讯。可是在大型项目中,通常需求在浏览器 保存多种数据和状况,而运用 Props/Events 或许 EventBus 是很难保护和扩展的。所以才有了 Vuex 和 Pinia。

Pinia 为何能替代 Vuex

作为 Vue 开发者都知道,Vuex 作为 Vue 的老牌官方状况库,现已和 Vue 一起存在了很长时间,为什么现在会被 Pinia 替代呢?

官方的说法首要是以下几点:

  1. 撤销 mutations。由于在大部分开发者眼中,mutations 只支撑 同步修改状况数据,而 actions 尽管支撑 异步,却仍然要在内部调用 mutations 去修改状况,无疑是十分繁琐和剩余的
  2. 一切的代码都是 TypeScript 编写的,而且一切接口都尽可能的利用了 TypeScript 的 类型揣度,而不像 Vuex 相同需求自界说 TS 的包装器来完结对 TypeScript 的支撑
  3. 不像 Vuex 相同需求在实例/Vue原型上注入状况依靠,而是经过直接引入状况模块、调用 getter/actions 函数来完结状况的更新获取;而且由于自身对 TypeScript 的杰出支撑和类型揣度,开发者能够享用很优异的代码提示
  4. 不需求预先注册状况数据,默认情况下都是依据代码逻辑自动处理的;而且能够在运用中随时注册新的状况
  5. 没有 Vuex 的 modules 嵌套结构,一切状况都是扁平化办理的。也能够理解为 pinia 注册的状况都相似 vuex 的 module,仅仅 pinia 不需求一致的入口来注册一切状况模块
  6. 尽管是扁平化的结构,可是仍然支撑 每个状况之间的相互引证和嵌套
  7. 不需求 namespace 命名空间,得利于扁平化结构,每个状况在注册时即便没有声明状况模块称号,pinia 也会默认对它进行处理

总结一下便是:Pinia 在完结 Vuex 大局状况同享的功用前提下,改善了状况存储结构,优化了运用方法,简化了 API 设计与标准;而且依据 TypeScript 的类型揣度,为开发者供给了杰出的 TypeScript 支撑与代码提示。

如何运用

至于 Pinia 在项目中的装置,咱们应该都知道,直接经过包办理东西装置即可。

1. 注册 Pinia 实例

以 Vue 3 项目为例,只需求在入口文件 main.ts 中引入即可完结 Pinia 的注册。

import { createApp } from 'vue'
import { createPinia } from 'pinia'
const app = createApp(App)
const pinia = createPinia()
app.use(pinia)

当然,由于支撑 createApp 支撑 链式调用,所以也能够直接写成 createApp(App).use(createPinia()).mount('#app').

此刻 createPinia() 创立的是一个根实例,在 app.use 的时分会在 app 中注入该实例,而且装备一个 app.config.globalProperties.$pinia 也指向该实例。

2. 界说状况 Store

在注册一个 Pinia 状况模块的时分,能够经过 defineStore 方法创立一个 状况模块函数(之所以是函数,是由于后边调用的时分需求经过函数的方法获取到里面的状况)。

deineStore 函数的 TypeScript 界说如下:

function defineStore<Id, S, G, A>(id, options): StoreDefinition<Id, S, G, A>
function defineStore<Id, S, G, A>(options): StoreDefinition<Id, S, G, A>
function defineStore<Id, SS>(id, storeSetup, options?): StoreDefinition<Id, _ExtractStateFromSetupStore<SS>, _ExtractGettersFromSetupStore<SS>, _ExtractActionsFromSetupStore<SS>>
type Id = ID extends string
type storeSetup = () => SS
type options = Omit<DefineStoreOptions<Id, S, G, A>, "id"> | DefineStoreOptions<Id, S, G, A> | DefineSetupStoreOptions<Id, _ExtractStateFromSetupStore<SS>, _ExtractGettersFromSetupStore<SS>, _ExtractActionsFromSetupStore<SS>>

能够看到该函数最多接纳 3个参数,可是咱们最常用的一般都是第一种或许第二种方法。这儿以 第一种方法 例,创立一个状况模块函数:

// 该部分节选字我的开源项目 vite-vue-bpmn-process
import { defineStore } from 'pinia'
import { defaultSettings } from '@/config'
import { EditorSettings } from 'types/editor/settings'
const state = {
  editorSettings: defaultSettings
}
export default defineStore('editor', {
  state: () => state,
  getters: {
    getProcessDef: (state) => ({
      processName: state.editorSettings.processName,
      processId: state.editorSettings.processId
    }),
    getProcessEngine: (state) => state.editorSettings.processEngine,
    getEditorConfig: (state) => state.editorSettings
  },
  actions: {
    updateConfiguration(conf: Partial<EditorSettings>) {
      this.editorSettings = { ...this.editorSettings, ...conf }
    }
  }
})

其间的 options 装备项包括三个部分:

  • state:状况的初始值,引荐运用的是一个 箭头函数,便利进行类型揣度
  • getters:状况的获取,是一个目标格局;引荐装备为每个 getters 的目标属性为 箭头函数,便利进行类型揣度;在运用时等同于获取该函数处理后的 state 状况成果;而且与 Vue 的核算属性相同,该方法也是慵懒的,具有缓存作用
  • actions:相似 Vue 中的 methods 装备项,支撑异步操作,首要作用是 处理事务逻辑并更新状况数据;另外,此刻的 actions 是一个 函数集合目标,与 getters 不同的是 不主张运用箭头函数而且函数内部的 this 就指向当时 store 的 state。

留意:getters 的函数界说中 第一个参数便是当时 store 的状况数据 state,而 actions 中的函数参数为 实际调用时传递的参数,能够传递多个,内部经过 this 上下文 直接拜访 state 并进行更新。

3. 组件运用(合作 setup)

众所周知,vue 3 最大的亮点之一便是 组合式API(Composition API),所以咱们先以组件合作 setup 运用。

import { defineComponent, ref, computed } from 'vue'
import { storeToRefs } from 'pinia'
import { EditorSettings } from 'types/editor/settings'
import editorStore from '@/store/editor'
export default defineComponent({
  setup(props) {
    const editor = editorStore()
    // 直接获取 state 状况
    const { editorSettings } = storeToRefs(editor)
    // 运用 computed
    const editorSettings = computed(() => editor.editorSettings)
    // getters
    const prefix = editor.getProcessEngine
    // 更新方法 1:调用 actions
    editorStore.updateConfiguration({})
    // 更新方法 2:直接改动 state 的值
    editorStore.editorSettings = {}
    // 更新方法 3:调用 $patch
    editorStore.$patch((state) => {
      state.editorSettings = {}
    })
    return {
      editorStore
    }
  }
})

这儿对以上几种处理方法进行说明:

获取值:

  1. 能够经过 解构 获取 state 界说的数据,可是 解构会失去响应式,所以需求用 storeToRefs 重新对其进行响应式处理
  2. 经过 computed 核算属性,好处是 能够对 state 中的状况数据进行组合
  3. 经过界说的 getters 方法来获取值,这种方法获取的成果自身便是 响应式的,能够直接运用

更新值:

  1. 首先是能够 直接改动 state 的状况值,缺点是多次运用容易有重复代码,且欠好保护;也会影响代码的可读性
  2. 经过界说的 actions 更新,也算是引荐方法之一;在后续迭代和扩展中,只需求保护好 store 中的代码即可
  3. $patch: 这个方法 能够接纳一个目标或许函数,可是 引荐运用箭头函数(函数参数为状况数据 state);由于如果是目标,则需求依据新数据和当时状况 重建整个 state,增加了很多的功能损耗;而运用箭头函数,其实就与 actions 中的方法相似,能够 按代码逻辑修改指定的状况数据

4. 组件运用(没有 setup)

而在传统的 optionsAPI 模式的组件中(也没有装备 setup),Pinia 也供给了与 Vuex 一致的 API:mapState,mapGetters,mapActions,另外还增加了 mapStores 用来拜访一切已注册的 store 数据,新增了 mapWritableState 用来 界说可更新状况;也由于 pinia 没有 mutations,所以也撤销了 mapMutations 的支撑。

mapGetters 也仅仅为了便利搬迁 Vuex 的组件代码,后边仍然主张 运用 mapState 替换 mapGetters

<template>
	<div>
    <p>{{ settings }}</p>
    <p>{{ processEngine }}</p>
    <button @click="updateConfiguration({})">调用 action</button>
    <button @click="update">调用 mapWritableState</button>
  </div>
</template>
<script>
  import { defineComponent, ref, storeToRefs } from 'vue'
  import { mapState, mapActions, mapWritableState } from 'pinia'
  import editorStore from '@/store/editor'
  export default defineComponent({
    computed: {
      ...mapState(editorStore, {
        settings: 'editorSettings',
        processEngine: (state) => `This process engine is ${state.editorSettings.processEngine}`
      }),
      ...mapWritableState(editorStore, ['editorSettings'])
    },
    methods: {
      ...mapActions(editorStore, ['updateConfiguration']),
      update() {
        this.editorSettings.processEngine = "xxx"
      }
    }
  })
</script>

mapStores 用来拜访 一切已注册 store 状况。假定咱们除了上文界说的 editor,还界说了一个 id 为 modeler 的 store,则能够这么运用:

import editor from '@/store/editor'
import modeler from '@/store/modeler'
export default defineComponent({
  computed: {
    ...mapStores(editor, modeler)
  },
  methods: {
    async updateAll() {
      if (this.editorStore.processEngine === 'camunda') {
        await this.modelerStore.update()
      }
    }
  }
})

其间引证的一切 store,都能够经过 id + ‘Store’ 的方法在 Vue 实例中拜访到。

5. 相互引证

由于 Pinia 自身是支撑各个 store 模块相互引证的,所以在界说的时分能够直接引证其他 store 的数据进行操作。

例如咱们这儿依据 editor store 创立一个 modeler store

import { defineStore } from 'pinia'
import editor from '@/store/editor'
export default defineStore('editor', {
  state: () => ({
    element: null,
    modeler: null
  }),
  actions: {
    updateElement(element) {
      const editorStore = editor()
      if (!editorStore.getProcessEngine) {
        editorStore.updateConfiguration({ processEngine: 'camunda' })
      }
      this.element = element
    }
  }
})

6. 脱离 store 模块和组件运用

由于 Pinia 的每个 store 模块都是依靠 vue 运用和 pinia 根实例的,在组件内部运用时由于 Vue 运用和 pinia 根实例必定都现已是 注册完结处于活动状况中的,所以能够直接经过调用对应的 store 状况模块函数即可。

可是在脱离 store 模块与组件,直接在外部的纯函数中运用时,则需求留意 store 状况模块函数的调用时机。

以官方的示例来看:

import { createRouter } from 'vue-router'
const router = createRouter({
  // ...
})
// ❌ 依据导入的顺序,这将失利
const store = useStore()
router.beforeEach((to, from, next) => {
  // 咱们想在这儿运用 store 
  if (store.isLoggedIn) next()
  else next('/login')
})
router.beforeEach((to) => {
  // ✅ 这将起作用,由于路由器在之后开始导航
   // 路由已装置,pinia 也将装置
  const store = useStore()
  if (to.meta.requiresAuth && !store.isLoggedIn) return '/login'
})

直接在js模块的履行中 直接调用是可能会报错的,由于此刻可能在 import router 的时分 还没有调用 createApp 和 createPinia 创立对应的运用实例和 pinia 根实例,所以无法运用。

而在路由导航的拦截器中运用时,由于 路由拦截触发时,运用和 pinia 根实例必定现已悉数实例化结束,才能够正常运用。

所以 如果是在外部的 hooks 函数或许 utils 东西函数等纯函数模块中运用 store 数据时,最好是界说一个函数方法导出,在组件或许 store 模块中调用该方法,确保此刻能正确履行

最终

总的来说,Pinia 作为 Vue 官方引荐的状况库,合作 Vue 3 的组合式 API,能够更好的完结项目中各种数据状况的办理,而不是像曾经运用 Vuex 相同经过 modules 的方法注册各种状况。Pinia 关于抽离逻辑进行复用(hooks),简化运用方法来说,比之前的 Vuex 好了很多倍;加上杰出的类型支撑与代码提示,让咱们在开发过程中能够省去很多前置工作,也是对咱们的开发效率的一种提升吧。

当然,、Vue DevTools 在更新之后,也完结了对 Pinia 的支撑。

往期精彩

Bpmn.js 进阶指南

Vue 2 源码阅读理解

一行指令完结大屏元素分辨率适配(Vue)

依据 Vue 2 与 高德地图 2.0 的“线面编辑器”