主题
Pinia
提示
当前版本:v3.x
介绍
Pinia 是一个轻量级的全局状态管理库。
请注意:Vue3 更推荐使用 Pinia 而非 Vuex。
安装
bash
npm install pinia
项目搭建
bash
/src
/store
/counter.js
/user.js
// ...
main.js
ts
import { defineStore } from 'pinia'
// 以“组合式”写法为例:
const useCounter = defineStore('counter', () => {
const count = ref(0)
const increment = (val: number) => {
count.value += val
}
const decrement = (val: number) => {
count.value -= val
}
const asyncIncrement = (val: number) => {
setTimeout(() => {
count.value += val * 100
}, 3000)
}
return {
count,
increment,
decrement,
asyncIncrement,
}
})
export default useCounter
ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
app.use(createPinia()) // 亦或者单独提取出 `pinia` 实例
app.mount('#app')
使用
vue
<script setup lang="ts">
import useCounter from '../../store/counter'
const counter = useCounter()
</script>
<template>
<button type="button" @click="() => counter.increment(2)">++</button>
<button type="button" @click="() => counter.asyncIncrement(2)">异步 ++</button>
<span>{{ counter.count }}</span>
<button type="button" @click="() => counter.decrement(1)">--</button>
</template>
项目结构的说明
Vuex 有一个概念,带有多个模块的单一 store。这些模块可以被命名,甚至可以互相嵌套。
将这个概念过渡到 Pinia 最简单的方法是,你以前使用的每个模块现在都是一个 store。每个 store 都需要一个 id,类似于 Vuex 中的命名空间。这意味着每个 store 都有命名空间的设计。嵌套模块也可以成为自己的 store。互相依赖的 store 可以直接导入其他 store。
你的 Vuex 模块如何重构为 Pinia store,完全取决于你,不过这里有一个示例:
bash
# 假设是命名模块
src
└── store
├── index.js # 初始化 Vuex,导入模块
└── modules
├── module1.js # 命名模块 'module1'
└── nested
├── index.js # 命名模块 'nested',导入 module2 与 module3
├── module2.js # 命名模块 'nested/module2'
└── module3.js # 命名模块 'nested/module3'
bash
# 注意 ID 与之前的命名模块相匹配
src
└── store
├── index.js # (可选) 初始化 Pinia,不必导入 store
├── module1.js # 'module1' id
├── nested-module2.js # 'nested/module2' id
├── nested-module3.js # 'nested/module3' id
└── nested.js # 'nested' id
这为 store 创建了一个扁平的结构,但也保留了和之前等价的 id 命名方式。如果你在根 store (在 Vuex 的 store/index.js
文件中)中有一些 state/getter/action/mutation
,你可以创建一个名为 root
的 store
,来保存它们。
Pinia 的目录一般被命名为 stores 而不是 store。这是为了强调 Pinia 可以使用多个 store,而不是 Vuex 的单一 store。
对于大型项目,你可能希望逐个模块进行转换,而不是一次性全部转换。其实在迁移过程中,你可以同时使用 Pinia 和 Vuex。这样也完全可以正常工作,这也是将 Pinia 目录命名为 stores
的另一个原因。
定义 Store
Store 是用 defineStore()
定义的,它的 第一个参数 要求是 一个独一无二 的名字。
js
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
Setup Store
与 Vue 的 Options API 类似:
state
相当于data
。getters
相当于computed
。actions
相当于methods
。
js
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
getters: {
double: state => state.count * 2,
},
actions: {
increment() {
this.count++
},
},
})
使用 Store
在组件中使用 Store
一旦 store 被实例化,你可以直接访问在 store 的 state
、getters
和 actions
中定义的任何属性。
请注意,store 是一个用 reactive
包装的对象,这意味着不需要在 getters
后面写 .value
。就像 setup 中的 props
一样,我们不能对它进行解构:
vue
<script setup>
import { useCounterStore } from '@/stores/counter'
// 可以在组件中的任意位置访问 `store` 变量 ✨
const store = useCounterStore()
</script>
从 Store 解构
为了从 store 中提取属性时保持其响应性,你需要使用 storeToRefs()
。它将为每一个响应式属性创建引用。当你只使用 store 的 state
而不调用任何 action
时,它会非常有用。
请注意,你可以直接从 store
中解构 action
,因为它们也被绑定到 store 上。
举个例子:
vue
<script setup>
import { storeToRefs } from 'pinia'
const store = useCounterStore()
// `name` 和 `doubleCount` 是响应式的 ref
// 同时通过插件添加的属性也会被提取为 ref
// 并且会跳过所有的 action 或非响应式 (不是 ref 或 reactive) 的属性
const { name, doubleCount } = storeToRefs(store)
// 作为 action 的 increment 可以直接解构
const { increment } = store
</script>
在组件外使用 Store
如果你不做任何 SSR(服务器端渲染),在用 app.use(pinia)
安装 pinia 插件后,对 useStore()
的任何调用都会正常执行:
js
import { useUserStore } from '@/stores/user'
import { createApp } from 'vue'
import App from './App.vue'
const userStore = useUserStore() // ❌ 失败,因为它是在创建 pinia 之前被调用的
const pinia = createPinia()
const app = createApp(App)
app.use(pinia)
const userStore = useUserStore() // ✅ 成功,因为 pinia 实例现在激活了
为确保 pinia 实例被激活,最简单的方法就是将 useStore()
的调用放在 pinia 安装后才会执行的函数中。
例如,在 Vue Router 的导航守卫中使用 store:
js
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 => {
const store = useStore() // ✅ 这样做是可行的,因为路由器是在其被安装之后开始导航的,而此时 Pinia 也已经被安装。
if (to.meta.requiresAuth && !store.isLoggedIn) return '/login'
})