安装

用你喜欢的包管理器安装 pinia

1
2
3
yarn add pinia
# 或者使用 npm
npm install pinia

如果你的应用使用的 Vue 版本低于 2.7,你还需要安装组合式 API 包:@vue/composition-api

创建一个 pinia 实例

创建一个 pinia 实例 (根 store) 并将其传递给应用:

1
2
3
4
5
6
7
8
9
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const pinia = createPinia()
const app = createApp(App)

app.use(pinia)
app.mount('#app')

如果你使用的是 Vue 2,你还需要安装一个插件,并在应用的根部注入创建的 pinia

1
2
3
4
5
6
7
8
9
10
11
12
13
import { createPinia, PiniaVuePlugin } from 'pinia'

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

new Vue({
el: '#app',
// 其他配置...
// ...
// 请注意,同一个`pinia'实例
// 可以在同一个页面的多个 Vue 应用中使用。
pinia,
})

这样才能提供 devtools 的支持。在 Vue 3 中,一些功能仍然不被支持,如 time traveling 和编辑,这是因为 vue-devtools 还没有相关的 API,但 devtools 也有很多针对 Vue 3 的专属功能,而且就开发者的体验来说,Vue 3 整体上要好得多。在 Vue 2 中,Pinia 使用的是 Vuex 的现有接口 (因此不能与 Vuex 一起使用) 。

应该在什么时候使用 Store?

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

另一方面,你应该避免在 Store 中引入那些原本可以在组件中保存的本地数据,例如,一个元素在页面中的可见性。

并非所有的应用都需要访问全局状态,但如果你的应用确实需要一个全局状态,那 Pinia 将使你的开发过程更轻松。

什么时候不应该使用 Store?

有的时候我们会过度使用 store。如果觉得应用程序的 store 过多,你可能需要重新考虑使用 store 的目的。例如其中一些逻辑应该只是组合式函数,或者应该只是组件的本地状态。


定义 Store

Store 是用 defineStore() 定义的,它的第一个参数要求是一个独一无二的名字:

1
2
3
4
5
6
7
8
9
import { defineStore } from 'pinia'

// `defineStore()` 的返回值的命名是自由的
// 但最好含有 store 的名字,且以 `use` 开头,以 `Store` 结尾。
// (比如 `useUserStore`,`useCartStore`,`useProductStore`)
// 第一个参数是你的应用中 Store 的唯一 ID。
export const useAlertsStore = defineStore('alerts', {
// 其他配置...
})

这个名字 ,也被用作 id ,是必须传入的, Pinia 将用它来连接 store 和 devtools。为了养成习惯性的用法,将返回的函数命名为 use… 是一个符合组合式函数风格的约定。

defineStore() 的第二个参数可接受两类值:Setup 函数Option 对象

Option Store

与 Vue 的选项式 API 类似,我们也可以传入一个带有 stateactionsgetters 属性的 Option 对象

1
2
3
4
5
6
7
8
9
10
11
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0, name: 'Eduardo' }),
getters: {
doubleCount: (state) => state.count * 2,
},
actions: {
increment() {
this.count++
},
},
})

你可以认为 state 是 store 的数据 (data),getters 是 store 的计算属性 (computed),而 actions 则是方法 (methods)。

为方便上手使用,Option Store 应尽可能直观简单。

Setup Store

也存在另一种定义 store 的可用语法。与 Vue 组合式 API 的 setup 函数 相似,我们可以传入一个函数,该函数定义了一些响应式属性和方法,并且返回一个带有我们想暴露出去的属性和方法的对象。

1
2
3
4
5
6
7
8
9
10
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const name = ref('Eduardo')
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}

return { count, name, doubleCount, increment }
})

Setup Store 中:

  • ref() 就是 state 属性
  • computed() 就是 getters 属性
  • function() 就是 actions 属性

注意,要让 pinia 正确识别 state,你必须在 setup store 中返回 state 的所有属性。这意味着,你不能在 store 中使用私有属性。不完整返回会影响 SSR ,开发工具和其他插件的正常运行。

