Pinia学习笔记

如题,结合官方文档与网络帖子,利用AI整理

Pinia 知识点整理:从基础到进阶

目录

  1. 简介

  2. 安装和基本配置

  3. 核心概念

  4. 基础用法

  5. 进阶用法

  6. 插件系统

  7. TypeScript 支持

  8. 最佳实践

  9. 与 Vuex 的对比

  10. 总结

简介

什么是 Pinia?

Pinia 是 Vue.js 的专属状态管理库,它允许你跨组件或页面共享状态。Pinia 起始于 2019 年 11 月的一次实验,其目的是设计一个拥有组合式 API 的 Vue 状态管理库。

为什么选择 Pinia?

Pinia 相比传统的状态管理方案具有以下优势:

  • TypeScript 支持:原生支持 TypeScript,提供完善的类型推断

  • 简洁的 API:比 Vuex 更简洁,减少了样板代码

  • 组合式 API:支持 Vue 3 的组合式 API 风格

  • DevTools 支持:完整的 DevTools 集成,支持时间旅行调试

  • 插件系统:强大的插件系统,可以扩展 Pinia 的功能

  • 服务端渲染支持:原生支持服务端渲染

  • 热更新:开发时无需重载页面即可修改 Store

核心特性

Pinia 的核心概念包括:

  • Store:存储应用状态的容器

  • State:存储具体状态数据

  • Getters:计算属性,用于派生状态

  • Actions:用于修改状态的方法,可以是同步或异步

安装和基本配置

安装 Pinia

使用 npm 或 yarn 安装 Pinia:

1
2
3
4
5
6
7
8
9
10
11
# npm
npm install pinia

# yarn
yarn add pinia

# pnpm
pnpm add pinia

# bun
bun add pinia

Vue 2 兼容性

如果使用 Vue 2,还需要安装 composition-api:

1
npm install pinia @vue/composition-api

基本配置

在 main.ts 中初始化 Pinia:

1
2
3
4
5
6
7
8
9
10
// src/main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const app = createApp(App)
const pinia = createPinia()

app.use(pinia)
app.mount('#app')

核心概念

Store

Store 是 Pinia 的核心概念,每个 Store 都是一个独立的模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// src/stores/counter.ts
import { defineStore } from 'pinia'

// 组合式语法(推荐)
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const name = ref('Pinia')

const doubleCount = computed(() => count.value * 2)

function increment() {
count.value++
}

async function fetchData() {
const data = await fetch('/api/data')
name.value = data.name
}

return { count, name, doubleCount, increment, fetchData }
})

State

State 是存储应用状态的地方,相当于组件中的 data:

1
2
3
4
// 组合式
const count = ref(0)
const user = ref(null)
const todos = ref([])

Getters

Getters 是计算属性,用于从 State 中派生出新的状态:

1
2
3
// 组合式
const doubleCount = computed(() => count.value * 2)
const doublePlusOne = computed(() => doubleCount.value + 1)

Actions

Actions 用于修改状态,可以是同步或异步的:

1
2
3
4
5
6
7
8
9
// 组合式
function increment() {
count.value++
}

async function fetchUser() {
const response = await fetch('/api/user')
user.value = await response.json()
}

基础用法

在组件中使用 Store

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- src/components/Counter.vue -->
<template>
<div>
<h2>{{ counterStore.name }}</h2>
<p>Count: {{ counterStore.count }}</p>
<p>Double Count: {{ counterStore.doubleCount }}</p>
<button @click="counterStore.increment">Increment</button>
<button @click="counterStore.fetchData">Fetch Data</button>
</div>
</template>

<script setup lang="ts">
import { useCounterStore } from '@/stores/counter'

const counterStore = useCounterStore()
</script>

解构 Store

直接解构会失去响应性,需要使用 storeToRefs:

1
2
3
4
5
6
7
8
9
10
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useCounterStore } from '@/stores/counter'

const counterStore = useCounterStore()
// 保持响应性
const { count, doubleCount } = storeToRefs(counterStore)
// 方法可以直接解构
const { increment, fetchData } = counterStore
</script>

修改 State

有多种方式可以修改 State:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script setup lang="ts">
import { useCounterStore } from '@/stores/counter'

const counterStore = useCounterStore()

// 1. 直接修改
counterStore.count++

// 2. 使用 $patch 批量修改
counterStore.$patch({
count: counterStore.count + 1,
name: 'New Name'
})

// 3. 使用 $patch 函数形式(推荐用于复杂修改)
counterStore.$patch((state) => {
state.count++
state.todos.push({ id: 1, text: 'Learn Pinia' })
})

