《Pinia 从入门到精通》Vue 3 官方状态管理 -- 进阶使用篇

发布于:2025-05-01 ⋅ 阅读:(20) ⋅ 点赞:(0)

《Pinia 从入门到精通》Vue 3 官方状态管理 – 基础入门篇
《Pinia 从入门到精通》Vue 3 官方状态管理 – 进阶使用篇
《Pinia 从入门到精通》Vue 3 官方状态管理 – 插件扩展篇

Store 的模块化设计

随着项目规模扩大,单一 Store 会迅速膨胀,导致维护困难。
我们将从实际项目结构出发,讲解如何构建多模块 Store,支持清晰组织、职责分离和可维护性。
Pinia 提供了天然模块化的设计,每一个 Store 就是一个模块,天生支持按需加载、组合调用。


4.1 多模块结构设计

✅ 推荐目录结构(中大型项目)
src/
├── stores/
│   ├── index.ts            # 导出所有 store(可选)
│   ├── user.ts             # 用户模块
│   ├── auth.ts             # 登录认证模块
│   ├── cart.ts             # 购物车模块
│   └── product.ts          # 商品模块

每个模块都用 defineStore 定义自己的状态、逻辑、计算属性,保持内聚。


4.2 定义独立模块 Store

// stores/user.ts
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', {
  state: () => ({
    name: '',
    email: ''
  }),
  actions: {
    setUser(name: string, email: string) {
      this.name = name
      this.email = email
    }
  }
})
// stores/cart.ts
import { defineStore } from 'pinia'

export const useCartStore = defineStore('cart', {
  state: () => ({
    items: [] as { id: number; name: string; qty: number }[]
  }),
  getters: {
    totalItems: (state) => state.items.reduce((sum, item) => sum + item.qty, 0)
  },
  actions: {
    addItem(item: { id: number; name: string; qty: number }) {
      this.items.push(item)
    }
  }
})

4.3 在组件中组合多个 Store

<script setup lang="ts">
import { useUserStore } from '@/stores/user'
import { useCartStore } from '@/stores/cart'

const userStore = useUserStore()
const cartStore = useCartStore()

function checkout() {
  console.log(`${userStore.name} 正在购买 ${cartStore.totalItems} 件商品`)
}
</script>

4.4 跨模块调用(解耦调用)

Pinia 中不同 Store 可相互独立调用,而无需手动注入依赖:

// stores/order.ts
import { defineStore } from 'pinia'
import { useUserStore } from './user'

export const useOrderStore = defineStore('order', {
  actions: {
    submitOrder() {
      const userStore = useUserStore()
      console.log(`下单用户:${userStore.name}`)
      // ...业务逻辑
    }
  }
})

注意调用其他 Store 必须在 action 中动态获取,而不能在 Store 顶层直接引入(避免依赖环问题)。


4.5 模块化 Store 的命名约定

内容 命名规则 示例
Store 函数 useXxxStore useUserStore
文件名 模块名小写 user.ts
Store ID 与函数名一致的小写形式 'user'
命名空间 使用 id 区分,无需嵌套定义 所有 Store 自动隔离

4.6 所有 Store 统一导出(可选)

你可以在 stores/index.ts 中统一导出,方便使用:

// stores/index.ts
export * from './user'
export * from './cart'
export * from './order'
// 使用
import { useUserStore, useCartStore } from '@/stores'

✅ 小结

模块化是构建大型 Vue 应用的核心策略,Pinia 以其函数式 Store 和天然隔离的 Store ID 设计,使模块化变得:

  • 更加清晰(每个模块职责单一)
  • 更易维护(按需加载,互不干扰)
  • 更易测试(每个 Store 独立可测试)

类型系统集成(TypeScript 支持)

Pinia 天生支持 TypeScript,基于其函数式 API 和明确的声明结构,使得类型推导更加自然、精准。


5.1 Store 的类型推导基础

最基础的 defineStore 形式在 TS 中已经具备类型提示能力:

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    name: 'Counter Module'
  }),
  getters: {
    doubleCount: (state) => state.count * 2
  },
  actions: {
    increment() {
      this.count++
    }
  }
})

此时使用该 Store 时会自动获得类型提示:

const store = useCounterStore()
store.count       // number ✅
store.increment() // 自动补全 ✅

5.2 自定义类型(推荐)

虽然大多数场景下可自动推导,但我们仍推荐使用接口显式定义 stategettersactions 类型以获得更强可维护性。

✅ 定义接口
// types/store.d.ts
export interface CounterState {
  count: number
  name: string
}

export interface CounterGetters {
  doubleCount(state: CounterState): number
}

export interface CounterActions {
  increment(): void
}
✅ 使用泛型定义 Store
import { defineStore, StoreDefinition } from 'pinia'
import type { CounterState, CounterActions, CounterGetters } from '@/types/store'