Setup storeOption Store 带来了更多的灵活性,因为你可以在一个 store 内创建侦听器,并自由地使用任何组合式函数。不过,请记住,使用组合式函数会让 SSR 变得更加复杂。

Setup store 也可以依赖于全局提供的属性,比如路由。任何应用层面提供的属性都可以在 store 中使用 inject() 访问,就像在组件中一样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { inject } from 'vue'
import { useRoute } from 'vue-router'
import { defineStore } from 'pinia'

export const useSearchFilters = defineStore('search-filters', () => {
const route = useRoute()
// 这里假定 `app.provide('appProvided', 'value')` 已经调用过
const appProvided = inject('appProvided')

// ...

return {
// ...
}
})

不要返回像 routeappProvided (上例中)之类的属性,因为它们不属于 store,而且你可以在组件中直接用 useRoute()inject('appProvided') 访问。

你应该选用哪种语法?

和在 Vue 中如何选择组合式 API 与选项式 API 一样,选择你觉得最舒服的那一个就好。两种语法都有各自的优势和劣势。Option Store 更容易使用,而 Setup Store 更灵活和强大。

使用 Store

虽然我们前面定义了一个 store,但在我们使用 <script setup> 调用 useStore()(或者使用 setup() 函数,像所有的组件那样) 之前,store 实例是不会被创建的:

1
2
3
import { useCounterStore } from '@/stores/counter'
// 在组件内部的任何地方均可以访问变量 `store` ✨
const store = useCounterStore()

请注意,store 是一个用 reactive 包装的对象,这意味着不需要在 getters 后面写 .value。就像 setup 中的 props 一样,我们不能对它进行解构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script setup>
import { useCounterStore } from '@/stores/counter'
import { computed } from 'vue'

const store = useCounterStore()
// ❌ 下面这部分代码不会生效,因为它的响应式被破坏了
// 与 reactive 相同: https://vuejs.org/guide/essentials/reactivity-fundamentals.html#limitations-of-reactive
const { name, doubleCount } = store
name // 将会一直是 "Eduardo" //
doubleCount // 将会一直是 0 //
setTimeout(() => {
store.increment()
}, 1000)
// ✅ 而这一部分代码就会维持响应式
// 💡 在这里你也可以直接使用 `store.doubleCount`
const doubleValue = computed(() => store.doubleCount)
</script>

从 Store 解构 - storeToRefs()

为了从 store 中提取属性时保持其响应性,你需要使用 storeToRefs()。它将为每一个响应式属性创建引用。当你只使用 store 的状态而不调用任何 action 时,它会非常有用。请注意,你可以直接从 store 中解构 action,因为它们也被绑定到 store 上:

1
2
3
4
5
6
7
8
9
10
<script setup>
import { storeToRefs } from 'pinia'
const store = useCounterStore()
// `name` 和 `doubleCount` 都是响应式引用
// 下面的代码同样会提取那些来自插件的属性的响应式引用
// 但是会跳过所有的 action 或者非响应式(非 ref 或者 非 reactive)的属性
const { name, doubleCount } = storeToRefs(store)
// 名为 increment 的 action 可以被解构
const { increment } = store
</script>

state

在 Pinia 中,state 被定义为一个返回初始状态的函数。这使得 Pinia 可以同时支持服务端客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { defineStore } from 'pinia'

const useStore = defineStore('storeId', {
// 为了完整类型推理,推荐使用箭头函数
state: () => {
return {
// 所有这些属性都将自动推断出它们的类型
count: 0,
name: 'Eduardo',
isAdmin: true,
items: [],
hasChanged: true,
}
},
})

TypeScript

你并不需要做太多努力就能使你的 state 兼容 TS。确保启用了 strict,或者至少启用了 noImplicitThis,Pinia 将自动推断您的状态类型! 但是,在某些情况下,您应该帮助它进行一些转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const useStore = defineStore('storeId', {
state: () => {
return {
// 用于初始化空列表
userList: [] as UserInfo[],
// 用于尚未加载的数据
user: null as UserInfo | null,
}
},
})

interface UserInfo {
name: string
age: number
}

