Pinia 是一个直观、类型安全、轻量级的 Vue.js 状态管理库,专为 Vue 3 设计,但也支持 Vue 2。它是 Vuex 5 的非官方继任者,旨在提供更简洁、更灵活、更易于理解和使用的状态管理体验,同时完美支持 TypeScript。Pinia 不仅提供了 Vuex 的所有功能,还通过优化其 API 设计和提供更好的类型推断,解决了 Vuex 在大规模应用中遇到的一些痛点。

核心思想:Pinia 将状态分割成独立的“Store”,每个 Store 都是一个模块化的、自包含的状态管理单元,拥有自己的 state、getters、actions。这种设计使得状态管理更加模块化、可维护,并能够按需加载。


一、为什么选择 Pinia?

在 Vue 3 中,Pinia 已经成为官方推荐的状态管理库,替代了Vuex。它带来的主要优势包括:

1.1 更好的 TypeScript 支持

  • 类型安全:Pinia 从设计之初就考虑了 TypeScript。所有的 Store 定义、state、getters、actions 都有良好的类型推断,无需手动编写复杂的类型声明。
  • 代码补全:在 IDE 中,你可以获得完整的代码补全功能,大大提升开发效率和减少错误。
  • 重构友好:类型安全使得重构代码时能及时发现因为类型不匹配引入的潜在问题。

1.2 模块化设计

  • Store 是独立的:每个 Store 都是一个独立的模块,拥有自己的状态、getter 和 action。这使得组织代码更加清晰,易于按功能划分。
  • 无需嵌套模块:不同于 Vuex 需要通过 modules 选项进行嵌套,Pinia 的 Store 默认是扁平的,但可以在任何组件中导入和使用,从而实现模块间的组合。
  • 动态注册:Store 可以动态注册和注销,对于按需加载和优化应用大小非常有帮助。

1.3 更简洁的 API 和更少的概念

  • 没有 Mutate:Pinia 移除了 Vuex 中的 mutations 概念。在 Pinia 中,直接修改 state 是被允许的(如果你需要严格的状态变更日志,可以在开发工具中启用 strict 模式来追踪),或者在 actions 中执行状态修改。
  • 更简单的语法state 是一个函数,getters 类似计算属性,actions 类似方法,非常直观。
  • 无需命名空间:Pinia 的 Store 本身就是命名空间化的,你不必担心命名冲突,也不需要在组件中显式引用 module/action

1.4 更小的体积

Pinia 的核心打包体积非常小,对应用的性能影响微乎其微。

1.5 易于学习和迁移

如果你熟悉 Vuex,迁移到 Pinia 会非常顺利,因为其 API 设计更加直观和友好。

二、核心概念

Pinia 的核心围绕着 Store 展开。每个 Store 都是使用 defineStore 函数定义的。

2.1 defineStore(id, options)

defineStore 是创建 Pinia Store 的核心 API。

  • id (string): 一个唯一的字符串,用作 Store 的 ID。这是 Pinia 识别 Store 的关键,也用于在 DevTools 中连接 Store。
  • options (object): 定义 Store 的配置对象,包含 stategettersactions
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import { defineStore } from 'pinia';

// 定义一个名为 'counter' 的 Store
export const useCounterStore = defineStore('counter', {
/**
* state: Store 的实际数据
* 必须是一个返回初始状态对象的函数,
* 这样可以防止在服务器渲染时,多个实例共享同一个状态对象。
*/
state: () => ({
count: 0,
name: 'Eduardo'
}),

/**
* getters: 类似于 Vue 组件中的 computed 属性
* 用于从 state 派生出一些计算值,它们是只读的。
* getter 函数接收 state 作为第一个参数。
*/
getters: {
doubleCount: (state) => state.count * 2,
// getter 也可以访问其他 getter (通过 this 访问)
// 必须明确指定返回类型,因为 TS 无法自动推断 this 的类型
doubleCountPlusOne(): number {
return this.doubleCount + 1;
},
// getter 可以在 getters 中接收其他数据
getUserById: (state) => (id: number) => {
// 假设 state 中有一个 users 数组
// return state.users.find(user => user.id === id);
return {id, name: 'Guest'}; // 示例
}
},

/**
* actions: 类似于 Vue 组件中的 methods
* 用于执行异步操作或修改 state。
* actions 可以通过 `this` 访问整个 Store 实例 (state, getters, other actions)。
*/
actions: {
increment(value = 1) {
if (this.count < 10) {
this.count += value; // 直接修改 state
}
},
async fetchData() {
// 模拟异步请求
const response = await fetch('https://jsonplaceholder.typicode.com/todos/1');
const data = await response.json();
console.log('Fetched data:', data);
// this.name = data.title; // 示例:直接修改 state
},
// Action 之间可以相互调用
async incrementAndFetch() {
this.increment(2);
await this.fetchData();
}
}
});