export const useCounterStore: StoreDefinition<'counter', CounterState, CounterGetters, CounterActions> = defineStore('counter', {
  state: (): CounterState => ({
    count: 0,
    name: 'Counter Store'
  }),
  getters: {
    doubleCount: (state) => state.count * 2
  },
  actions: {
    increment() {
      this.count++
    }
  }
})

5.3 Setup Store(组合式写法)中的类型支持

对于复杂逻辑,我们可以使用组合式写法,即 setup 形式的 Store,TS 类型控制更灵活。

import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useUserStore = defineStore('user', () => {
  const name = ref('Alice')
  const age = ref(25)

  const summary = computed(() => `${name.value}${age.value}岁)`)

  function setName(newName: string) {
    name.value = newName
  }

  return {
    name,
    age,
    summary,
    setName
  }
})

此种写法中,Pinia 能自动推导返回值类型,无需额外泛型,只要你在返回时显式写出 ref / computed


5.4 类型提示与 IDE 自动补全效果

  • State 属性:可识别为 ref<number> / ref<string>
  • Getter 属性:识别为 ComputedRef
  • Action 方法:自动推导参数和返回值类型

举个例子:

const userStore = useUserStore()
userStore.age.value     // number ✅
userStore.summary.value // string ✅
userStore.setName('Bob') // ✅

5.5 类型约束下的好处

特性 说明
自动补全 所有属性、方法可自动提示,无需手动查找
静态校验 错误属性/参数在编译期即可发现,避免运行时异常
支持重构 改名、移动属性时 IDE 可自动跟踪更新引用
接口复用 同一份接口可复用在组件、接口、后端通讯中

✅ 小结

Pinia 在 TypeScript 项目中具备一流的类型体验:

  • 推荐用接口明确 State / Action / Getter 结构
  • 可选使用组合式写法提升灵活性与组合能力
  • 强类型推导贯穿编写、使用、调试各个环节

持久化存储与插件机制

本节将深入讲解如何使用 Pinia 插件 实现 Store 状态的持久化存储,以及自定义扩展 Store 功能。
在实际项目中,我们常常需要:

  • 保持用户登录信息,即使刷新页面也不丢失
  • 记住用户的主题偏好、语言设置
  • 在多个 Tab 页面之间共享状态

Pinia 原生支持插件机制,非常适合用于这些扩展场景。


6.1 什么是 Pinia 插件?

插件是一个函数,在每个 Store 实例创建时执行。你可以通过插件:

  • 添加全局状态
  • 劫持/监听 Store 生命周期
  • 自动同步状态到 localStorage / sessionStorage
  • 注入外部依赖(如 Axios)

6.2 状态持久化插件:pinia-plugin-persistedstate

最常用的第三方插件是 pinia-plugin-persistedstate,可自动将状态持久化到本地存储。

✅ 安装
npm install pinia-plugin-persistedstate
✅ 注册插件
// main.ts
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)

app.use(pinia)

6.3 使用持久化配置

只需在 defineStore 中增加 persist 配置:

// stores/user.ts
export const useUserStore = defineStore('user', {
  state: () => ({
    token: '',
    theme: 'light'
  }),
  persist: true // 默认使用 localStorage
})

刷新后依然保留 token 和主题偏好。


6.4 自定义持久化策略

persist: {
  enabled: true,
  strategies: [
    {
      key: 'user-token',
      storage: sessionStorage,
      paths: ['token'] // 只存 token
    }
  ]
}
  • key:本地存储的键名
  • storage:可选 localStoragesessionStorage
  • paths:只持久化指定字段

6.5 多模块下持久化组合

每个模块 Store 都可独立配置 persist,互不影响:

// user.ts => 存 token 到 sessionStorage
persist: {
  storage: sessionStorage,
  paths: ['token']
}

// settings.ts => 存 theme 到 localStorage
persist: {
  storage: localStorage,
  paths: ['theme']
}

6.6 自定义插件机制(进阶)

你可以自定义自己的插件来扩展 Store 功能:

// plugins/logger.ts
import type { PiniaPluginContext } from 'pinia'

export function loggerPlugin({ store }: PiniaPluginContext) {
  store.$subscribe((mutation, state) => {
    console.log(`[${mutation.storeId}]`, mutation.type, mutation.events)
  })
}

注册插件:

pinia.use(loggerPlugin)

这样每次状态改变都会打印日志。


6.7 生命周期钩子:$subscribe & $onAction

Pinia 提供两种原生生命周期钩子:

1. $subscribe —— 监听状态变化
const userStore = useUserStore()

userStore.$subscribe((mutation, state) => {
  console.log('状态发生变化:', mutation, state)
})
2. $onAction —— 监听 Action 执行
userStore.$onAction(({ name, args, after, onError }) => {
  console.log(`Action 被调用: ${name}`, args)
  after(() => console.log(`${name} 执行成功`))
  onError((err) => console.error(`${name} 报错`, err))
})