如果你愿意,你可以用一个接口定义 state,并添加 state() 的返回值的类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface State {
userList: UserInfo[]
user: UserInfo | null
}

const useStore = defineStore('storeId', {
state: (): State => {
return {
userList: [],
user: null,
}
},
})

interface UserInfo {
name: string
age: number
}

访问 state

默认情况下,你可以通过 store 实例访问 state,直接对其进行读写。

1
2
3
const store = useStore()

store.count++

注意,新的属性如果没有在 state() 中被定义,则不能被添加。它必须包含初始状态。

重置state

  • 使用 选项式 API 时,你可以通过调用 store 的 $reset() 方法将 state 重置为初始值。
1
2
3
const store = useStore()

store.$reset()

$reset() 内部,会调用 state() 函数来创建一个新的状态对象,并用它替换当前状态。


  • Setup Stores 中,您需要创建自己的 $reset() 方法:
1
2
3
4
5
6
7
8
9
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)

function $reset() {
count.value = 0
}

return { count, $reset }
})

使用选项式 API 的用法

在下面的例子中,你可以假设相关 store 已经创建了:

1
2
3
4
5
6
7
8
9
10
// 示例文件路径:
// ./src/stores/counter.js

import { defineStore } from 'pinia'

const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
}),
})

如果你不能使用组合式 API,但你可以使用 computed,methods,…,那你可以使用 mapState() 辅助函数将 state 属性映射为只读的计算属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { mapState } from 'pinia'
import { useCounterStore } from '../stores/counter'

export default {
computed: {
// 可以访问组件中的 this.count
// 与从 store.count 中读取的数据相同
...mapState(useCounterStore, ['count'])
// 与上述相同,但将其注册为 this.myOwnName
...mapState(useCounterStore, {
myOwnName: 'count',
// 你也可以写一个函数来获得对 store 的访问权
double: store => store.count * 2,
// 它可以访问 `this`,但它没有标注类型...
magicValue(store) {
return store.someGetter + this.count + this.double
},
}),
},
}

可修改的 state

如果你想修改这些 state 属性 (例如,如果你有一个表单),你可以使用 mapWritableState() 作为代替。但注意你不能像 mapState() 那样传递一个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { mapWritableState } from 'pinia'
import { useCounterStore } from '../stores/counter'

export default {
computed: {
// 可以访问组件中的 this.count,并允许设置它。
// this.count++
// 与从 store.count 中读取的数据相同
...mapWritableState(useCounterStore, ['count'])
// 与上述相同,但将其注册为 this.myOwnName
...mapWritableState(useCounterStore, {
myOwnName: 'count',
}),
},
}

变更 state - $patch

除了用 store.count++ 直接改变 store,你还可以调用 $patch 方法。它允许你用一个 state 的补丁对象在同一时间更改多个属性:

1
2
3
4
5
store.$patch({
count: store.count + 1,
age: 120,
name: 'DIO',
})

不过,用这种语法的话,有些变更真的很难实现或者很耗时:任何集合的修改(例如,向数组中添加、移除一个元素或是做 splice 操作)都需要你创建一个新的集合。因此,$patch 方法也接受一个函数来组合这种难以用补丁对象实现的变更。

1
2
3
4
store.$patch((state) => {
state.items.push({ name: 'shoes', quantity: 1 })
state.hasChanged = true
})

两种变更 store 方法的主要区别是,$patch() 允许你将多个变更归入 devtools 的同一个条目中。同时请注意,直接修改 state,$patch() 也会出现在 devtools 中,而且可以进行 time travel (在 Vue 3 中还没有)。

替换 state

你不能完全替换掉 store 的 state,因为那样会破坏其响应性。但是,你可以 patch 它。

1
2
3
4
// 这实际上并没有替换`$state`
store.$state = { count: 24 }
// 在它内部调用 `$patch()`:
store.$patch({ count: 24 })

订阅 state - $subscribe()

类似于 Vuex 的 subscribe 方法,你可以通过 store 的 $subscribe() 方法侦听 state 及其变化。比起普通的 watch(),使用 $subscribe() 的好处是 subscriptionspatch 后只触发一次 (例如,当使用上面的函数版本时)。

