官网直通车
What is Pinia?
官网的介绍是:
Pinia最初是在 2019 年 11 月左右重新设计使用Composition API的 Vue Store 的实验。从那时起,最初的原则仍然相同,但 Pinia 适用于 Vue 2 和 Vue 3 ,并且不需要你使用组合 API。除了安装和SSR之外,两者的 API 都是相同的,并且这些文档针对 Vue 3 ,并在必要时提供有关 Vue 2 的注释,以便 Vue 2 和 Vue 3 用户可以阅读!
我的理解是:
Pinia 是 Vue.js 的轻量级状态管理库,最近很受欢迎。它使用 Vue 3 中的新反应系统来构建一个直观且完全类型化的状态管理库。
Pinia的成功可以归功于其管理存储数据的独特功能(可扩展性、存储模块组织、状态变化分组、多存储创建等)。
另一方面,Vuex也是为Vue框架建立的一个流行的状态管理库,它也是Vue核心团队推荐的状态管理库。 Vuex高度关注应用程序的可扩展性、开发人员的工效和信心。它基于与Redux相同的流量架构。
自3月份掘金夜谈No.1犹大重比提到了Pinia,我关注到了这个库。
尤大明确表示了:“不会有vuex5,或者说pinia就是vuex5。pinia是vuex的维护者在决定vuex5风格的时候做的一个库,效果比预期的好,同时为了尊重作者,pinia也不会更名为vuex5”
Pinia 是 Vue 的存储库,它允许您跨组件/页面共享状态。如果您熟悉 Composition API,您可能会认为您已经可以使用简单的export const state = reactive({})
. 这对于单页应用程序来说是正确的,但如果它是服务器端呈现的,则会将您的应用程序暴露给安全漏洞。 但即使在小型单页应用程序中,您也可以从使用 Pinia 中获得很多好处:
-
开发工具支持
- A timeline to track actions, mutations
- Stores appear in components where they are used
- Time travel and easier debugging
-
热模块更换
- 在不重新加载页面的情况下修改您的Store
- 在开发时保持任何现有状态
插件:使用插件扩展 Pinia 功能
为 JS 用户提供适当的 TypeScript 支持或自动完成功能
服务器端渲染支持
基本示例
这就是使用 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 还支持一组类似的map helpers,例如 Vuex。您以相同的方式定义存储,但随后使用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']),
},
}
您将在核心概念中找到有关每个地图助手的更多信息。
为什么选择Pinia#
Pinia(发音为/piːnjʌ/
,如英语中的“peenya”)是最接近piña(西班牙语中的菠萝)的词,它是一个有效的包名称。菠萝实际上是一组单独的花朵,它们结合在一起形成多个水果。与Store类似,每一家都是独立诞生的,但最终都是相互联系的。它也是一种美味的热带水果,原产于南美洲。
一个更现实的例子#
这是一个更完整的 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 })
},
},
})
与 Vuex 的比较#
Pinia 最初是为了探索 Vuex 的下一次迭代会是什么样子,结合了 Vuex 5 核心团队讨论中的许多想法。最终,我们意识到 Pinia 已经实现了我们在 Vuex 5 中想要的大部分内容,并决定实现它取而代之的是新的建议。
与 Vuex 相比,Pinia 提供了一个更简单的 API,具有更少的仪式,提供了 Composition-API 风格的 API,最重要的是,在与 TypeScript 一起使用时具有可靠的类型推断支持。
RFC#
最初 Pinia 没有通过任何 RFC。我根据我开发应用程序、阅读其他人的代码、为使用 Pinia 的客户工作以及在 Discord 上回答问题的经验测试了想法。这使我能够提供一种适用于各种情况和应用程序大小的解决方案。我过去经常发布并在保持其核心 API 不变的同时使库不断发展。
现在 Pinia 已经成为默认的状态管理解决方案,它和 Vue 生态系统中的其他核心库一样遵循 RFC 流程,其 API 也进入了稳定状态。
与 Vuex 3.x/4.x 的比较#
Vuex 3.x 对应的是 Vue 2 而 Vuex 4.x 是 Vue 3
Pinia API 与 Vuex ≤4 有很大不同,即:
- 突变不再存在。他们经常被认为是非常冗长的。他们最初带来了 devtools 集成,但这不再是问题。
- 无需创建自定义复杂包装器来支持 TypeScript,所有内容都是类型化的,并且 API 的设计方式尽可能利用 TS 类型推断。
- 不再需要注入魔法字符串、导入函数、调用它们,享受自动完成功能!
- 无需动态添加Store,默认情况下它们都是动态的,您甚至都不会注意到。请注意,您仍然可以随时手动使用Store进行注册,但因为它是自动的,您无需担心。
- 不再有模块的嵌套结构。您仍然可以通过在另一个Store中导入和使用Store来隐式嵌套Store,但 Pinia 通过设计提供平面结构,同时仍然支持Store之间的交叉组合方式。你甚至可以有 store 的循环依赖。
- 没有命名空间的模块。鉴于Store的扁平架构,“命名空间”Store是其定义方式所固有的,您可以说所有Store都是命名空间的。
有关如何将现有 Vuex ≤4 项目转换为使用 Pinia 的更详细说明,请参阅官网从 Vuex 迁移指南。
安装#
pinia
使用您最喜欢的包管理器安装:
yarn add pinia
# or with npm
npm install pinia
# or with pnpm
pnpm install pinia
提示
如果您的应用使用 Vue 2,您还需要安装组合 api: @vue/composition-api
。如果您使用 Nuxt,则应遵循这些说明。
如果你使用的是 Vue CLI,你可以试试这个非官方的插件。
创建一个 pinia(根存储)并将其传递给应用程序:
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 的现有接口(因此不能与它一起使用)。
什么是 Store?#
一个Store(如 Pinia)是一个实体,它持有未绑定到您的组件树的状态和业务逻辑。换句话说,它托管全局状态。它有点像一个始终存在并且每个人都可以读取和写入的组件。它包含三个概念,状态、getter和Action,并且可以安全地假设这些概念等同于data
,computed
和methods
在组件中。
我什么时候应该使用Store#
存储应该包含可以在整个应用程序中访问的数据。这包括在许多地方使用的数据,例如在导航栏中显示的用户信息,以及需要通过页面保存的数据,例如非常复杂的多步骤表单。
另一方面,您应该避免在存储中包含可能托管在组件中的本地数据,例如页面本地元素的可见性。
并非所有应用程序都需要访问全局状态,但如果您需要一个,Pania 将使您的生活更轻松。
定义Store
在深入研究核心概念之前,我们需要知道存储是使用定义的defineStore()
,并且它需要一个唯一的名称,作为第一个参数传递:
import { defineStore } from 'pinia'
// useStore could be anything like useUser, useCart
// the first argument is a unique id of the store across your application
export const useStore = defineStore('main', {
// other options...
})
这个名称,也称为id,是必要的,Pania 使用它来将Store连接到 devtools。将返回的函数命名为use... 是可组合项之间的约定,以使其使用习惯。
使用Store#
我们正在定义useStore()
一个Store,因为Store在被调用之前不会被创建setup()
:
import { useStore } from '@/stores/counter'
export default {
setup() {
const store = useStore()
return {
// you can return the whole store instance to use it in the template
store,
}
},
}
您可以根据需要定义任意数量的Store,并且应该在不同的文件中定义每个Store以充分利用 pinia(例如自动允许您的包进行代码拆分和 TypeScript 推理)。
如果您还没有使用setup
组件,您仍然可以将 Pinia 与map helpers一起使用。
实例化Store后,您可以直接在Store中访问定义在state
、getters
和中的任何属性。actions
我们将在接下来的页面中详细介绍这些内容,但自动补全会对您有所帮助。
请注意,这store
是一个用 包裹的对象reactive
,这意味着不需要.value
在 getter 之后编写,但是像props
in一样setup
,我们不能对其进行解构:
export default defineComponent({
setup() {
const store = useStore()
// ❌ This won't work because it breaks reactivity
// it's the same as destructuring from `props`
const { name, doubleCount } = store
name // "eduardo"
doubleCount // 2
return {
// will always be "eduardo"
name,
// will always be 2
doubleCount,
// this one will be reactive
doubleValue: computed(() => store.doubleCount),
}
},
})
为了从存储中提取属性同时保持其反应性,您需要使用storeToRefs()
. 它将为每个反应属性创建参考。当您仅使用Store中的状态但不调用任何操作时,这很有用。请注意,您可以直接从Store中解构操作,因为它们也绑定到Store本身:
import { storeToRefs } from 'pinia'
export default defineComponent({
setup() {
const store = useStore()
// `name` and `doubleCount` are reactive refs
// This will also create refs for properties added by plugins
// but skip any action or non reactive (non ref/reactive) property
const { name, doubleCount } = storeToRefs(store)
// the increment action can be just extracted
const { increment } = store
return {
name,
doubleCount
increment,
}
},
})
状态 State
大多数时候,州是Store的中心部分。人们通常从定义代表他们的应用程序的状态开始。在 Pinia 中,状态被定义为返回初始状态的函数。这允许 Pinia 在服务器端和客户端工作。
import { defineStore } from 'pinia'
const useStore = defineStore('storeId', {
// arrow function recommended for full type inference
state: () => {
return {
// all these properties will have their type inferred automatically
counter: 0,
name: 'Eduardo',
isAdmin: true,
}
},
})
提示
如果您使用 Vue 2,您在其中创建的数据state
遵循与在 Vue 实例中相同的规则data
,即状态对象必须是普通的,并且您需要在向其添加新属性Vue.set()
时调用。另请参阅: Vue#data。********
访问state
#
默认情况下,您可以通过store
实例访问状态来直接读取和写入状态:
const store = useStore()
store.counter++
重置状态#
您可以通过调用store 上的方法将状态重置为其初始值:$reset()
const store = useStore()
store.$reset()
使用选项 API
对于以下示例,您可以假设已创建以下Store:
// Example File Path:
// ./src/stores/counterStore.js
import { defineStore } from 'pinia',
const useCounterStore = defineStore('counterStore', {
state: () => ({
counter: 0
})
})
和setup()
#
虽然 Composition API 并不适合所有人,但setup()
钩子可以使在 Options API 中使用 Pinia 变得更容易。不需要额外的map helper功能!
import { useCounterStore } from '../stores/counterStore'
export default {
setup() {
const counterStore = useCounterStore()
return { counterStore }
},
computed: {
tripleCounter() {
return this.counterStore.counter * 3
},
},
}
没有setup()
#
如果您没有使用 Composition API,而您正在使用computed
, methods
, ...,则可以使用mapState()
帮助器将State属性映射为只读计算属性:
import { mapState } from 'pinia'
import { useCounterStore } from '../stores/counterStore'
export default {
computed: {
// gives access to this.counter inside the component
// same as reading from store.counter
...mapState(useCounterStore, ['counter'])
// same as above but registers it as this.myOwnName
...mapState(useCounterStore, {
myOwnName: 'counter',
// you can also write a function that gets access to the store
double: store => store.counter * 2,
// it can have access to `this` but it won't be typed correctly...
magicValue(store) {
return store.someGetter + this.counter + this.double
},
}),
},
}
可修改State#
如果您希望能够写入这些#### State属性(例如,如果您有一个表单),您可以mapWritableState()
改用。请注意,您不能传递类似 with 的函数mapState()
:
import { mapWritableState } from 'pinia'
import { useCounterStore } from '../stores/counterStore'
export default {
computed: {
// gives access to this.counter inside the component and allows setting it
// this.counter++
// same as reading from store.counter
...mapWritableState(useCounterStore, ['counter'])
// same as above but registers it as this.myOwnName
...mapWritableState(useCounterStore, {
myOwnName: 'counter',
}),
},
}
提示
您不需要mapWritableState()
像数组这样的集合,除非您将整个数组替换为cartItems = []
,mapState()
仍然允许您调用集合上的方法。
改变状态#
除了直接用 改变 store 之外store.counter++
,您还可以调用该$patch
方法。state
它允许您对部分对象同时应用多个更改:
store.$patch({
counter: store.counter + 1,
name: 'Abalam',
})
但是,使用这种语法应用某些突变确实很难或成本很高:任何集合修改(例如,从数组中推送、删除、拼接元素)都需要您创建一个新集合。正因为如此,该$patch
方法还接受一个函数来对这种难以用补丁对象应用的突变进行分组:
cartStore.$patch((state) => {
state.items.push({ name: 'shoes', quantity: 1 })
state.hasChanged = true
})
这里的主要区别是$patch()
允许您将多个更改分组到 devtools 中的一个条目中。请注意 ,直接更改state
并$patch()
出现在 devtools 中,并且可以穿越时间(在 Vue 3 中还没有)。
更换state
#
$state
您可以通过将Store的属性设置为新对象来替换Store的整个状态:
store.$state = { counter: 666, name: 'Paimon' }
您还可以通过更改实例state
的来替换应用程序的整个状态。pinia
这在SSR 期间用于补水。
pinia.state.value = {}
订阅状态#
你可以通过 store 的方法观察状态及其变化$subscribe()
,类似于 Vuex 的subscribe 方法。$subscribe()
与常规相比使用的优点watch()
是订阅只会在补丁后触发一次(例如,使用上面的函数版本时)。
cartStore.$subscribe((mutation, state) => {
// import { MutationType } from 'pinia'
mutation.type // 'direct' | 'patch object' | 'patch function'
// same as cartStore.$id
mutation.storeId // 'cart'
// only available with mutation.type === 'patch object'
mutation.payload // patch object passed to cartStore.$patch()
// persist the whole state to the local storage whenever it changes
localStorage.setItem('cart', JSON.stringify(state))
})
默认情况下,状态订阅绑定到添加它们的组件(如果存储在组件的内部setup()
)。意思是,当组件被卸载时,它们将被自动删除。如果要在组件卸载后保留它们,请{ detached: true }
作为第二个参数传递以从当前组件中分离 状态订阅:
export default {
setup() {
const someStore = useSomeStore()
// this subscription will be kept after the component is unmounted
someStore.$subscribe(callback, { detached: true })
// ...
},
}
提示
pinia
您可以查看实例上的整个状态:
watch(
pinia.state,
(state) => {
// persist the whole state to the local storage whenever it changes
localStorage.setItem('piniaState', JSON.stringify(state))
},
{ deep: true }
)
Getters
Getter 完全等同于 Store 状态的计算值。它们可以用 中的getters
属性定义defineStore()
。他们接收state
作为第一个参数来鼓励使用箭头函数:
export const useStore = defineStore('main', {
state: () => ({
counter: 0,
}),
getters: {
doubleCount: (state) => state.counter * 2,
},
})
大多数时候,getter 只会依赖状态,但是,他们可能需要使用其他 getter。因此,我们可以在定义常规函数时访问整个store 实例, 但需要定义返回类型的类型(在 TypeScript 中) 。这是由于 TypeScript 中的一个已知限制,不会影响使用箭头函数定义的 getter,也不会影响不使用的 getter:this
**** this
export const useStore = defineStore('main', {
state: () => ({
counter: 0,
}),
getters: {
// automatically infers the return type as a number
doubleCount(state) {
return state.counter * 2
},
// the return type **must** be explicitly set
doublePlusOne(): number {
// autocompletion and typings for the whole store ✨
return this.doubleCount + 1
},
},
})
然后你可以直接在 store 实例上访问 getter:
Double count is {{ store.doubleCount }}
访问其他 getter#
与计算属性一样,您可以组合多个 getter。通过 访问任何其他Gettersthis
。即使您不使用 TypeScript,您也可以使用JSDoc提示您的 IDE 类型:
export const useStore = defineStore('main', {
state: () => ({
counter: 0,
}),
getters: {
// type is automatically inferred because we are not using `this`
doubleCount: (state) => state.counter * 2,
// here we need to add the type ourselves (using JSDoc in JS). We can also
// use this to document the getter
/**
* Returns the counter value times two plus one.
*
* @returns {number}
*/
doubleCountPlusOne() {
// autocompletion ✨
return this.doubleCount + 1
},
},
})
将参数传递给 getter#
Getter只是在幕后计算的属性,因此不可能将任何参数传递给它们。但是,您可以从getter返回一个函数以接受任何参数:
export const useStore = defineStore('main', {
getters: {
getUserById: (state) => {
return (userId) => state.users.find((user) => user.id === userId)
},
},
})
并在组件中使用:
User 2: {{ getUserById(2) }}
请注意,执行此操作时,getter 不再缓存,它们只是您调用的函数。但是,您可以在 getter 本身内部缓存一些结果,这并不常见,但应该证明性能更高:
export const useStore = defineStore('main', {
getters: {
getActiveUserById(state) {
const activeUsers = state.users.filter((user) => user.active)
return (userId) => activeUsers.find((user) => user.id === userId)
},
},
})
访问其他Store的Getters#
要使用其他存储 getter,您可以直接在getter内部使用它:**
import { useOtherStore } from './other-store'
export const useStore = defineStore('main', {
state: () => ({
// ...
}),
getters: {
otherGetter(state) {
const otherStore = useOtherStore()
return state.localData + otherStore.data
},
},
})
用法与setup()
#
您可以直接访问任何 getter 作为 store 的属性(与 state 属性完全一样):
export default {
setup() {
const store = useStore()
store.counter = 3
store.doubleCount // 6
},
}
使用选项 API
对于以下示例,您可以假设已创建以下Store:
// Example File Path:
// ./src/stores/counterStore.js
import { defineStore } from 'pinia',
const useCounterStore = defineStore('counterStore', {
state: () => ({
counter: 0
}),
getters: {
doubleCounter() {
return this.counter * 2
}
}
})
和setup()
#
虽然 Composition API 并不适合所有人,但setup()
钩子可以使在 Options API 中使用 Pinia 变得更容易。不需要额外的map helper功能!
import { useCounterStore } from '../stores/counterStore'
export default {
setup() {
const counterStore = useCounterStore()
return { counterStore }
},
computed: {
quadrupleCounter() {
return counterStore.doubleCounter * 2
},
},
}
没有setup()
#
您可以使用上一部分状态中使用的相同mapState()
函数来映射到 getter:
import { mapState } from 'pinia'
import { useCounterStore } from '../stores/counterStore'
export default {
computed: {
// gives access to this.doubleCounter inside the component
// same as reading from store.doubleCounter
...mapState(useCounterStore, ['doubleCount'])
// same as above but registers it as this.myOwnName
...mapState(useCounterStore, {
myOwnName: 'doubleCounter',
// you can also write a function that gets access to the store
double: store => store.doubleCount,
}),
},
}
Actions
Actions相当于组件中的方法。它们可以使用actions
in 属性进行定义,defineStore()
并且非常适合定义业务逻辑:
export const useStore = defineStore('main', {
state: () => ({
counter: 0,
}),
actions: {
increment() {
this.counter++
},
randomizeCounter() {
this.counter = Math.round(100 * Math.random())
},
},
})
像getter一样,action通过完全输入(和自动完成✨)支持访问整个Store实例。与它们不同的是,它 可以是异步的,您可以在它们内部进行任何 API 调用甚至其他操作!这是一个使用Mande的示例。请注意,您使用的库并不重要,只要您获得 a ,您甚至可以使用本机函数(仅限浏览器):this
**** actions
await
Promise``fetch
import { mande } from 'mande'
const api = mande('/api/users')
export const useUsers = defineStore('users', {
state: () => ({
userData: null,
// ...
}),
actions: {
async registerUser(login, password) {
try {
this.userData = await api.post({ login, password })
showTooltip(`Welcome back ${this.userData.name}!`)
} catch (error) {
showTooltip(error)
// let the form component display the error
return error
}
},
},
})
你也可以完全自由地设置你想要的任何参数并返回任何东西。调用action时,一切都会自动推断!
action像方法一样被调用:
export default defineComponent({
setup() {
const main = useMainStore()
// call the action as a method of the store
main.randomizeCounter()
return {}
},
})
访问其他Store操作#
要使用另一个Store,您可以直接在action内部使用它:**
import { useAuthStore } from './auth-store'
export const useSettingsStore = defineStore('settings', {
state: () => ({
preferences: null,
// ...
}),
actions: {
async fetchUserPreferences() {
const auth = useAuthStore()
if (auth.isAuthenticated) {
this.preferences = await fetchPreferences()
} else {
throw new Error('User must be authenticated')
}
},
},
})
用法与setup()
#
您可以直接调用任何操作作为 store 的方法:
export default {
setup() {
const store = useStore()
store.randomizeCounter()
},
}
使用选项 API
对于以下示例,您可以假设已创建以下Store:
// Example File Path:
// ./src/stores/counterStore.js
import { defineStore } from 'pinia',
const useCounterStore = defineStore('counterStore', {
state: () => ({
counter: 0
}),
actions: {
increment() {
this.counter++
}
}
})
和setup()
#
虽然 Composition API 并不适合所有人,但setup()
钩子可以使在 Options API 中使用 Pinia 变得更容易。不需要额外的map helper功能!
import { useCounterStore } from '../stores/counterStore'
export default {
setup() {
const counterStore = useCounterStore()
return { counterStore }
},
methods: {
incrementAndPrint() {
this.counterStore.increment()
console.log('New Count:', this.counterStore.count)
},
},
}
没有setup()
#
如果您根本不想使用 Composition API,可以使用mapActions()
帮助器将操作属性映射为组件中的方法:
import { mapActions } from 'pinia'
import { useCounterStore } from '../stores/counterStore'
export default {
methods: {
// gives access to this.increment() inside the component
// same as calling from store.increment()
...mapActions(useCounterStore, ['increment'])
// same as above but registers it as this.myOwnName()
...mapActions(useCounterStore, { myOwnName: 'doubleCounter' }),
},
}
订阅操作#
可以用 观察Action及其结果store.$onAction()
。传递给它的回调在操作本身之前执行。after
处理承诺并允许您在操作解决后执行功能。以类似的方式,onError
允许您在操作抛出或拒绝时执行函数。这些对于在运行时跟踪错误很有用,类似于Vue 文档中的这个技巧。
这是一个在运行操作之前和它们解决/拒绝之后记录的示例。
const unsubscribe = someStore.$onAction(
({
name, // name of the action
store, // store instance, same as `someStore`
args, // array of parameters passed to the action
after, // hook after the action returns or resolves
onError, // hook if the action throws or rejects
}) => {
// a shared variable for this specific action call
const startTime = Date.now()
// this will trigger before an action on `store` is executed
console.log(`Start "${name}" with params [${args.join(', ')}].`)
// this will trigger if the action succeeds and after it has fully run.
// it waits for any returned promised
after((result) => {
console.log(
`Finished "${name}" after ${
Date.now() - startTime
}ms.\nResult: ${result}.`
)
})
// this will trigger if the action throws or returns a promise that rejects
onError((error) => {
console.warn(
`Failed "${name}" after ${Date.now() - startTime}ms.\nError: ${error}.`
)
})
}
)
// manually remove the listener
unsubscribe()
默认情况下,操作订阅绑定到添加它们的组件(如果Store位于组件的 内部setup()
)。意思是,当组件被卸载时,它们将被自动删除。如果要在卸载组件后保留它们,请true
作为第二个参数传递以将操作订阅与当前组件分离:**
export default {
setup() {
const someStore = useSomeStore()
// this subscription will be kept after the component is unmounted
someStore.$onAction(callback, true)
// ...
},
}
Plugins 插件
由于低级 API,Pania Store可以完全扩展。以下是您可以执行的操作列表:
- 向Store添加新属性
- 定义Store时添加新选项
- 向Store添加新方法
- 包装现有方法
- 更改甚至取消操作
- 实现像本地存储这样的副作用
- 仅适用于特定Store
插件被添加到 pinia 实例中pinia.use()
。最简单的例子是通过返回一个对象为所有Store添加一个静态属性:
import { createPinia } from 'pinia'
// add a property named `secret` to every store that is created after this plugin is installed
// this could be in a different file
function SecretPiniaPlugin() {
return { secret: 'the cake is a lie' }
}
const pinia = createPinia()
// give the plugin to pinia
pinia.use(SecretPiniaPlugin)
// in another file
const store = useStore()
store.secret // 'the cake is a lie'
这对于添加全局对象(如路由器、模式或 toast 管理器)很有用。
介绍#
Pinia 插件是一个函数,可以选择返回要添加到Store的属性。它需要一个可选参数,一个context:
export function myPiniaPlugin(context) {
context.pinia // the pinia created with `createPinia()`
context.app // the current app created with `createApp()` (Vue 3 only)
context.store // the store the plugin is augmenting
context.options // the options object defining the store passed to `defineStore()`
// ...
}
然后将此函数传递给pinia
with pinia.use()
:
pinia.use(myPiniaPlugin)
插件仅适用于传递给应用 后创建pinia
的Store,否则将不会被应用。
扩充Store#
您可以通过简单地在插件中返回它们的对象来为每个Store添加属性:
pinia.use(() => ({ hello: 'world' }))
您也可以直接在 上设置属性,store
但如果可能,请使用返回版本,以便它们可以被 devtools 自动跟踪:
pinia.use(({ store }) => {
store.hello = 'world'
})
插件返回的任何属性都将由 devtools 自动跟踪,因此为了hello
在 devtools 中可见,请确保仅当您想在 devtools 中调试它时才将其添加到store._customProperties
dev 模式:
// from the example above
pinia.use(({ store }) => {
store.hello = 'world'
// make sure your bundler handle this. webpack and vite should do it by default
if (process.env.NODE_ENV === 'development') {
// add any keys you set on the store
store._customProperties.add('hello')
}
})
请注意,每个Store都用 包装reactive
,自动解包装它包含的任何 Ref ( ref()
, computed()
, ...):
const sharedRef = ref('shared')
pinia.use(({ store }) => {
// each store has its individual `hello` property
store.hello = ref('secret')
// it gets automatically unwrapped
store.hello // 'secret'
// all stores are sharing the value `shared` property
store.shared = sharedRef
store.shared // 'shared'
})
这就是为什么您可以在没有的情况下访问所有计算属性.value
以及它们是反应性的原因。
添加新状态#
如果您想将新的状态属性添加到Store或打算在水合期间使用的属性,您必须在两个地方添加它:
- 在
store
所以你可以访问它store.myState
-
store.$state
因此它可以在 devtools 中使用,并在SSR 期间被序列化。
请注意,这允许您共享ref
orcomputed
属性:
const globalSecret = ref('secret')
pinia.use(({ store }) => {
// `secret` is shared among all stores
store.$state.secret = globalSecret
store.secret = globalSecret
// it gets automatically unwrapped
store.secret // 'secret'
const hasError = ref(false)
store.$state.hasError = hasError
// this one must always be set
store.hasError = toRef(store.$state, 'hasError')
// in this case it's better not to return `hasError` since it
// will be displayed in the `state` section in the devtools
// anyway and if we return it, devtools will display it twice.
})
请注意,插件中发生的状态更改或添加(包括调用store.$patch()
)发生在Store处于活动状态之前,因此不会触发任何订阅。
警告
如果您使用的是Vue 2,Pinia 会受到与Vue相同的反应性警告。创建新的状态属性时需要使用set
from ,例如and :@vue/composition-api``secret``hasError
import { set } from '@vue/composition-api'
pinia.use(({ store }) => {
if (!store.$state.hasOwnProperty('hello')) {
const secretRef = ref('secret')
// If the data is meant to be used during SSR, you should
// set it on the `$state` property so it is serialized and
// picked up during hydration
set(store.$state, 'secret', secretRef)
// set it directly on the store too so you can access it
// both ways: `store.$state.secret` / `store.secret`
set(store, 'secret', secretRef)
store.secret // 'secret'
}
})
添加新的外部属性#
当添加外部属性、来自其他库的类实例或仅仅是非反应性的东西时,您应该在将对象markRaw()
传递给 pinia 之前将其包装起来。这是一个将路由器添加到每个Store的示例:
import { markRaw } from 'vue'
// adapt this based on where your router is
import { router } from './router'
pinia.use(({ store }) => {
store.router = markRaw(router)
})
$subscribe
在插件内部调用#
您也可以在插件中使用store.onAction :
pinia.use(({ store }) => {
store.$subscribe(() => {
// react to store changes
})
store.$onAction(() => {
// react to store actions
})
})
添加新选项#
可以在定义Store时创建新选项,以便以后从插件中使用它们。例如,您可以创建一个debounce
允许您对任何操作进行去抖动的选项:
defineStore('search', {
actions: {
searchContacts() {
// ...
},
},
// this will be read by a plugin later on
debounce: {
// debounce the action searchContacts by 300ms
searchContacts: 300,
},
})
然后插件可以读取该选项以包装操作并替换原始操作:
// use any debounce library
import debounce from 'lodash/debunce'
pinia.use(({ options, store }) => {
if (options.debounce) {
// we are overriding the actions with new ones
return Object.keys(options.debounce).reduce((debouncedActions, action) => {
debouncedActions[action] = debounce(
store[action],
options.debounce[action]
)
return debouncedActions
}, {})
}
})
请注意,使用设置语法时,自定义选项作为第三个参数传递:
defineStore(
'search',
() => {
// ...
},
{
// this will be read by a plugin later on
debounce: {
// debounce the action searchContacts by 300ms
searchContacts: 300,
},
}
)
TypeScript#
上面显示的所有内容都可以通过键入支持来完成,因此您无需使用any
或@ts-ignore
.
Typing plugins 插件#
Pinia 插件可以按如下方式键入:
import { PiniaPluginContext } from 'pinia'
export function myPiniaPlugin(context: PiniaPluginContext) {
// ...
}
键入新的Store属性#
向Store添加新属性时,您还应该扩展PiniaCustomProperties
接口。
import 'pinia'
declare module 'pinia' {
export interface PiniaCustomProperties {
// by using a setter we can allow both strings and refs
set hello(value: string | Ref)
get hello(): string
// you can define simpler values too
simpleNumber: number
}
}
然后可以安全地写入和读取它:
pinia.use(({ store }) => {
store.hello = 'Hola'
store.hello = ref('Hola')
store.simpleNumber = Math.random()
// @ts-expect-error: we haven't typed this correctly
store.simpleNumber = ref(Math.random())
})
PiniaCustomProperties
是一种通用类型,允许您引用Store的属性。想象以下示例,我们将初始选项复制为$options
(这仅适用于选项存储):
pinia.use(({ options }) => ({ $options: options }))
我们可以通过使用 4 种通用类型来正确输入PiniaCustomProperties
:
import 'pinia'
declare module 'pinia' {
export interface PiniaCustomProperties {
$options: {
id: Id
state?: () => S
getters?: G
actions?: A
}
}
}
提示
在泛型中扩展类型时,它们的命名必须与源代码中的完全相同。Id
不能命名id
或I
,S
也不能命名State
。以下是每个字母所代表的含义:
- S: State
- G: Getters
- A: Actions
- SS: Setup Store / Store
输入新状态#
当添加新的状态属性(store
和store.$state
)时,您需要添加类型来PiniaCustomStateProperties
代替。与 不同的是PiniaCustomProperties
,它只接收State
泛型:
import 'pinia'
declare module 'pinia' {
export interface PiniaCustomStateProperties {
hello: string
}
}
键入新的创建选项#
在为 创建新选项时defineStore()
,您应该扩展DefineStoreOptionsBase
. 与 不同的是PiniaCustomProperties
,它只公开了两个泛型:State 和 Store 类型,允许您限制可以定义的内容。例如,您可以使用操作的名称:
import 'pinia'
declare module 'pinia' {
export interface DefineStoreOptionsBase {
// allow defining a number of ms for any of the actions
debounce?: Partial, number>>
}
}
提示
还有一种类型可以从 Store 类型StoreGetters
中提取getter 。 您也可以仅通过分别扩展类型和来扩展设置Store或选项Store 的选项。****DefineStoreOptions``DefineSetupStoreOptions
Nuxt.js#
在Nuxt 旁边使用 pinia 时,您必须先创建一个Nuxt 插件。这将使您可以访问该pinia
实例:
// plugins/myPiniaPlugin.js
import { PiniaPluginContext } from 'pinia'
import { Plugin } from '@nuxt/types'
function MyPiniaPlugin({ store }: PiniaPluginContext) {
store.$subscribe((mutation) => {
// react to store changes
console.log(`[ ${mutation.storeId}]: ${mutation.type}.`)
})
// Note this has to be typed if you are using TS
return { creationTime: new Date() }
}
const myPlugin: Plugin = ({ $pinia }) => {
$pinia.use(MyPiniaPlugin)
}
export default myPlugin
请注意,上面的示例使用的是 TypeScript,如果您使用的是文件,则必须删除类型注释PiniaPluginContext
及其导入。Plugin``.js
在组件之外使用存储
Pinia Store依靠pinia
实例在所有调用中共享同一个Store实例。useStore()
大多数情况下,只需调用您的函数即可开箱即用。例如,在 中setup()
,您无需执行任何其他操作。但在组件之外,情况有些不同。在幕后,useStore()
将pinia
您提供给app
. 这意味着如果pinia
无法自动注入实例,则必须手动将其提供给useStore()
函数。您可以根据您正在编写的应用程序的类型以不同的方式解决这个问题。
单页应用程序#
如果你没有做任何 SSR(服务器端渲染),useStore()
安装 pinia 插件后的任何调用都app.use(pinia)
将起作用:
import { useUserStore } from '@/stores/user'
import { createApp } from 'vue'
import App from './App.vue'
// ❌ fails because it's called before the pinia is created
const userStore = useUserStore()
const pinia = createPinia()
const app = createApp(App)
app.use(pinia)
// ✅ works because the pinia instance is now active
const userStore = useUserStore()
确保始终应用此功能的最简单方法是通过将调用放在将始终在 pinia 安装后运行的函数中来推迟调用。useStore()
让我们看一下这个使用 Vue Router 的导航守卫内部的Store的例子:
import { createRouter } from 'vue-router'
const router = createRouter({
// ...
})
// ❌ Depending on the order of imports this will fail
const store = useStore()
router.beforeEach((to, from, next) => {
// we wanted to use the store here
if (store.isLoggedIn) next()
else next('/login')
})
router.beforeEach((to) => {
// ✅ This will work because the router starts its navigation after
// the router is installed and pinia will be installed too
const store = useStore()
if (to.meta.requiresAuth && !store.isLoggedIn) return '/login'
})
SSR 应用#
在处理服务器端渲染时,您必须将pinia
实例传递给useStore()
. 这可以防止 pinia 在不同的应用程序实例之间共享全局状态。
在SSR 指南中有一个专门的部分,这只是一个简短的解释:
服务器端渲染 (SSR)
提示
如果您使用的是Nuxt.js, 则需要阅读这些说明。
只要您在useStore()
函数顶部调用函数,使用 Pinia 创建Store就可以立即使用 SSR setup
,getters
并且actions
:
export default defineComponent({
setup() {
// this works because pinia knows what application is running inside of
// `setup()`
const main = useMainStore()
return { main }
},
})
在外面使用Storesetup()
#
如果您需要在其他地方使用Store,则需要将传递给应用程序pinia
的实例传递给函数调用:useStore()
const pinia = createPinia()
const app = createApp(App)
app.use(router)
app.use(pinia)
router.beforeEach((to) => {
// ✅ This will work make sure the correct store is used for the
// current running app
const main = useMainStore(pinia)
if (to.meta.requiresAuth && !main.isLoggedIn) return '/login'
})
Pinia 方便地将自身添加$pinia
到您的应用程序中,因此您可以在以下功能中使用它serverPrefetch()
:
export default {
serverPrefetch() {
const store = useStore(this.$pinia)
},
}
State hydration#
为了水合初始状态,您需要确保 rootState 包含在 HTML 中的某个位置,以便 Pinia 稍后获取它。根据您用于 SSR的内容,出于安全原因,您应该转义该状态。我们建议使用Nuxt.js 使用的@nuxt/ devalue:
import devalue from '@nuxt/devalue'
import { createPinia } from 'pinia'
// retrieve the rootState server side
const pinia = createPinia()
const app = createApp(App)
app.use(router)
app.use(pinia)
// after rendering the page, the root state is build and can be read directly
// on `pinia.state.value`.
// serialize, escape (VERY important if the content of the state can be changed
// by the user, which is almost always the case), and place it somewhere on
// the page, for example, as a global variable.
devalue(pinia.state.value)
根据您用于 SSR 的内容,您将设置将在 HTML 中序列化的初始状态变量。您还应该保护自己免受 XSS 攻击。例如,使用vite-ssr您可以使用transformState
选项和@nuxt/devalue
:
import devalue from '@nuxt/devalue'
export default viteSSR(
App,
{
routes,
transformState(state) {
return import.meta.env.SSR ? devalue(state) : state
},
},
({ initialState }) => {
// ...
if (import.meta.env.SSR) {
// this will be stringified and set to window.__INITIAL_STATE__
initialState.pinia = pinia.state.value
} else {
// on the client side, we restore the state
pinia.state.value = initialState.pinia
}
}
)
您可以根据需要使用其他替代方案@nuxt/devalue
,例如,如果您可以使用JSON.stringify()
/序列化和解析您的状态JSON.parse()
,则可以大大提高您的性能。
使此策略适应您的环境。useStore()
在调用客户端的任何函数之前,请确保水合 pinia 的状态。例如,如果我们将状态序列化为一个标签,使其可以在客户端通过 全局访问
window.__pinia
,我们可以这样写:
const pinia = createPinia()
const app = createApp(App)
app.use(pinia)
// must be set by the user
if (isClient) {
pinia.state.value = JSON.parse(window.__pinia)
}
Nuxt.js#
将 Pinia 与Nuxt.js一起使用更容易,因为 Nuxt 在服务器端渲染方面处理了很多事情。例如,您不需要关心序列化或 XSS 攻击。
安装#
确保安装在旁边:@nuxtjs/composition-api
pinia
yarn add pinia @pinia/nuxt @nuxtjs/composition-api
# or with npm
npm install pinia @pinia/nuxt @nuxtjs/composition-api
# or with pnpm
pnpm install pinia @pinia/nuxt @nuxtjs/composition-api
我们提供了一个模块来为您处理所有事情,您只需将其添加到buildModules
您的nuxt.config.js
文件中:
// nuxt.config.js
export default {
// ... other options
buildModules: [
// Nuxt 2 only:
// https://composition-api.nuxtjs.org/getting-started/setup#quick-start
'@nuxtjs/composition-api/module',
'@pinia/nuxt',
],
}
就是这样,像往常一样使用您的Store!
在外面使用Storesetup()
#
如果你想在外面使用一个 store setup()
,记得把pinia
对象传给useStore()
. 我们将它添加到上下文中asyncData()
,因此您可以在and中访问它fetch()
:
import { useStore } from '~/stores/myStore'
export default {
asyncData({ $pinia }) {
const store = useStore($pinia)
},
}
在Store中使用 Nuxt 上下文#
您还可以通过使用注入属性在任何Store中使用上下文$nuxt
:
import { useUserStore } from '~/stores/userStore'
defineStore('cart', {
actions: {
purchase() {
const user = useUserStore()
if (!user.isAuthenticated()) {
this.$nuxt.redirect('/login')
}
},
},
})
将 Pinia 与 Vuex 一起使用#
建议避免同时使用 Pinia 和 Vuex,但如果您需要同时使用两者,则需要告诉 pinia 不要禁用它:
// nuxt.config.js
export default {
buildModules: [
'@nuxtjs/composition-api/module',
['@pinia/nuxt', { disableVuex: false }],
],
// ... other options
}
TypeScript#
如果您使用的是 TypeScript 或有jsconfig.json
,您还应该添加以下类型context.pinia
:
{
"types": [
// ...
"@pinia/nuxt"
]
}
这也将确保您具有自动完成功能。
不使用setup()
#
即使您不使用组合 API,也可以使用 Pinia(如果您使用的是 Vue 2,您仍然需要安装@vue/composition-api
插件)。虽然我们建议您尝试使用 Composition API 并学习它,但您和您的团队可能还不是时候,您可能正在迁移应用程序或任何其他原因。有几个功能:
- mapStores
- mapState
- mapWritableState
- ⚠️ mapGetters (just for migration convenience, use
mapState()
instead) - mapActions
授予对整个Store的访问权限#
如果您需要访问Store中的几乎所有内容,则映射Store的每个属性可能太多了......相反,您可以通过以下方式访问整个StoremapStores()
:
import { mapStores } from 'pinia'
// given two stores with the following ids
const useUserStore = defineStore('user', {
// ...
})
const useCartStore = defineStore('cart', {
// ...
})
export default {
computed: {
// note we are not passing an array, just one store after the other
// each store will be accessible as its id + 'Store'
...mapStores(useCartStore, useUserStore)
},
methods: {
async buyStuff() {
// use them anywhere!
if (this.userStore.isAuthenticated()) {
await this.cartStore.buy()
this.$router.push('/purchased')
}
},
},
}
默认情况下,Pania 会为每个Store添加"Store"
后缀。id
您可以通过调用来自定义此行为setMapStoreSuffix()
:
import { createPinia, setMapStoreSuffix } from 'pinia'
// completely remove the suffix: this.user, this.cart
setMapStoreSuffix('')
// this.user_store, this.cart_store (it's okay, I won't judge you)
setMapStoreSuffix('_store')
export const pinia = createPinia()
TypeScript#
默认情况下,所有地图助手都支持自动完成,你不需要做任何事情。如果您调用setMapStoreSuffix()
更改"Store"
后缀,您还需要将其添加到 TS 文件或您的文件中的某个位置global.d.ts
。最方便的地方是您打电话的地方setMapStoreSuffix()
:
import { createPinia, setMapStoreSuffix } from 'pinia'
setMapStoreSuffix('') // completely remove the suffix
export const pinia = createPinia()
declare module 'pinia' {
export interface MapStoresCustomization {
// set it to the same value as above
suffix: ''
}
}
警告
如果您使用的是 TypeScript 声明文件(如global.d.ts
),请确保import 'pinia'
在其顶部公开所有现有类型。