2.2 State

  • 定义:Store 的实际数据,必须是一个返回初始状态对象的函数。
  • 访问与修改
    • 在组件中,通过 store.propertyName 访问。
    • actions 中,通过 this.propertyName 访问和修改。
    • 在组件中,可以直接通过 store.propertyName = newValue 修改(如果你不需要严格的状态变更日志)。
    • 使用 store.$patch() 方法进行批量或复杂的状态修改,这在性能上更优。

2.2.1 直接访问和修改 (适用于简单场景)

1
2
3
4
5
6
7
8
9
10
11
12
<script setup lang="ts">
import { useCounterStore } from './stores/counter';

const counter = useCounterStore();

console.log(counter.count); // 0
counter.count++; // 直接修改
</script>

<template>
<p>Count: {{ counter.count }}</p>
</template>

2.2.2 使用 storeToRefs 保持响应式

当你直接从 Store 解构 state 属性时,会失去响应式。为了保持响应式,可以使用 Pinia 提供的 storeToRefs 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script setup lang="ts">
import { useCounterStore } from './stores/counter';
import { storeToRefs } from 'pinia';

const counter = useCounterStore();
const { count, name } = storeToRefs(counter); // count 和 name 都是响应式的 Ref 对象

// count.value++; // 修改 count 的值

// 也可以直接在模板中使用 store.count 或 count.value
</script>

<template>
<p>Count: {{ count }}</p>
<p>Name: {{ name }}</p>
</template>

2.2.3 $patch() (批量修改或复杂修改)

$patch 方法允许你通过传递一个回调函数或者一个对象来修改状态。它在底层会优化性能,只修改真正变化的属性。

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

const counter = useCounterStore();

function changeState() {
// 通过对象方式修改
// counter.$patch({
// count: counter.count + 5,
// name: 'Pinia User'
// });

// 通过回调函数方式修改 (更适合复杂逻辑)
counter.$patch((state) => {
state.count += 5;
state.name = 'Pinia User';
});
}
</script>

<template>
<p>Count: {{ counter.count }}</p>
<button @click="changeState">Change State</button>
</template>

2.3 Getters

  • 定义:类似 Vue 的计算属性,用于从 state 中派生新的数据。它们是只读的。
  • 访问:在组件中,通过 store.getterName 访问。在 actions 或其他 getters 中,通过 this.getterName 访问。
  • 带参数的 Getter:可以定义返回一个函数的 Getter,以便在访问时传入参数。
1
2
3
4
5
6
7
8
9
10
11
12
<script setup lang="ts">
import { useCounterStore } from './stores/counter';

const counter = useCounterStore();
</script>

<template>
<p>Original Count: {{ counter.count }}</p>
<p>Double Count: {{ counter.doubleCount }}</p>
<p>Double Count Plus One: {{ counter.doubleCountPlusOne }}</p>
<p>User by ID 1: {{ counter.getUserById(1).name }}</p>
</template>

2.4 Actions

  • 定义:用于执行业务逻辑,可以包含异步操作,也可以直接修改 state
  • 访问:在组件中,通过 store.actionName() 调用。在其他 actions 中,通过 this.actionName() 调用。
  • 可以直接修改 state:这是 Pinia 区别于 Vuex mutations 的一个重要特点。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script setup lang="ts">
import { useCounterStore } from './stores/counter';

const counter = useCounterStore();

function handleClick() {
counter.increment(2); // 调用 action
}

async function handleFetch() {
await counter.fetchData(); // 调用异步 action
}
</script>

<template>
<p>Count: {{ counter.count }}</p>
<button @click="handleClick">Increment</button>
<button @click="handleFetch">Fetch Data</button>
</template>

三、安装与使用

3.1 安装 Pinia

1
2
3
4
5
npm install pinia
# 或者
yarn add pinia
# 或者
pnpm add pinia

3.2 在 Vue 2 或 Vue 3 应用中集成

Vue 3 (推荐)

在你的 main.ts (或 main.js) 文件中:

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

const app = createApp(App);
const pinia = createPinia(); // 创建 Pinia 实例

app.use(pinia); // 将 Pinia 挂载到 Vue 应用
app.mount('#app');

Vue 2 (使用 Composition API 插件)