// 4. 使用 actions(推荐)
counterStore.increment()
</script>

进阶用法

监听 State 变化

使用 $subscribe 监听状态变化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script setup lang="ts">
import { useCounterStore } from '@/stores/counter'

const counterStore = useCounterStore()

// 监听单个 store
const unsubscribe = counterStore.$subscribe((mutation, state) => {
console.log('State changed:', mutation, state)
// 持久化到本地存储
localStorage.setItem('counter', JSON.stringify(state))
})

// 监听所有 store
const pinia = usePinia()
pinia.use(({ store }) => {
store.$subscribe((mutation, state) => {
console.log(`Store ${store.$id} changed`)
})
})

// 停止监听
// unsubscribe()
</script>

Getter 高级用法

访问其他 Store

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/stores/user.ts
import { defineStore } from 'pinia'
import { useCounterStore } from './counter'

export const useUserStore = defineStore('user', {
state: () => ({
userId: 1
}),
getters: {
// 访问其他 store
userWithCount: (state) => {
const counterStore = useCounterStore()
return {
userId: state.userId,
count: counterStore.count
}
}
}
})

Getter 参数化

1
2
3
4
5
6
7
// 组合式
const getUserById = computed(() => {
return (userId: number) => users.value.find(user => user.id === userId)
})

// 在组件中使用
const user = userStore.getUserById(1)

Action 高级用法

异步 Actions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// src/stores/user.ts
export const useUserStore = defineStore('user', {
state: () => ({
users: [],
loading: false,
error: null
}),
actions: {
async fetchUsers() {
this.loading = true
this.error = null
try {
const response = await fetch('/api/users')
this.users = await response.json()
} catch (error) {
this.error = error.message
} finally {
this.loading = false
}
},
async addUser(userData) {
const response = await fetch('/api/users', {
method: 'POST',
body: JSON.stringify(userData)
})
const newUser = await response.json()
this.users.push(newUser)
return newUser
}
}
})

Action 间的调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
actions: {
async fetchAndProcessData() {
// 调用其他 action
await this.fetchUsers()
this.processUserData()
},
processUserData() {
// 处理用户数据
this.users = this.users.map(user => ({
...user,
fullName: `${user.firstName} ${user.lastName}`
}))
}
}

插件系统

自定义插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// src/plugins/pinia-logger.ts
export function piniaLogger() {
return (context) => {
const { store } = context

// 监听 action 调用
store.$onAction(({ name, args, after, onError }) => {
console.log(`[Pinia Logger] Action "${name}" started with args:`, args)

after((result) => {
console.log(`[Pinia Logger] Action "${name}" succeeded with result:`, result)
})

onError((error) => {
console.error(`[Pinia Logger] Action "${name}" failed with error:`, error)
})
})

// 监听 state 变化
store.$subscribe((mutation) => {
console.log(`[Pinia Logger] State changed in store "${store.$id}":`, mutation)
})
}
}

使用插件

1
2
3
4
5
6
// src/main.ts
import { createPinia } from 'pinia'
import { piniaLogger } from './plugins/pinia-logger'

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

状态持久化插件

使用 pinia-plugin-persistedstate 实现状态持久化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
npm install pinia-plugin-persistedstate
// src/main.ts
import { createPinia } from 'pinia'
import piniaPluginPersist from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(piniaPluginPersist)
// src/stores/user.ts
export const useUserStore = defineStore('user', {
state: () => ({
token: '',
userInfo: null
}),
// 配置持久化
persist: {
enabled: true,
strategies: [
{
key: 'user-store',
storage: localStorage,
paths: ['token', 'userInfo']
}
]
}
})

TypeScript 支持

类型定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// src/types/index.ts
interface User {
id: number
name: string
email: string
}

interface CounterState {
count: number
name: string
user: User | null
}

// src/stores/counter.ts
import { defineStore } from 'pinia'
import type { CounterState } from '@/types'

export const useCounterStore = defineStore('counter', {
state: (): CounterState => ({
count: 0,
name: 'Pinia',
user: null
}),
getters: {
userName: (state): string => state.user?.name || 'Guest'
},
actions: {
setUser(user: User | null) {
this.user = user
}
}
})

组合式 API 类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// src/stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { User } from '@/types'

export const useUserStore = defineStore('user', () => {
const users = ref<User[]>([])
const loading = ref(false)

const userCount = computed(() => users.value.length)
const activeUsers = computed(() =>
users.value.filter(user => user.active)
)

const fetchUsers = async (): Promise<void> => {
loading.value = true
const response = await fetch('/api/users')
users.value = await response.json()
loading.value = false
}

const addUser = (user: Omit<User, 'id'>): void => {
const newUser = { ...user, id: Date.now() }
users.value.push(newUser)
}

return {
users,
loading,
userCount,
activeUsers,
fetchUsers,
addUser
}
})

