主题
Pinia
提示
当前版本:v3.x
介绍
Pinia 是一个轻量级的全局状态管理库。
请注意:Vue3 更推荐使用 Pinia 而非 Vuex。
安装
bash
npm i pinia项目搭建
bash
/src
/store
/counter.js
/user.js
// ...
main.jsts
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 useCounterts
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'
})常见问题
vuex 和 pinia 的区别?
vuex
- 核心概念:
state、getters、mutations、actions - state 状态修改
- 组件中
commit()->mutations->state - 组件中
dispatch()->actions(异步处理) ->mutations->state
- 组件中
- 模块化:需要
modules,开启namespaced
pinia
- 核心概念:
state、getters、actions - state 状态修改
- 组件中
state - 组件中
actions->state
- 组件中
- 模块化:支持多个
store
如何实现 pinia 持久化?
- 可以直接配合
localStorage/sessionStorage或者 cookie 等浏览器存储方案自行实现持久化。 - 可以配合 pinia-plugin-persistedstate 插件来实现。