1
2
3
4
5
6
7
8
9
10
11
cartStore.$subscribe((mutation, state) => {
// import { MutationType } from 'pinia'
mutation.type // 'direct' | 'patch object' | 'patch function'
// 和 cartStore.$id 一样
mutation.storeId // 'cart'
// 只有 mutation.type === 'patch object'的情况下才可用
mutation.payload // 传递给 cartStore.$patch() 的补丁对象。

// 每当状态发生变化时,将整个 state 持久化到本地存储。
localStorage.setItem('cart', JSON.stringify(state))
})

刷新时机

在底层实现上,$subscribe() 使用了 Vue 的 watch() 函数。你可以传入与 watch() 相同的选项。当你想要在 每次 state 变化后立即触发订阅时很有用:

1
2
3
4
cartStore.$subscribe((mutation, state) => {
// 每当状态发生变化时,将整个 state 持久化到本地存储
localStorage.setItem('cart', JSON.stringify(state))
}, { flush: 'sync' })

取消订阅

默认情况下,state subscription 会被绑定到添加它们的组件上 (如果 store 在组件的 setup() 里面)。这意味着,当该组件被卸载时,它们将被自动删除。如果你想在组件卸载后依旧保留它们,请将 { detached: true } 作为第二个参数,以将 state subscription 从当前组件中分离:

1
2
3
4
5
<script setup>
const someStore = useSomeStore()
// 此订阅器即便在组件卸载之后仍会被保留
someStore.$subscribe(callback, { detached: true })
</script>

你可以在 pinia 实例上使用 watch() 函数侦听整个 state。

1
2
3
4
5
6
7
8
watch(
pinia.state,
(state) => {
// 每当状态发生变化时,将整个 state 持久化到本地存储。
localStorage.setItem('piniaState', JSON.stringify(state))
},
{ deep: true }
)


Getter

Getter 完全等同于 store 的 state 的计算值。可以通过 defineStore() 中的 getters 属性来定义它们。推荐使用箭头函数,并且它将接收 state 作为第一个参数:

1
2
3
4
5
6
7
8
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
}),
getters: {
doubleCount: (state) => state.count * 2,
},
})

大多数时候,getter 仅依赖 state。不过,有时它们也可能会使用其他 getter。因此,即使在使用常规函数定义 getter 时,我们也可以通过 this 访问到整个 store 实例,但(在 TypeScript 中)必须定义返回类型。这是为了避免 TypeScript 的已知缺陷,不过这不影响用箭头函数定义的 getter,也不会影响不使用 this 的 getter。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
}),
getters: {
// 自动推断出返回类型是一个 number
doubleCount(state) {
return state.count * 2
},
// 返回类型**必须**明确设置
doublePlusOne(): number {
// 整个 store 的 自动补全和类型标注 ✨
return this.doubleCount + 1
},
},
})

然后你可以直接访问 store 实例上的 getter 了:

1
2
3
4
5
6
7
8
9
<script setup>
import { useCounterStore } from './counterStore'

const store = useCounterStore()
</script>

<template>
<p>Double count is {{ store.doubleCount }}</p>
</template>

访问其他 getter

与计算属性一样,你也可以组合多个 getter。通过 this,你可以访问到其他任何 getter。在这种情况下,你需要为这个 getter 指定一个返回值的类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// counterStore.ts
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
}),
getters: {
doubleCount(state) {
return state.count * 2
},
doubleCountPlusOne(): number {
return this.doubleCount + 1
},
},
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// counterStore.js
// 你可以在 JavaScript 中使用 JSDoc (https://jsdoc.app/tags-returns.html)
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
}),
getters: {
// 类型是自动推断出来的,因为我们没有使用 `this`
doubleCount: (state) => state.count * 2,
// 这里我们需要自己添加类型(在 JS 中使用 JSDoc)
// 可以用 this 来引用 getter
/**
* 返回 count 的值乘以 2 加 1
*
* @returns {number}
*/
doubleCountPlusOne() {
// 自动补全 ✨
return this.doubleCount + 1
},
},
})

