Pinia

Store 组合、TypeScript 与插件

学习 Pinia 中多个 Store 的组织方式、TypeScript 类型支持,以及如何使用插件实现数据持久化等扩展能力。

🎯 引言

学会这篇文章,你能知道中大型项目里应该怎么拆分和组织多个 Store,怎么给 Pinia 加上 TypeScript 类型,以及怎么用插件做数据持久化。我们会避开前面文章已经讲过的内容,只聚焦这几个进阶话题。


🧱 多个 Store 如何组织

项目变大后,把所有状态塞进一个 Store 会很乱。推荐按功能拆分:

stores/
├── user.js
├── cart.js
└── settings.js

比如:

  • useUserStore:登录状态、用户信息
  • useCartStore:购物车商品
  • useSettingsStore:主题、语言等配置

每个 Store 只关心自己的状态,互相独立。需要组合时再通过引入实现。

Pinia 不需要像 Vuex 那样配置模块。每个 Store 天然就是一个独立模块。

⚡ Store 之间如何组合

当某个业务需要多个 Store 的数据时,可以在 action 中引入其他 Store:

stores/order.js
import { defineStore } from 'pinia';
import { useCartStore } from './cart.js';

export const useOrderStore = defineStore('order', {
    actions: {
        async submit() {
            const cart = useCartStore();

            await apiCreateOrder({ items: cart.items });
            cart.clear();
        },
    },
});
不要循环引入 Store。A 引入 B,B 又引入 A,会导致死循环。如果确实需要双向依赖,考虑把公共状态抽到一个独立的 Store 中。

🛠 Pinia 与 TypeScript

Pinia 对 TypeScript 很友好。给 state 加上类型断言后,类型推断会自动生效。

stores/counter.ts
import { defineStore } from 'pinia';

export const useCounterStore = defineStore('counter', {
    state: () => ({
        count: 0,
        message: '' as string,
    }),
    getters: {
        double(): number {
            return this.count * 2;
        },
    },
    actions: {
        add(n: number) {
            this.count += n;
        },
    },
});

组件中使用:

App.vue
<script setup lang="ts">
import { useCounterStore } from './stores/counter.ts';

const counter = useCounterStore();

counter.add(5); // TS 会提示参数必须是 number
</script>
如果 state 里某个字段的类型推断不准确,用 as Type 做类型断言即可,比如 items: [] as Product[]

🔌 插件:扩展 Pinia

Pinia 支持插件,可以在每个 Store 创建时注入额外能力。常见的用途有:日志、持久化、调试等。

注册插件的方式:

main.js
import { createPinia } from 'pinia';

const pinia = createPinia();

pinia.use(({ store }) => {
    console.log('Store 创建:', store.$id);
});

app.use(pinia);
插件会在每个 Store 创建时执行一次,store 参数就是当前 Store 实例。

💾 数据持久化思路

最常见的插件需求是把状态保存到 localStorage,刷新页面后不丢失。核心思路只有两步:

  1. Store 创建时,从 localStorage 读取之前保存的状态。
  2. 状态变化时,把新状态写回 localStorage
plugins/persist.js
export function piniaPersistPlugin({ store }) {
    const key = `pinia-${store.$id}`;
    const saved = localStorage.getItem(key);

    if (saved) {
        store.$patch(JSON.parse(saved));
    }

    store.$subscribe((mutation, state) => {
        localStorage.setItem(key, JSON.stringify(state));
    });
}

注册:

main.js
import { piniaPersistPlugin } from './plugins/persist.js';

pinia.use(piniaPersistPlugin);
这是简化示例。生产环境建议用 pinia-plugin-persistedstate,它支持选择持久化字段、自定义存储方式等,更健壮。

🧾 小节总结

  • 中大型项目按功能拆分 Store,如 user、cart、settings。
  • Store 之间可以在 action 中相互引入,但要避免循环依赖。
  • Pinia 原生支持 TypeScript,用类型断言可以让类型推断更准确。
  • 插件可以扩展 Pinia 能力,最常见的应用是数据持久化。

❓ 知识问答

Q1:Pinia 还需要像 Vuex 那样配置 modules 吗?

不需要。每个 Store 本身就是一个独立模块,按功能拆分即可。

Q2:两个 Store 互相引用会怎样?

可能导致循环依赖,程序报错。建议把公共状态抽到第三个 Store 中。

Q3:$subscribe 是什么?

它是 Store 实例上的方法,用来监听 state 变化,适合在插件中做持久化、日志等。

Q4:所有状态都适合持久化吗?

不是。敏感信息、临时状态、大量列表数据不建议持久化。只持久化真正需要跨页面保持的数据。


🧪 小练习

补全下面的 useSettingsStore,用 TypeScript 实现主题切换:

  1. theme 类型为 'light' | 'dark',初始为 'light'
  2. toggleTheme() action 在两种主题之间切换。
stores/settings.ts
import { defineStore } from 'pinia';

type Theme = 'light' | 'dark';

export const useSettingsStore = defineStore('settings', {
    state: () => ({
        // 请在这里定义 theme
    }),
    actions: {
        toggleTheme() {
            // 请在这里编写代码
        },
    },
});

🎉 恭喜你已经完成了 Pinia 课程的学习!从基础安装到 Store 组合、TypeScript 和插件,你已经可以在 Vue3 项目中熟练使用 Pinia 了。