首先确保你已经安装了 @vue/composition-api

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
npm install @vue/composition-api pinia@^2.0.0```

在 `main.js` 文件中:

```javascript
// main.js
import Vue from 'vue';
import VueCompositionAPI from '@vue/composition-api';
import { createPinia, PiniaVuePlugin } from 'pinia';
import App from './App.vue';

Vue.use(VueCompositionAPI); // 注册 Composition API 插件
Vue.use(PiniaVuePlugin); // 注册 Pinia Vue 插件

const pinia = createPinia(); // 创建 Pinia 实例

new Vue({
pinia, // 将 Pinia 实例传入 Vue 根实例的选项中
render: h => h(App)
}).$mount('#app');

四、Store 之间的交互

Store 是独立的,但它们之间可以相互调用,这使得复杂的业务逻辑能够更好地模块化。

4.1 在一个 Store 中使用另一个 Store

你可以在一个 Store 的 actionsgetters 中,直接导入并使用另一个 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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// stores/user.ts
import { defineStore } from 'pinia';
import { useCounterStore } from './counter'; // 导入另一个 Store

export const useUserStore = defineStore('user', {
state: () => ({
userId: 0,
userName: 'Guest',
}),
actions: {
async login(username: string, password: string) {
// 模拟登录请求
await new Promise(resolve => setTimeout(resolve, 500));
this.userId = 123;
this.userName = username;

// 登录成功后,可以调用其他 Store 的 action
const counterStore = useCounterStore();
counterStore.increment(10); // 增加计数
console.log('Login successful! Counter incremented by 10.');
return true;
},
logout() {
this.userId = 0;
this.userName = 'Guest';
const counterStore = useCounterStore();
counterStore.$reset(); // 调用 counterStore 的 $reset 方法重置其状态
// $reset 方法是 Pinia 自动为每个 Store 提供的方法
}
},
});

// stores/counter.ts (保持不变)
import { defineStore } from 'pinia';

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

五、插件 (Plugins)

Pinia 插件允许你扩展 Store 的功能,例如添加新的属性、方法,或修改其行为。

5.1 创建一个 Pinia 插件

一个 Pinia 插件就是一个函数,它接收一个 Context 对象作为参数。

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
import storage from 'localforage'; // 假设使用 localforage 进行异步存储
import { PiniaPluginContext } from 'pinia';

/**
* 这是一个 Pinia 插件,用于将特定 Store 的状态持久化到 localforage 中。
*/
function PiniaPersistPlugin({ store }: PiniaPluginContext) {
// 定义哪些 Store 需要持久化
const persistStores = ['user', 'settings'];

// 如果当前 Store 不在持久化列表中,则跳过
if (!persistStores.includes(store.$id)) {
return;
}

// 尝试从存储中加载状态
storage.getItem(store.$id).then((persistedState: any) => {
if (persistedState) {
store.$patch(persistedState); // 应用加载的状态
}
});

// 订阅状态变化,并在变化时更新存储
store.$subscribe((mutation, state) => {
// mutation.storeId 是当前 Store 的 ID
// state 是当前 Store 的整个状态
storage.setItem(mutation.storeId, state);
}, { detached: true }); // detached: true 表示即使组件卸载,订阅仍然有效
}

5.2 注册插件

main.ts (或 main.js) 中将插件注册到 Pinia 实例上。

1
2
3
4
5
6
7
8
9
10
11
12
13
// main.ts
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
import PiniaPersistPlugin from './plugins/persist'; // 导入我们创建的插件

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

pinia.use(PiniaPersistPlugin); // 注册插件

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

六、与 Vue DevTools 的集成

Pinia 与 Vue DevTools 深度集成,提供了极佳的开发体验:

  • Store 状态可视化:可以检查所有 Store 的状态,包括 stategetters
  • Action 追踪:所有 actions 的调用都会被记录,并显示其 payload。
  • 时间旅行调试:可以在历史记录中切换,查看状态在不同时间点的变化。
  • 状态修改:可以直接在 DevTools 中修改 Store 的状态。
  • 插件调试:插件对 Store 的影响也会在 DevTools 中体现。

七、总结

Pinia 作为 Vue 3 官方推荐的状态管理库,凭借其简洁的 API、强大的 TypeScript 支持、模块化的设计以及与 Vue DevTools 的深度集成,为开发者提供了一个高效、愉悦的状态管理解决方案。它不仅解决了 Vuex 在特定场景下的痛点,还在易用性和开发体验上取得了显著进步。无论你是开发小型应用还是大型企业级项目,Pinia 都是一个非常值得选择的状态管理工具。