向 getter 传递参数

Getter 只是幕后的计算属性,所以不可以向它们传递任何参数。不过,你可以从 getter 返回一个函数,该函数可以接受任意参数:

1
2
3
4
5
6
7
export const useUserListStore = defineStore('userList', {
getters: {
getUserById: (state) => {
return (userId) => state.users.find((user) => user.id === userId)
},
},
})

并在组件中使用:

1
2
3
4
5
6
7
8
9
import { useUserListStore } from './store'
const userList = useUserListStore()
const { getUserById } = storeToRefs(userList)
// 请注意,你需要使用 `getUserById.value` 来访问
// <script setup> 中的函数

<template>
<p>User 2: {{ getUserById(2) }}</p>
</template>

请注意,当你这样做时,getter 将不再被缓存。它们只是一个被你调用的函数。不过,你可以在 getter 本身中缓存一些结果,虽然这种做法并不常见,但有证明表明它的性能会更好:

1
2
3
4
5
6
7
8
export const useUserListStore = defineStore('userList', {
getters: {
getActiveUserById(state) {
const activeUsers = state.users.filter((user) => user.active)
return (userId) => activeUsers.find((user) => user.id === userId)
},
},
})

访问其他 store 的 getter

想要使用另一个 store 的 getter 的话,那就直接在 getter 内使用就好:

1
2
3
4
5
6
7
8
9
10
11
12
13
import { useOtherStore } from './other-store'

export const useStore = defineStore('main', {
state: () => ({
// ...
}),
getters: {
otherGetter(state) {
const otherStore = useOtherStore()
return state.localData + otherStore.data
},
},
})

使用 setup() 时的用法

1
2
3
4
5
<script setup>
const store = useCounterStore()
store.count = 3
store.doubleCount // 6
</script>

使用选项式 API 的用法

在下面的例子中,你可以假设相关的 store 已经创建了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 示例文件路径:
// ./src/stores/counter.js

import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
}),
getters: {
doubleCount(state) {
return state.count * 2
},
},
})

Action

Action 相当于组件中的 method。它们可以通过 defineStore() 中的 actions 属性来定义,并且它们也是定义业务逻辑的完美选择。

1
2
3
4
5
6
7
8
9
10
11
12
13
export const useCounterStore = defineStore('main', {
state: () => ({
count: 0,
}),
actions: {
increment() {
this.count++
},
randomizeCounter() {
this.count = Math.round(100 * Math.random())
},
},
})

类似 getter,action 也可通过 this 访问整个 store 实例,并支持完整的类型标注(以及自动补全✨)。不同的是,action 可以是异步的,你可以在它们里面 await 调用任何 API,以及其他 action!
下面是一个使用 Mande 的例子。请注意,你使用什么库并不重要,只要你得到的是一个Promise。你甚至可以 (在浏览器中) 使用原生 fetch 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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)
// 让表单组件显示错误
return error
}
},
},
})

你也完全可以自由地设置任何你想要的参数以及返回任何结果。当调用 action 时,一切类型也都是可以被自动推断出来的。

Action 可以像函数或者通常意义上的方法一样被调用:

1
2
3
4
5
6
7
8
9
<script setup>
const store = useCounterStore()
// 将 action 作为 store 的方法进行调用
store.randomizeCounter()
</script>
<template>
<!-- 即使在模板中也可以 -->
<button @click="store.randomizeCounter()">Randomize</button>
</template>

访问其他 store 的 action

想要使用另一个 store 的话,那你直接在 action 中调用就好了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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')
}
},
},
})

使用选项式 API 的用法

在下面的例子中,你可以假设相关的 store 已经创建了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 示例文件路径:
// ./src/stores/counter.js

import { defineStore } from 'pinia'

