学习 Pinia 状态管理【Plan - May - Week 2】
一、定义 Store
Store 由
defineStore()
定义,它的第一个参数要求独一无二的idimport { defineStore } from 'pinia'export const useAlertsStore = defineStore('alert', {// 配置 })
- 最好使用以
use
开头且以Store
结尾 (比如useUserStore
,useCartStore
,useProductStore
) 来命名defineStore()
的返回值- Pinia 将用传入的
id
来连接 store 和 devtoolsdefineStore()
的第二个参数可接受两类值:Setup 函数或 Option 对象。
1、Option Store
与 Vue 的选项式 API 类似,可以传入一个带有
state
、actions
与getters
属性的 Option 对象export const useCounterStore = defineStore('counter', { state: () => ({ count: 0, name: 'Tomato' }), getters: {doubleCount: (state) => state.count * 2, }, actions: {increment() {this.count++}, }, })
state
相当于 store 的数据,getters
相当于 store 的计算属性,action
相当于 store 的方法
2、Setup Store
Setup Store 与 Vue 组合式 API 的 setup 函数 相似
export const useCounterStore = defineStore('counter', () => { const count = ref(0) const doubleCount = computed(() => count.value * 2) function increment() {count.value++ }return { count, doubleCount, increment } })
ref()
相当于state
属性computed()
相当于getters
function()
相当于actions
- 要让 pinia 正确识别
state
,必须在 setup store 中返回state
的所有属性。因此不能在 store 中使用私有属性。- Setup store 也可以依赖于全局提供的属性,比如路由。任何应用层面提供的属性都可以在 store 中使用
inject()
访问,就像在组件中一样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 {// ...} })
3、使用 Store
store 实例需要像使用
<script setup>
调用useStore()
才会被创建<script setup> import { useCounterStore } from '@/stores/counter'const store = useCounterStore() </script>
store
是一个用reactive
包装的对象,这意味着不需要在 getters 后面写.value
,就像setup
中的props
一样,所以不能对它进行解构<script setup> import { useCounterStore } from '@/stores/counter' import { computed } from 'vue'const store = useCounterStore()// 这就和直接解构 `props` 一样 const { name, doubleCount } = store name // 将始终是 "Tomato" doubleCount // 将始终是 0 setTimeout(() => {store.increment() }, 1000) // 这样写是响应式的,也可以直接使用 `store.doubleCount` const doubleValue = computed(() => store.doubleCount) </script>
4、从 Store 解构
使用
storeToRefs()
保持属性从 store 中提取时仍然保持其响应性
- 它可以为每一个响应式属性创建引用
<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>
二、Pinia
1、使用 Pinia
使用 Pinia可以获得以下功能:
- 测试工具集
- 插件:可通过插件扩展 Pinia 功能
- 为 JS 开发者提供适当的 TypeScript 支持以及自动补全功能。
- 支持服务端渲染
- Devtools 支持
- 追踪 actions、mutations 的时间线
- 在组件中展示它们所用到的 Store
- 让调试更容易的 Time travel
- 热更新
- 不必重载页面即可修改 Store
- 开发时可保持当前的 State
2、基础示例
创建 Store
// stores/counter.js import { defineStore } from 'pinia'export const useCounterStore = defineStore('counter', { state: () => {return { count: 0 } }, // 也可以这样定义 // state: () => ({ count: 0 }) actions: {increment() {this.count++}, }, })
使用 store
<script setup> import { useCounterStore } from '@/stores/counter'const counter = useCounterStore()counter.count++ // 自动补全! counter.$patch({ count: counter.count + 1 }) // 或使用 action 代替 counter.increment() </script><template> <!-- 直接从 store 中访问 state --> <div>Current Count: {{ counter.count }}</div> </template>
- 使用函数定义 Store
export const useCounterStore = defineStore('counter', () => {const count = ref(0)function increment() {count.value++}return { count, increment } })
3、对比 Vuex
- Pinia 已经实现了我们在 Vuex 5 中想要的大部分功能
- 与 Vuex 相比,Pinia 不仅提供了一个更简单的 API,也提供了符合组合式 API 风格的 API,最重要的是,搭配 TypeScript 一起使用时有非常可靠的类型推断支持
- Pinia 同样要经过 RFC 流程,并且其 API 也已经进入稳定状态
- 无需要创建自定义的复杂包装器来支持 TypeScript,一切都可标注类型,API 的设计方式是尽可能地利用 TS 类型推理
- 无过多的魔法字符串注入,只需要导入函数并调用它们
- 无需要动态添加 Store,它们默认都是动态的
- ……
三、Action
Action 相当于组件中的 method。它们可以通过
defineStore()
中的actions
属性来定义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
可以是异步的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}},}, })
- 调用
<script setup> const store = useCounterStore() // 将 action 作为 store 的方法进行调用 store.randomizeCounter() </script> <template><!-- 即使在模板中也可以 --><button @click="store.randomizeCounter()">Randomize</button> </template>
1、访问其他 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')}}, }, })
2、使用选项式 API 的用法
使用 setup()
<script> import { useCounterStore } from '../stores/counter' export default defineComponent({ setup() {const counterStore = useCounterStore()return { counterStore } }, methods: {incrementAndPrint() {this.counterStore.increment()console.log('New Count:', this.counterStore.count)}, }, }) </script>
3、订阅 action
通过
store.$onAction()
来监听 action 和它们的结果
- 传递给它的回调函数会在 action 本身之前执行
after
表示在 promise 解决之后,允许你在 action 解决后执行一个回调函数onError
允许你在 action 抛出错误或 reject 时执行一个回调函数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 成功并完全运行后触发。// 它等待着任何返回的 promiseafter((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 订阅器,可以实现即便在组件卸载之后仍会被保留
<script setup> const someStore = useSomeStore() // 此订阅器即便在组件卸载之后仍会被保留 someStore.$onAction(callback, true) </script>
四、插件
1、简介
Pinia 插件是一个函数,可以选择性地返回要添加到 store 的属性。它接收一个可选参数,即
context
。export function myPiniaPlugin(context) { context.pinia // 用 `createPinia()` 创建的 pinia。 context.app // 用 `createApp()` 创建的当前应用(仅 Vue 3)。 context.store // 该插件想扩展的 store context.options // 定义传给 `defineStore()` 的 store 的可选对象。 // ... }pinia.use(myPiniaPlugin)
2、扩展 Store
- 直接通过在一个插件中返回包含特定属性的对象来为每个 store 都添加上特定属性
pinia.use(() => ({ hello: 'world' }))
或者
- 直接在
store
上设置该属性(建议使用返回对象的方法,这样它们就能被 devtools 自动追踪到)pinia.use(({ store }) => {store.hello = 'world'// 确保你的构建工具能处理这个问题,webpack 和 vite 在默认情况下应该能处理。if (process.env.NODE_ENV === 'development') {// 添加你在 store 中设置的键值store._customProperties.add('hello')} })
每个 store 都被
reactive
包装过,所以可以自动解包任何它所包含的 Ref(ref()
、computed()
…)const sharedRef = ref('shared') pinia.use(({ store }) => { // 每个 store 都有单独的 `hello` 属性 store.hello = ref('secret') // 它会被自动解包 store.hello // 'secret'// 所有的 store 都在共享 `shared` 属性的值 store.shared = sharedRef store.shared // 'shared' })
添加新的 state
如果给 store 添加新的 state 属性或者在服务端渲染的激活过程中使用的属性,必须同时在两个地方添加。
- 在
store
上- 在
store.$state
上import { toRef, ref } from 'vue'pinia.use(({ store }) => {// 为了正确地处理 SSR,我们需要确保我们没有重写任何一个// 现有的值if (!store.$state.hasOwnProperty('hasError')) {// 在插件中定义 hasError,因此每个 store 都有各自的// hasError 状态const hasError = ref(false)// 在 `$state` 上设置变量,允许它在 SSR 期间被序列化。store.$state.hasError = hasError}// 我们需要将 ref 从 state 转移到 store// 这样的话,两种方式:store.hasError 和 store.$state.hasError 都可以访问// 并且共享的是同一个变量// 查看 https://cn.vuejs.org/api/reactivity-utilities.html#torefstore.hasError = toRef(store.$state, 'hasError')// 在这种情况下,最好不要返回 `hasError`// 因为它将被显示在 devtools 的 `state` 部分// 如果我们返回它,devtools 将显示两次。 })
- 在一个插件中, state 变更或添加(包括调用
store.$patch()
)都是发生在 store 被激活之前
重置插件中添加的 state
import { toRef, ref } from 'vue'pinia.use(({ store }) => { if (!store.$state.hasOwnProperty('hasError')) {const hasError = ref(false)store.$state.hasError = hasError } store.hasError = toRef(store.$state, 'hasError')// 确认将上下文 (`this`) 设置为 store const originalReset = store.$reset.bind(store)// 覆写其 $reset 函数 return {$reset() {originalReset()store.hasError = false}, } })
3、添加新的外部属性
当添加外部属性、第三方库的类实例或非响应式的简单值时,需要先用
markRaw()
进行包装后再传给 piniaimport { markRaw } from 'vue' // 根据你的路由器的位置来调整 import { router } from './router'pinia.use(({ store }) => { store.router = markRaw(router) })
4、添加新的选项
在定义 store 时,可以创建新的选项,以便在插件中使用它们。例如,你可以创建一个
debounce
选项,允许你让任何 action 实现防抖。defineStore('search', { actions: {searchContacts() {// ...}, },// 这将在后面被一个插件读取 debounce: {// 让 action searchContacts 防抖 300mssearchContacts: 300, }, })
// 使用任意防抖库 import debounce from 'lodash/debounce'pinia.use(({ options, store }) => { if (options.debounce) {// 我们正在用新的 action 来覆盖这些 actionreturn Object.keys(options.debounce).reduce((debouncedActions, action) => {debouncedActions[action] = debounce(store[action],options.debounce[action])return debouncedActions}, {}) } })
学习资料来源:
定义 Store | Pinia
简介 | Pinia
Action | Pinia
插件 | Pinia