安装
用你喜欢的包管理器安装 pinia
:
1 | yarn add pinia |
如果你的应用使用的 Vue 版本低于
2.7
,你还需要安装组合式 API 包:@vue/composition-api
。
创建一个 pinia 实例
创建一个 pinia 实例 (根 store) 并将其传递给应用:
1 | import { createApp } from 'vue' |
如果你使用的是 Vue 2
,你还需要安装一个插件,并在应用的根部注入创建的 pinia
:
1 | import { createPinia, PiniaVuePlugin } from '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 | import { defineStore } from 'pinia' |
这个名字 ,也被用作 id ,是必须传入的, Pinia 将用它来连接 store 和 devtools。为了养成习惯性的用法,将返回的函数命名为 use… 是一个符合组合式函数风格的约定。
defineStore()
的第二个参数可接受两类值:Setup 函数
或 Option 对象
。
Option Store
与 Vue 的选项式 API 类似,我们也可以传入一个带有 state
、actions
与 getters
属性的 Option 对象
1 | export const useCounterStore = defineStore('counter', { |
你可以认为 state
是 store 的数据 (data
),getters
是 store 的计算属性 (computed
),而 actions
则是方法 (methods
)。
为方便上手使用,Option Store 应尽可能直观简单。
Setup Store
也存在另一种定义 store 的可用语法。与 Vue 组合式 API 的 setup 函数 相似,我们可以传入一个函数,该函数定义了一些响应式属性和方法,并且返回一个带有我们想暴露出去的属性和方法的对象。
1 | export const useCounterStore = defineStore('counter', () => { |
在 Setup Store 中:
ref()
就是state
属性computed()
就是getters
属性function()
就是actions
属性
注意,要让 pinia 正确识别 state
,你必须在 setup store 中返回 state
的所有属性。这意味着,你不能在 store 中使用私有属性。不完整返回会影响 SSR
,开发工具和其他插件的正常运行。
Setup store
比 Option Store
带来了更多的灵活性,因为你可以在一个 store
内创建侦听器,并自由地使用任何组合式函数。不过,请记住,使用组合式函数会让 SSR
变得更加复杂。
Setup store 也可以依赖于全局提供的属性,比如路由。任何应用层面提供的属性都可以在 store 中使用 inject()
访问,就像在组件中一样:
1 | import { inject } from 'vue' |
不要返回像
route
或appProvided
(上例中)之类的属性,因为它们不属于 store,而且你可以在组件中直接用useRoute()
和inject('appProvided')
访问。
你应该选用哪种语法?
和在 Vue 中如何选择组合式 API 与选项式 API 一样,选择你觉得最舒服的那一个就好。两种语法都有各自的优势和劣势。Option Store 更容易使用,而 Setup Store 更灵活和强大。
使用 Store
虽然我们前面定义了一个 store,但在我们使用 <script setup>
调用 useStore()
(或者使用 setup()
函数,像所有的组件那样) 之前,store 实例是不会被创建的:
1 | import { useCounterStore } from '@/stores/counter' |
请注意,
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 | <script setup> |
state
在 Pinia 中,state 被定义为一个返回初始状态的函数。这使得 Pinia 可以同时支持服务端
和客户端
。
1 | import { defineStore } from 'pinia' |
TypeScript
你并不需要做太多努力就能使你的 state 兼容 TS。确保启用了 strict,或者至少启用了 noImplicitThis,Pinia 将自动推断您的状态类型! 但是,在某些情况下,您应该帮助它进行一些转换:
1 | const useStore = defineStore('storeId', { |
如果你愿意,你可以用一个接口定义 state,并添加 state() 的返回值的类型。
1 | interface State { |
访问 state
默认情况下,你可以通过 store
实例访问 state,直接对其进行读写。
1 | const store = useStore() |
注意,新的属性如果没有在
state()
中被定义,则不能被添加。它必须
包含初始状态。
重置state
- 使用 选项式 API 时,你可以通过调用 store 的
$reset()
方法将 state 重置为初始值。
1 | const store = useStore() |
在 $reset()
内部,会调用 state()
函数来创建一个新的状态对象,并用它替换当前状态。
- 在 Setup Stores 中,您需要
创建自己的
$reset() 方法:
1 | export const useCounterStore = defineStore('counter', () => { |
使用选项式 API 的用法
在下面的例子中,你可以假设相关 store 已经创建了:
1 | // 示例文件路径: |
如果你不能使用组合式 API,但你可以使用 computed,methods,…,那你可以使用 mapState()
辅助函数将 state 属性映射为只读的计算属性:
1 | import { mapState } from 'pinia' |
可修改的 state
如果你想修改这些 state 属性 (例如,如果你有一个表单),你可以使用 mapWritableState() 作为代替。但注意你不能像 mapState() 那样传递一个函数:
1 | import { mapWritableState } from 'pinia' |
变更 state - $patch
除了用 store.count++
直接改变 store,你还可以调用 $patch
方法。它允许你用一个 state
的补丁对象在同一时间更改多个属性:
1 | store.$patch({ |
不过,用这种语法的话,有些变更真的很难实现或者很耗时:任何集合的修改(例如,向数组中添加、移除一个元素或是做 splice
操作)都需要你创建一个新的集合。因此,$patch
方法也接受一个函数来组合这种难以用补丁对象实现的变更。
1 | store.$patch((state) => { |
两种变更 store 方法的主要区别是,$patch() 允许你将多个变更归入 devtools 的同一个条目中。同时请注意,直接修改 state,$patch() 也会出现在 devtools 中,而且可以进行 time travel (在 Vue 3 中还没有)。
替换 state
你不能完全替换掉 store 的 state,因为那样会破坏其响应性。但是,你可以 patch 它。
1 | // 这实际上并没有替换`$state` |
订阅 state - $subscribe()
类似于 Vuex 的 subscribe
方法,你可以通过 store 的 $subscribe()
方法侦听 state 及其变化。比起普通的 watch()
,使用 $subscribe()
的好处是 subscriptions 在 patch 后只触发一次 (例如,当使用上面的函数版本时)。
1 | cartStore.$subscribe((mutation, state) => { |
刷新时机
在底层实现上,$subscribe()
使用了 Vue 的 watch()
函数。你可以传入与 watch()
相同的选项。当你想要在 每次 state 变化后立即触发订阅时很有用:
1 | cartStore.$subscribe((mutation, state) => { |
取消订阅
默认情况下,state subscription 会被绑定到添加它们的组件上 (如果 store 在组件的 setup()
里面)。这意味着,当该组件被卸载时,它们将被自动删除。如果你想在组件卸载后依旧保留它们,请将 { detached: true }
作为第二个参数,以将 state subscription 从当前组件中分离:
1 | <script setup> |
你可以在
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 | export const useCounterStore = defineStore('counter', { |
大多数时候,getter 仅依赖 state。不过,有时它们也可能会使用其他 getter。因此,即使在使用常规函数定义 getter 时,我们也可以通过 this
访问到整个 store 实例,但(在 TypeScript 中)必须定义返回类型。这是为了避免 TypeScript 的已知缺陷,不过这不影响用箭头函数定义的 getter,也不会影响不使用 this
的 getter。
1 | export const useCounterStore = defineStore('counter', { |
然后你可以直接访问 store 实例上的 getter 了:
1 | <script setup> |
访问其他 getter
与计算属性一样,你也可以组合多个 getter。通过 this
,你可以访问到其他任何 getter。在这种情况下,你需要为这个 getter 指定一个返回值的类型。
1 | // counterStore.ts |
1 | // counterStore.js |
向 getter 传递参数
Getter 只是幕后的计算属性,所以不可以向它们传递任何参数。不过,你可以从 getter 返回一个函数,该函数可以接受任意参数:
1 | export const useUserListStore = defineStore('userList', { |
并在组件中使用:
1 | import { useUserListStore } from './store' |
请注意,当你这样做时,getter 将不再被缓存。它们只是一个被你调用的函数。不过,你可以在 getter 本身中缓存一些结果,虽然这种做法并不常见,但有证明表明它的性能会更好:
1 | export const useUserListStore = defineStore('userList', { |
访问其他 store 的 getter
想要使用另一个 store 的 getter 的话,那就直接在 getter 内使用就好:
1 | import { useOtherStore } from './other-store' |
使用 setup() 时的用法
1 | <script setup> |
使用选项式 API 的用法
在下面的例子中,你可以假设相关的 store 已经创建了:
1 | // 示例文件路径: |
Action
Action 相当于组件中的 method
。它们可以通过 defineStore()
中的 actions
属性来定义,并且它们也是定义业务逻辑的完美选择。
1 | export const useCounterStore = defineStore('main', { |
类似 getter,action 也可通过 this
访问整个 store 实例,并支持完整的类型标注(以及自动补全✨)。不同的是,action
可以是异步的,你可以在它们里面 await
调用任何 API,以及其他 action!
下面是一个使用 Mande 的例子。请注意,你使用什么库并不重要,只要你得到的是一个Promise。你甚至可以 (在浏览器中) 使用原生 fetch 函数:
1 | import { mande } from 'mande' |
你也完全可以自由地设置任何你想要的参数以及返回任何结果。当调用 action 时,一切类型也都是可以被自动推断出来的。
Action 可以像函数或者通常意义上的方法一样被调用:
1 | <script setup> |
访问其他 store 的 action
想要使用另一个 store 的话,那你直接在 action 中调用就好了:
1 | import { useAuthStore } from './auth-store' |
使用选项式 API 的用法
在下面的例子中,你可以假设相关的 store 已经创建了:
1 | // 示例文件路径: |
订阅 action
你可以通过 store.$onAction()
来监听 action 和它们的结果。传递给它的回调函数会在 action 本身之前执行。after
表示在 promise 解决之后,允许你在 action 解决后执行一个回调函数。同样地,onError
允许你在 action 抛出错误或 reject 时执行一个回调函数。这些函数对于追踪运行时错误非常有用.
这里有一个例子,在运行 action 之前以及 action resolve/reject 之后打印日志记录。
1 | const unsubscribe = someStore.$onAction( |
默认情况下,action 订阅器会被绑定到添加它们的组件上(如果 store 在组件的 setup()
内)。这意味着,当该组件被卸载时,它们将被自动删除。如果你想在组件卸载后依旧保留它们,请将 true
作为第二个参数传递给 action 订阅器,以便将其从当前组件中分离:
1 | <script setup> |
插件
由于有了底层 API 的支持,Pinia store 现在完全支持扩展。以下是你可以扩展的内容:
- 为 store 添加新的属性
- 定义 store 时增加新的选项
- 为 store 增加新的方法
- 包装现有的方法
- 改变甚至取消 action
- 实现副作用,如本地存储
- 仅应用插件于特定 store
插件是通过 pinia.use()
添加到 pinia 实例的。最简单的例子是通过返回一个对象将一个静态属性添加到所有 store。
1 | import { createPinia } from 'pinia' |
这对添加全局对象很有用,如路由器、modal 或 toast 管理器。
简介
Pinia 插件是一个函数,可以选择性地返回要添加到 store 的属性。它接收一个可选参数,即 context。
1 | export function myPiniaPlugin(context) { |
然后用 pinia.use()
将这个函数传给 pinia
:
1 | pinia.use(myPiniaPlugin) |
扩展 store
你可以通过返回一个对象来向 store 添加新的属性:
1 | pinia.use(() => ({ hello: 'world' })) |
你也可以直接在 store 上设置该属性,但可以的话,请使用返回对象的方法,这样它们就能被 devtools 自动追踪到:
1 | pinia.use(({ store }) => { |
任何由插件返回的属性都会被 devtools 自动追踪,所以如果你想在 devtools 中调试 hello 属性,为了使 devtools 能追踪到 hello,请确保在 dev 模式下将其添加到 store._customProperties 中:
1 | // 上文示例 |
在组件外使用 store
Pinia store 依靠 pinia
实例在所有调用中共享同一个 store 实例。大多数时候,只需调用你定义的 useStore()
函数,完全开箱即用。例如,在 setup()
中,你不需要再做任何事情。但在组件之外,情况就有点不同了。 实际上,useStore()
给你的 app
自动注入了 pinia
实例。这意味着,如果 pinia
实例不能自动注入,你必须手动提供给 useStore()
函数。 你可以根据不同的应用,以不同的方式解决这个问题。
单页面应用
如果你不做任何 SSR(服务器端渲染),在用 app.use(pinia)
安装 pinia 插件后,对 useStore()
的任何调用都会正常执行:
1 | import { useUserStore } from '@/stores/user' |
为确保 pinia 实例被激活,最简单的方法就是将 useStore()
的调用放在 pinia 安装后才会执行的函数中。
让我们来看看这个在 Vue Router 的导航守卫中使用 store 的例子。
1 | import { createRouter } from 'vue-router' |
服务端渲染应用
当处理服务端渲染时,你将必须把 pinia
实例传递给 useStore()
。这可以防止 pinia 在不同的应用实例之间共享全局状态。
服务端渲染 (SSR)
只要你只在 setup
函数、getter
和 action
的顶部调用你定义的 useStore()
函数,那么使用 Pinia 创建 store 对于 SSR 来说应该是开箱即用的:
1 | <script setup> |
在 setup() 外部使用 store
如果你需要在其他地方使用 store,你需要将原本被传递给应用 的 pinia
实例传递给 useStore()
函数:
1 | const pinia = createPinia() |
Pinia 会将自己作为 $pinia
添加到你的应用中,所以你可以在 serverPrefetch()
等函数中使用它。
1 | export default { |
State 激活
为了激活初始 state,你需要确保 rootState 包含在 HTML 中的某个地方,以便 Pinia 稍后能够接收到它。根据你服务端所渲染的内容,为了安全你应该转义 state。我们推荐 Nuxt 目前使用的 @nuxt/devalue:
1 | import devalue from '@nuxt/devalue' |
根据你服务端所渲染的内容,你将设置一个初始状态变量,该变量将在 HTML 中被序列化。你还应该保护自己免受 XSS 攻击。例如,在 vite-ssr中你可以使用transformState 选项 以及 @nuxt/devalue:
1 | import devalue from '@nuxt/devalue' |
你可以根据你的需要使用 @nuxt/devalue
的其他替代品,例如,你也可以用 JSON.stringify()/JSON.parse()
来序列化和解析你的 state,这样你可以把性能提高很多。
也可以根据你的环境调整这个策略。但确保在客户端调用任何 useStore()
函数之前,激活 pinia 的 state。例如,如果我们将 state 序列化为一个 <script>
标签,并在客户端通过 window.__pinia
全局访问它,我们可以这样写:
1 | const pinia = createPinia() |
Nuxt
搭配 Nuxt 的 Pinia 更易用,因为 Nuxt 处理了很多与服务器端渲染有关的事情。例如,你不需要关心序列化或 XSS 攻击。Pinia 既支持 Nuxt Bridge 和 Nuxt 3,也支持纯 Nuxt 2。
安装
1 | yarn add 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 | // nuxt.config.js |
这样配置就完成了,正常使用 store 就好啦!
在 setup() 外部使用 store
如果你想在 setup()
外部使用一个 store,记得把 pinia
对象传给 useStore()
。我们会把它添加到上下文中,然后你就可以在 asyncData()
和 fetch()
中访问它了:
1 | import { useStore } from '~/stores/myStore' |
与 onServerPrefetch()
一样,如果你想在 asyncData()
中调用一个存储动作,你不需要做任何特别的事情。
1 | <script setup> |
自动引入
默认情况下,@pinia/nuxt
会暴露一个自动引入的方法:usePinia()
,它类似于 getActivePinia()
,但在 Nuxt 中效果更好。你可以添加自动引入来减轻你的开发工作:
1 | // nuxt.config.js |
纯 Nuxt 2
@pinia/nuxt
v0.2.1 之前的版本中,Pinia 都支持 Nuxt 2。请确保在安装 pinia 的同时也安装 @nuxtjs/composition-api:
1 | yarn add pinia @pinia/nuxt@0.2.1 @nuxtjs/composition-api |
我们提供了一个 module 来为你处理一切工作,你只需要在 nuxt.config.js
文件的 buildModules
中添加它。
1 | // nuxt.config.js |
Pinia 搭配 Vuex 使用
建议避免同时使用 Pinia 和 Vuex,但如果你确实需要同时使用,你需要告诉 Pinia 不要禁用它:
1 | // nuxt.config.js |