1、介绍

Pinia​​ 最初是在2019年11月左右使用​​Composition API​​​重新设计​​Vue Store​​​。从那时起,最初的原则仍然相同,但​​Pinia​​​同时适用于​​Vue 2​​​和​​Vue 3​​​,并且不要求您使用​​Composition API​​​。除了安装和​​SSR​​​之外,两者的​​API​​​都是相同的,这些文档针对​​Vue 3​​​,并在必要时提供有关​​Vue 2​​​的注释,以便​​Vue 2​​​和​​Vue 3​​的用户都可以阅读!

1.1 为什么要使用 Pinia?

​Pinia​​​是​​Vue​​​的一个​​Store​​​库,它允许您跨组件/页面共享状态。如果您熟悉​​Composition API​​​,您可能会认为您已经可以用一个简单的​​export const state = reactive({})​​​来共享一个全局状态 。这对于单页应用程序来说是正确的,但是如果应用程序是在服务器端呈现的,那么它就会暴露出安全漏洞。但即使在小型单页应用程序中,使用 ​​Pinia​​也能获得很多好处:

  • ​Devtools​​支持
  • 追踪​​actions​​​,​​mutations​​的时间线
  • ​stores​​出现在使用它们的组件中
  • 时间旅行和更方便的调试
  • 热模块更新
  • 在不重新加载页面的情况下修改​​stores​
  • 在开发过程中保持任何现有状态
  • 插件:使用插件扩展​​Pinia​​功能
  • 为 JS 用户提供适当的​​TypeScript​​​支持或​​autocompletion​
  • 服务器端渲染支持

1.2 基础示例

这就是使用​​Pinia​​​在​​API​​​方面的样子。首先创建一个 ​​store​​:

// stores/counter.js
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
state: () => {
return { count: 0 }
},
// could also be defined as
// state: () => ({ count: 0 })
actions: {
increment() {
this.count++
},
},
})

然后在一个组件中使用它:

import { useCounterStore } from '@/stores/counter'

export default {
setup() {
const counter = useCounterStore()

counter.count++
// with autocompletion ✨
counter.$patch({ count: counter.count + 1 })
// or using an action instead
counter.increment()
},
}

您甚至可以使用一个函数(类似于组件的​​setup()​​​)来为更高级的用例定义一个​​store​​:

export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
function increment() {
count.value++
}
return { count, increment }
})

如果你还不熟悉 ​​setup()​​​ 和 ​​Composition API​​​,别担心,Pinia 也支持一组类似​​Vuex辅助函数​​​。 您以相同的方式定义​​store​​,但随后使用 mapStores()、mapState() 或 mapActions():

const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
getters: {
double: (state) => state.count * 2,
},
actions: {
increment() {
this.count++
}
}
})

const useUserStore = defineStore('user', {
// ...
})

export default {
computed: {
// other computed properties
// ...
// gives access to this.counterStore and this.userStore
...mapStores(useCounterStore, useUserStore)
// gives read access to this.count and this.double
...mapState(useCounterStore, ['count', 'double']),
},
methods: {
// gives access to this.increment()
...mapActions(useCounterStore, ['increment']),
},
}

您将在核心概念中找到关于每个辅助函数的更多信息。

1.3 为什么叫 Pinia

​Pinia​​​(发音为​​/piːnjʌ/​​​,就像英语中的​​“peenya”​​​)是最接近​​piña​​​(西班牙语中的 “菠萝​​pineapple​​​”)的一个有效的包名。事实上,菠萝是一群单独的花朵结合在一起,形成了多个果实的一种水果。与​​Stores​​​类似,每个​​store​​都是独立生成的,但他们最终都是连接在一起的。菠萝也是一种原产于南美洲的美味热带水果。

1.4 一个更实际的示例

下面是一个更完整的​​API​​​示例,您将在​​Pinia​​​中使用它,甚至在​​JavaScript​​中使用它的类型。对于一些人来说,这可能已经足够了,不需要进一步阅读就可以开始了,但我们仍然建议阅读完文档的其余部分,甚至跳过这个例子,当你阅读了所有的核心概念后再回来。

import { defineStore } from 'pinia'

export const todos = defineStore('todos', {
state: () => ({
/** @type {{ text: string, id: number, isFinished: boolean }[]} */
todos: [],
/** @type {'all' | 'finished' | 'unfinished'} */
filter: 'all',
// type will be automatically inferred to number
nextId: 0,
}),
getters: {
finishedTodos(state) {
// autocompletion! ✨
return state.todos.filter((todo) => todo.isFinished)
},
unfinishedTodos(state) {
return state.todos.filter((todo) => !todo.isFinished)
},
/**
* @returns {{ text: string, id: number, isFinished: boolean }[]}
*/
filteredTodos(state) {
if (this.filter === 'finished') {
// call other getters with autocompletion ✨
return this.finishedTodos
} else if (this.filter === 'unfinished') {
return this.unfinishedTodos
}
return this.todos
},
},
actions: {
// any amount of arguments, return a promise or not
addTodo(text) {
// you can directly mutate the state
this.todos.push({ text, id: this.nextId++, isFinished: false })
},
},
})

