简介
Pinia 最初是在 2019 年 11 月左右作为一个使用 Composition API 的实验进行设计的。从那时起,同时支持 Vue2 和 Vue3 以及不强制要求使用 composition API 的初始原则保留至今。除了安装和SSR之外,支持 Vue3 和 Vue2 的 API 都是相同的。虽然这些文档主要是面向 Vue3,但在必要时会标注出 Vue2 的内容,因此 Vue2 和 Vue3 的用户都可以阅读本文档。
为什么要使用 Pinia?
Pinia 是专属 Vue 的 store 库,它允许你跨组件/页面共享状态。如果你熟悉 Composition API 的话,你可能会认为你可以通过一行简单的 export const state = reactive({})
来共享一个全局状态。对于单页应用程序来说确实是这样的,但如果在服务器端渲染,这可能会使您的应用程序暴露出一些安全漏洞。 不过若是使用 Pinia,即使在小型单页应用程序中,你也可以获得很多好处:
- Devtools 支持
- 追踪 action、mutation 的时间线
- Store 可出现于使用它们的组件中
- Time travel 以及更容易的调试
- 热更新
- 不必重载页面即可修改 Store
- 开发时可保持现有状态
- 插件:可通过插件扩展 Pinia 功能
- 为JS 用户提供恰当的 TypeScript 支持或自动补全功能。
- 支持服务端渲染
基本示例
下面就是以 API 使用 pinia 的基本用法(为继续阅读本简介请确保你已阅读过了开始章节)。你可以先创建一个 Store:
// stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => {
return { count: 0 }
},
// 也可以这样定义
// state: () => ({ count: 0 })
actions: {
increment() {
this.count++
},
},
})
然后你就可以在一个组件中使用该 store 了:
import { useCounterStore } from '@/stores/counter'
export default {
setup() {
const counter = useCounterStore()
counter.count++
// 使用自动补全 ✨
counter.$patch({ count: counter.count + 1 })
// 或者使用 action 代替
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 的map helpers。你可以同样的方式定义 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']),
},
}
你将会在核心概念部分了解到更多关于 map helper 的信息。
为什么命名为 Pinia?
Pinia (发音为 /piːnjʌ/
,类似英文中的 “peenya”) 是最接近有效包名 piña(西班牙语中的 pineapple)的词。 菠萝实际上是一组各自独立的花朵,它们结合在一起,由此形成一个多重的水果。 与 Store 类似,每一个都是独立诞生的,但最终它们都是相互联系的。 它也是一种原产于南美洲的美味热带水果。
更真实的示例
这是一个更完整的你将会使用的 Pinia API 示例,即使在 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',
// 类型将自动推断为 number
nextId: 0,
}),
getters: {
finishedTodos(state) {
// 自动补全! ✨
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: {
// 接受任何数量的参数,返回一个 Promise 或不返回
addTodo(text) {
// 你可以直接 mutate 该状态
this.todos.push({ text, id: this.nextId++, isFinished: false })
},
},
})
与 Vuex 对比
Pinia 最初是为了探索 Vuex 的下一次迭代会是什么样子,结合了 Vuex 5 核心团队讨论中的许多想法。最终,我们意识到 Pinia 已经实现了我们在 Vuex 5 中想要的大部分功能,并决定将其作为新的推荐来代替 Vuex。
与 Vuex 相比,Pinia 不仅提供了一个更简单的 API,也提供了Composition-API 风格的 API,最重要的是,与 TypeScript 一起使用时有坚实的类型推断支持。
RFCs
最初,Pinia 没有经过任何 RFC。我根据自己开发应用程序的经验,同时阅读其他人的代码,为使用 Pinia 的客户工作,以及在 Discord 上回答问题,测试了一些想法。 这些使我能够产出了这样一个可行的解决方案,并适应各种情况和应用规模。我曾经常发表文章,并在保持其核心 API 不变的情况下不断优化本库。
现在 Pinea 已经成为默认的状态管理解决方案,它和 Vue 生态系统中的其他核心库一样,都要经过 RFC 流程,其 API 已经进入稳定状态。
与 Vuex 3.x/4.x 对比
Vuex 3.x 是适配 Vue2 的 Vuex,而 Vuex 4.x 是适配 Vue3 的。
Pinia API 与 Vuex ≤4 有很大不同,即:
- mutation 不再存在。它们经常被认为是极其冗长的。它们最初带来了 devtools 的集成,但这已不再是一个问题了。
- 无需要创建自定义的复杂包装器来支持 TypeScript,一切都被类型化了,API 的设计方式是尽可能地利用 TS 类型推理。
- 无过多的魔法字符串注入,导入函数。只需要调用它们,享受自动补全的乐趣就好。
- 无需要动态添加 Store,它们默认都是动态的,甚至你都不会注意到这点。注意,你仍然可以在任何时候手动使用一个 Store 来注册它,但因为它是自动的,所以你不需要担心它。
- 不再有嵌套结构的模块。你仍然可以通过导入和使用另一个 Store 来隐含地嵌套存储空间,但是 Pinia 在设计上提供了一个扁平的结构,同时仍然能够在 Store 之间进行交叉组合。你甚至可以有 Store 的循环依赖关系。
- 没有命名的模块。考虑到 Store 的扁平架构,“命名” Store 是与生俱来的,你可以说所有 Store 都是命名的。
关于如何将现有 Vuex ≤4 项目转化为使用 Pinia 的更多详细说明,请参阅从 Vuex 迁移指南。