export const useCounterStore = defineStore('main', {
state: () => ({
count: 0,
}),
actions: {
increment() {
this.count++
},
})

订阅 action

你可以通过 store.$onAction() 来监听 action 和它们的结果。传递给它的回调函数会在 action 本身之前执行。after 表示在 promise 解决之后,允许你在 action 解决后执行一个回调函数。同样地,onError 允许你在 action 抛出错误或 reject 时执行一个回调函数。这些函数对于追踪运行时错误非常有用.

这里有一个例子,在运行 action 之前以及 action resolve/reject 之后打印日志记录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const unsubscribe = someStore.$onAction(
({
name, // action 名称
store, // store 实例,类似 `someStore`
args, // 传递给 action 的参数数组
after, // 在 action 返回或解决后的钩子
onError, // action 抛出或拒绝的钩子
}) => {
// 为这个特定的 action 调用提供一个共享变量
const startTime = Date.now()
// 这将在执行 "store "的 action 之前触发。
console.log(`Start "${name}" with params [${args.join(', ')}].`)

// 这将在 action 成功并完全运行后触发。
// 它等待着任何返回的 promise
after((result) => {
console.log(
`Finished "${name}" after ${
Date.now() - startTime
}ms.\nResult: ${result}.`
)
})

// 如果 action 抛出或返回一个拒绝的 promise,这将触发
onError((error) => {
console.warn(
`Failed "${name}" after ${Date.now() - startTime}ms.\nError: ${error}.`
)
})
}
)

// 手动删除监听器
unsubscribe()

默认情况下,action 订阅器会被绑定到添加它们的组件上(如果 store 在组件的 setup() 内)。这意味着,当该组件被卸载时,它们将被自动删除。如果你想在组件卸载后依旧保留它们,请将 true 作为第二个参数传递给 action 订阅器,以便将其从当前组件中分离:

1
2
3
4
5
<script setup>
const someStore = useSomeStore()
// 此订阅器即便在组件卸载之后仍会被保留
someStore.$onAction(callback, true)
</script>

插件

由于有了底层 API 的支持,Pinia store 现在完全支持扩展。以下是你可以扩展的内容:

  • 为 store 添加新的属性
  • 定义 store 时增加新的选项
  • 为 store 增加新的方法
  • 包装现有的方法
  • 改变甚至取消 action
  • 实现副作用,如本地存储
  • 仅应用插件于特定 store

插件是通过 pinia.use() 添加到 pinia 实例的。最简单的例子是通过返回一个对象将一个静态属性添加到所有 store。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { createPinia } from 'pinia'

// 创建的每个 store 中都会添加一个名为 `secret` 的属性。
// 在安装此插件后,插件可以保存在不同的文件中
function SecretPiniaPlugin() {
return { secret: 'the cake is a lie' }
}

const pinia = createPinia()
// 将该插件交给 Pinia
pinia.use(SecretPiniaPlugin)

// 在另一个文件中
const store = useStore()
store.secret // 'the cake is a lie'

这对添加全局对象很有用,如路由器、modal 或 toast 管理器。

简介

Pinia 插件是一个函数,可以选择性地返回要添加到 store 的属性。它接收一个可选参数,即 context。

1
2
3
4
5
6
7
export function myPiniaPlugin(context) {
context.pinia // 用 `createPinia()` 创建的 pinia。
context.app // 用 `createApp()` 创建的当前应用(仅 Vue 3)。
context.store // 该插件想扩展的 store
context.options // 定义传给 `defineStore()` 的 store 的可选对象。
// ...
}

然后用 pinia.use() 将这个函数传给 pinia

1
pinia.use(myPiniaPlugin)

扩展 store

你可以通过返回一个对象来向 store 添加新的属性:

1
pinia.use(() => ({ hello: 'world' }))

你也可以直接在 store 上设置该属性,但可以的话,请使用返回对象的方法,这样它们就能被 devtools 自动追踪到:

1
2
3
pinia.use(({ store }) => {
store.hello = 'world'
})

任何由插件返回的属性都会被 devtools 自动追踪,所以如果你想在 devtools 中调试 hello 属性,为了使 devtools 能追踪到 hello,请确保在 dev 模式下将其添加到 store._customProperties 中:

1
2
3
4
5
6
7
8
9
// 上文示例
pinia.use(({ store }) => {
store.hello = 'world'
// 确保你的构建工具能处理这个问题,webpack 和 vite 在默认情况下应该能处理。
if (process.env.NODE_ENV === 'development') {
// 添加你在 store 中设置的键值
store._customProperties.add('hello')
}
})

在组件外使用 store

Pinia store 依靠 pinia 实例在所有调用中共享同一个 store 实例。大多数时候,只需调用你定义的 useStore() 函数,完全开箱即用。例如,在 setup() 中,你不需要再做任何事情。但在组件之外,情况就有点不同了。 实际上,useStore() 给你的 app 自动注入了 pinia 实例。这意味着,如果 pinia 实例不能自动注入,你必须手动提供给 useStore() 函数。 你可以根据不同的应用,以不同的方式解决这个问题。

单页面应用

如果你不做任何 SSR(服务器端渲染),在用 app.use(pinia) 安装 pinia 插件后,对 useStore() 的任何调用都会正常执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { useUserStore } from '@/stores/user'
import { createPinia } from 'pinia'
import { createApp } from 'vue'
import App from './App.vue'

// ❌ 失败,因为它是在创建 pinia 之前被调用的
const userStore = useUserStore()

const pinia = createPinia()
const app = createApp(App)
app.use(pinia)

// ✅ 成功,因为 pinia 实例现在激活了
const userStore = useUserStore()

为确保 pinia 实例被激活,最简单的方法就是将 useStore() 的调用放在 pinia 安装后才会执行的函数中。

让我们来看看这个在 Vue Router 的导航守卫中使用 store 的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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'
})