1.5 与 Vuex 对比

Pinia 最初是为了探索 Vuex 的下一次迭代会是什么样子,结合了 Vuex 5 核心团队讨论中的许多想法。最终,我们意识到 Pinia 已经实现了我们在 Vuex 5 中想要的大部分内容,并决定实现它 取而代之的是新的建议。

与 Vuex 相比,Pinia 提供了一个更简单的 API,具有更少的规范,提供了 Composition-API 风格的 API,最重要的是,在与 TypeScript 一起使用时具有可靠的类型推断支持。

1.5.1 RFC

虽然 Vuex 通过 RFC 从社区收集尽可能多的反馈,但 Pinia 没有。 我根据我开发应用程序、阅读其他人的代码、为使用 Pinia 的客户工作以及在 Discord 上回答问题的经验来测试想法。 这使我能够提供一种适用于各种情况和应用程序大小的有效解决方案。 我经常发布并在保持其核心 API 不变的同时使库不断发展

1.5.2 与 Vuex 3.x/4.x 对比

​Pinia API​​​与​​Vuex ≤ 4​​有很大差异,如:

  • mutations 不再存在。他们经常被认为是 非常 冗长。他们最初带来了 devtools 集成,但这不再是问题。
  • 无需创建自定义复杂包装器来支持 TypeScript,所有内容都是类型化的,并且 API 的设计方式尽可能利用 TS 类型推断。
  • 不再需要注入、导入函数、调用函数、享受自动完成功能!
  • 无需动态添加 Store,默认情况下它们都是动态的,您甚至都不会注意到。请注意,您仍然可以随时手动使用 Store 进行注册,但因为它是自动的,您无需担心。
  • 不再有 modules 的嵌套结构。您仍然可以通过在另一个 Store 中导入和 使用 来隐式嵌套 Store,但 Pinia 通过设计提供平面结构,同时仍然支持 Store 之间的交叉组合方式。 您甚至可以拥有 Store 的循环依赖关系。
  • 没有 命名空间模块。鉴于 Store 的扁平架构,“命名空间” Store 是其定义方式所固有的,您可以说所有 Store 都是命名空间的。

2、安装

使用您最喜欢的包管理工具安装​​Pinia​​:

yarn add pinia
# or with npm
npm install

提示
如果您的应用使用的是​​​Vue 2​​​,你还需要安装 ​​composition api​​​: ​​@vue/composite-api​​。

如果你使用的是​​Vue CLI​​​,你可以试试这个​​非官方的插件​​。

创建一个​​pinia​​​(根​​store​​)并将其传递给应用程序:

import { createPinia } from 'pinia'

app.use(createPinia())

如果您使用的是​​Vue 2​​​,您还需要安装一个插件,并将创建的​​pinia​​注入到应用程序的根目录:

import { createPinia, PiniaVuePlugin } from 'pinia'

Vue.use(PiniaVuePlugin)
const pinia = createPinia()

new Vue({
el: '#app',
// other options...
// ...
// note the same `pinia` instance can be used across multiple Vue apps on
// the same page
pinia,
})

这也将添加 devtools 支持。在 Vue 3 中,仍然不支持时间旅行和编辑等一些功能,因为 vue-devtools 尚未公开必要的 API,但 devtools 具有更多功能,并且整体开发人员体验要好得多。在 Vue 2 中,Pinia 使用 Vuex 的现有接口(因此不能与它一起使用)。

2.1 什么是Store?

​Store​​​(如​​Pinia​​​)是保存状态和业务逻辑的实体,它没有绑定到组件树。换句话说,它承载全局状态。它有点像一个总是存在的组件,每个人都可以读取和写入。它有三个核心概念,​​state​​​、​​getters​​​ 和 ​​actions​​​ 可以想当然地认为这些概念等同于组件中的​​data​​​、​​computed​​​ 和​​methods​​。

2.2 什么时候应该使用Store

​Store​​应该包含可以在整个应用程序中访问的数据。这包括在很多地方使用的数据,例如在导航栏中显示的用户信息,以及需要通过页面保存的数据,例如非常复杂的多步骤表单。

另一方面,你应该避免在​​store​​中包含可能托管在组件中的本地数据,例如,页面本地元素的可见性。

并不是所有的应用程序都需要访问全局状态,但是如果您需要,​​Pinia​​将使您的工作更轻松。