非常适合做埋点、错误日志等逻辑。


✅ 小结

Pinia 插件机制极为强大和灵活:

功能类型 工具 用途描述
本地持久化 pinia-plugin-persistedstate 自动同步状态到 Storage
状态监听 $subscribe 跟踪 State 的变化
行为监听 $onAction 跟踪 Action 的执行情况
自定义扩展 插件函数 注入工具、处理副作用、封装逻辑等

最佳实践总结与项目结构规范化设计

将基于前面内容,系统梳理一套企业级 Pinia 状态管理的最佳实践,从模块设计、命名规范、状态解耦、持久化、类型安全等多个维度,构建一个清晰、稳定、可维护、易扩展的 Store 架构体系。


7.1 推荐项目结构

src/
├── stores/                # 所有 Pinia Store 模块
│   ├── user.ts            # 用户相关状态
│   ├── auth.ts            # 权限与登录认证
│   ├── ui.ts              # UI 状态(如 sidebar)
│   ├── settings.ts        # 全局设置项
│   └── index.ts           # 自动导出所有 Store
├── types/                # Store 类型定义
│   └── store.d.ts
├── plugins/              # 自定义插件(如 router 注入、日志等)
├── composables/          # 组合逻辑封装,可配合 Store 使用

7.2 Store 命名规范

类型 命名建议 示例
Store 名称 useXxxStore useUserStore
ID(storeId) 模块名小写 user, auth, ui
文件名 模块名小写 user.ts, auth.ts

命名统一,利于团队协作和自动化生成。


7.3 状态设计原则

  1. 一个模块职责单一,避免巨型 Store
  2. 状态最小化:只存 UI 需要的状态,不要存派生数据(放 getters)
  3. 与组件无关的数据放 Store,临时数据放组件
  4. 使用 ref, computed, watch 配合使用 Store 提高响应性控制

7.4 Store 类型系统统一

统一定义 Store 的 state, getters, actions 类型接口:

// types/store.d.ts
export interface UserState { name: string; age: number }
export interface UserActions { login(): void; logout(): void }
export interface UserGetters { isAdult: boolean }

结合 StoreDefinition

export const useUserStore: StoreDefinition<'user', UserState, UserGetters, UserActions> =
  defineStore('user', {
    state: (): UserState => ({ ... }),
    ...
  })

优点:

  • 强类型保障
  • 便于重构
  • 编辑器智能提示清晰

7.5 持久化策略标准化

  • token、用户信息 → 存到 sessionStorage(浏览器关闭清空)
  • 用户偏好、主题设置 → 存到 localStorage(长期保存)
  • 配置统一封装成策略常量:
const localPersist = {
  storage: localStorage,
  paths: ['theme', 'language']
}

7.6 自动化导出 Store

// stores/index.ts
export * from './user'
export * from './auth'
export * from './settings'

支持模块自动导入:

import { useUserStore, useAuthStore } from '@/stores'

7.7 组合逻辑封装:useXXXLogic

业务逻辑不要堆在组件里,应该封装成组合逻辑:

// composables/useLogin.ts
export function useLogin() {
  const userStore = useUserStore()

  const doLogin = async (formData) => {
    await userStore.login(formData)
    router.push('/dashboard')
  }

  return { doLogin }
}
  • 提高复用性
  • 分离 UI 与业务逻辑
  • 组件更轻盈可测试

✅ 实战应用:多页面应用的 Store 模型设计范式

模块名称 典型状态字段 持久化 常驻内存 是否解耦 UI
user token, info
ui sidebar, theme
auth roles, routes
search keyword, filters ❌(页面级)

✅ 统一 Store 模板(可直接复制)

// stores/xxx.ts
import { defineStore } from 'pinia'
import type { XxxState } from '@/types/store'

export const useXxxStore = defineStore('xxx', {
  state: (): XxxState => ({ ... }),
  getters: {
    ...
  },
  actions: {
    ...
  },
  persist: {
    enabled: true,
    strategies: [...]
  }
})

✅ 结语:Pinia 最佳实践总览图

+-------------------------------+
|  模块划分清晰                |
|  ↓                           |
|  单一职责                    |
|  ↓                           |
|  类型定义接口统一            |
|  ↓                           |
|  Plugin 封装 & 逻辑抽离       |
|  ↓                           |
|  状态最小化 & 可持久化        |
|  ↓                           |
|  与 Router / API / UI 解耦    |
+-------------------------------+

Pinia 的设计理念是简单、透明、类型友好。配合组合式 API,它不仅可以替代 Vuex,还能帮助我们构建更现代、更可控、更高效的前端架构。



网站公告

今日签到

点亮在社区的每一天
去签到