服务端渲染应用

当处理服务端渲染时,你将必须把 pinia 实例传递给 useStore()。这可以防止 pinia 在不同的应用实例之间共享全局状态。

服务端渲染 (SSR)

只要你只在 setup 函数、getteraction 的顶部调用你定义的 useStore() 函数,那么使用 Pinia 创建 store 对于 SSR 来说应该是开箱即用的:

1
2
3
4
5
<script setup>
// 这是可行的,
// 因为 pinia 知道在 `setup` 中运行的是什么程序。
const main = useMainStore()
</script>

在 setup() 外部使用 store

如果你需要在其他地方使用 store,你需要将原本被传递给应用pinia 实例传递给 useStore() 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
const pinia = createPinia()
const app = createApp(App)

app.use(router)
app.use(pinia)

router.beforeEach((to) => {
// ✅这会正常工作,因为它确保了正确的 store 被用于
// 当前正在运行的应用
const main = useMainStore(pinia)

if (to.meta.requiresAuth && !main.isLoggedIn) return '/login'
})

Pinia 会将自己作为 $pinia 添加到你的应用中,所以你可以在 serverPrefetch() 等函数中使用它。

1
2
3
4
5
export default {
serverPrefetch() {
const store = useStore(this.$pinia)
},
}

State 激活

为了激活初始 state,你需要确保 rootState 包含在 HTML 中的某个地方,以便 Pinia 稍后能够接收到它。根据你服务端所渲染的内容,为了安全你应该转义 state。我们推荐 Nuxt 目前使用的 @nuxt/devalue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import devalue from '@nuxt/devalue'
import { createPinia } from 'pinia'
// 检索服务端的 rootState
const pinia = createPinia()
const app = createApp(App)
app.use(router)
app.use(pinia)

// 渲染页面后,rootState 被建立,
// 可以直接在 `pinia.state.value`上读取。

// 序列化,转义(如果 state 的内容可以被用户改变,这点就非常重要,几乎都是这样的)
// 并将其放置在页面的某处
// 例如,作为一个全局变量。
devalue(pinia.state.value)

根据你服务端所渲染的内容,你将设置一个初始状态变量,该变量将在 HTML 中被序列化。你还应该保护自己免受 XSS 攻击。例如,在 vite-ssr中你可以使用transformState 选项 以及 @nuxt/devalue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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) {
// 序列化并设置为 window.__INITIAL_STATE__
initialState.pinia = pinia.state.value
} else {
// 在客户端,我们恢复 state
pinia.state.value = initialState.pinia
}
}
)

你可以根据你的需要使用 @nuxt/devalue其他替代品,例如,你也可以用 JSON.stringify()/JSON.parse() 来序列化和解析你的 state,这样你可以把性能提高很多。

也可以根据你的环境调整这个策略。但确保在客户端调用任何 useStore() 函数之前,激活 pinia 的 state。例如,如果我们将 state 序列化为一个 <script> 标签,并在客户端通过 window.__pinia 全局访问它,我们可以这样写:

1
2
3
4
5
6
7
8
const pinia = createPinia()
const app = createApp(App)
app.use(pinia)

// 必须由用户设置
if (isClient) {
pinia.state.value = JSON.parse(window.__pinia)
}

Nuxt

搭配 Nuxt 的 Pinia 更易用,因为 Nuxt 处理了很多与服务器端渲染有关的事情。例如,你不需要关心序列化或 XSS 攻击。Pinia 既支持 Nuxt Bridge 和 Nuxt 3,也支持纯 Nuxt 2。

安装

1
2
3
yarn add pinia @pinia/nuxt
# 或者使用 npm
npm install pinia @pinia/nuxt

如果你正在使用 npm,你可能会遇到 ERESOLVE unable to resolve dependency tree 错误。如果那样的话,将以下内容添加到 package.json 中:

1
2
3
4
5
{
"overrides": {
"vue": "latest"
}
}

我们提供了一个 module 来为你处理一切,你只需要在 nuxt.config.js 文件的 modules 中添加它。

1
2
3
4
5
6
7
8
// nuxt.config.js
export default defineNuxtConfig({
// ... 其他配置
modules: [
// ...
'@pinia/nuxt',
],
})

这样配置就完成了,正常使用 store 就好啦!

在 setup() 外部使用 store

如果你想在 setup() 外部使用一个 store,记得把 pinia 对象传给 useStore()。我们会把它添加到上下文中,然后你就可以在 asyncData()fetch() 中访问它了:

1
2
3
4
5
6
7
import { useStore } from '~/stores/myStore'

export default {
asyncData({ $pinia }) {
const store = useStore($pinia)
},
}

onServerPrefetch() 一样,如果你想在 asyncData() 中调用一个存储动作,你不需要做任何特别的事情。

1
2
3
4
<script setup>
const store = useStore()
const { data } = await useAsyncData('user', () => store.fetchUser())
</script>

自动引入

默认情况下,@pinia/nuxt 会暴露一个自动引入的方法:usePinia(),它类似于 getActivePinia(),但在 Nuxt 中效果更好。你可以添加自动引入来减轻你的开发工作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// nuxt.config.js
export default defineNuxtConfig({
// ... 其他配置
modules: [
// ...
[
'@pinia/nuxt',
{
autoImports: [
// 自动引入 `defineStore()`
'defineStore',
// 自动引入 `defineStore()` 并重命名为 `definePiniaStore()`
['defineStore', 'definePiniaStore'],
],
},
],
],
})

纯 Nuxt 2

@pinia/nuxt v0.2.1 之前的版本中,Pinia 都支持 Nuxt 2。请确保在安装 pinia 的同时也安装 @nuxtjs/composition-api:

1
2
3
yarn add pinia @pinia/nuxt@0.2.1 @nuxtjs/composition-api
# 使用 npm
npm install pinia @pinia/nuxt@0.2.1 @nuxtjs/composition-api

我们提供了一个 module 来为你处理一切工作,你只需要在 nuxt.config.js 文件的 buildModules 中添加它。

1
2
3
4
5
6
7
8
9
10
// nuxt.config.js
export default {
// ... 其他配置
buildModules: [
// 仅支持 Nuxt 2:
// https://composition-api.nuxtjs.org/getting-started/setup#quick-start
'@nuxtjs/composition-api/module',
'@pinia/nuxt',
],
}

Pinia 搭配 Vuex 使用

建议避免同时使用 Pinia 和 Vuex,但如果你确实需要同时使用,你需要告诉 Pinia 不要禁用它:

1
2
3
4
5
6
7
8
// nuxt.config.js
export default {
buildModules: [
'@nuxtjs/composition-api/module',
['@pinia/nuxt', { disableVuex: false }],
],
// ... 其他配置
}