最佳实践

项目结构

推荐的项目结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
src/
├── stores/
│ ├── index.ts # Pinia 实例
│ ├── modules/
│ │ ├── system/ # 系统级状态
│ │ │ ├── app.ts # 应用配置
│ │ │ ├── user.ts # 用户信息
│ │ │ └── permission.ts # 权限管理
│ │ └── business/ # 业务模块
│ │ ├── cart.ts # 购物车
│ │ └── product.ts # 产品管理
└── types/ # 类型定义
└── index.ts

模块化设计

1
2
3
4
5
6
// src/stores/index.ts
export * from './modules/system/app'
export * from './modules/system/user'
export * from './modules/system/permission'
export * from './modules/business/cart'
export * from './modules/business/product'

状态设计原则

  1. 单一职责:每个 Store 应该专注于一个特定的业务领域

  2. 最小状态:只存储必要的状态,避免存储可以计算得出的数据

  3. 不可变性:虽然 Pinia 允许直接修改状态,但复杂对象建议使用不可变模式

  4. 类型安全:为所有状态和操作添加类型定义

性能优化

  1. 避免过度使用全局状态:只将真正需要共享的状态放入 Store

  2. 合理使用 Getters:利用缓存机制提高性能

  3. 批量更新:使用 $patch 进行批量状态更新

  4. 按需加载:对于大型应用,可以考虑动态注册 Store

测试策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// tests/stores/counter.test.ts
import { setActivePinia, createPinia } from 'pinia'
import { useCounterStore } from '@/stores/counter'

describe('Counter Store', () => {
beforeEach(() => {
setActivePinia(createPinia())
})

it('should initialize with count 0', () => {
const counterStore = useCounterStore()
expect(counterStore.count).toBe(0)
})

it('should increment count', () => {
const counterStore = useCounterStore()
counterStore.increment()
expect(counterStore.count).toBe(1)
})

it('should calculate double count correctly', () => {
const counterStore = useCounterStore()
counterStore.count = 2
expect(counterStore.doubleCount).toBe(4)
})
})

与 Vuex 的对比

核心差异

特性 Pinia Vuex
TypeScript 支持 原生支持,类型推断完善 需要额外类型声明
模块化设计 每个 store 独立,无嵌套 需要通过 modules 嵌套
状态修改 直接修改 state 必须通过 mutations
Actions 支持同步和异步 异步操作需要通过 actions
Mutations 已废弃 必须使用 mutations 修改状态
代码简洁性 更简洁,减少样板代码 相对繁琐
DevTools 支持 完整支持 完整支持

迁移指南

从 Vuex 迁移到 Pinia:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// Vuex
const store = new Vuex.Store({
state: { count: 0 },
mutations: {
increment(state) {
state.count++
}
},
actions: {
incrementAsync({ commit }) {
setTimeout(() => commit('increment'), 1000)
}
},
getters: {
doubleCount: state => state.count * 2
}
})

// Pinia 等效代码
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
actions: {
increment() {
this.count++
},
incrementAsync() {
setTimeout(() => this.increment(), 1000)
}
},
getters: {
doubleCount: state => state.count * 2
}
})

Pinia 的优势

  1. 现代化设计:基于 Vue 3 的组合式 API 设计

  2. 优秀的 TypeScript 支持:提供完善的类型推断

  3. 简洁的 API:减少了 Vuex 中的样板代码

  4. 强大的插件系统:可以轻松扩展功能

  5. 良好的开发体验:完整的 DevTools 支持和热更新

适用场景

  • 中大型 Vue 3 项目:需要集中管理状态的应用

  • TypeScript 项目:需要类型安全的状态管理

  • 需要服务端渲染的项目:Pinia 原生支持 SSR

  • 从 Vuex 迁移的项目:提供更简洁的 API

学习建议

  1. 掌握基础概念:理解 Store、State、Getters、Actions 的基本用法

  2. 实践组合式 API:推荐使用组合式语法定义 Store

  3. 学会插件开发:利用插件扩展 Pinia 的功能

  4. 关注最佳实践:合理设计状态结构,避免过度使用全局状态

  5. 持续学习:关注 Pinia 的最新特性和最佳实践

Pinia 作为 Vue 官方推荐的状态管理库,正在成为 Vue 生态系统中的标准选择。掌握 Pinia 将有助于构建更加健壮、可维护的 Vue 应用。

参考资源: