Pinia 深度使用实战

Pinia 深度使用实战

Vue 3 官方状态管理方案:从 Setup Store 到插件体系、持久化、权限联动与中后台工程化落地。


目录

  1. [导读:Pinia 在架构中的位置](#导读:Pinia 在架构中的位置)
  2. [Setup Store vs Option Store](#Setup Store vs Option Store)
  3. [TypeScript 类型设计](#TypeScript 类型设计)
  4. [状态分层:什么该进 Pinia](#状态分层:什么该进 Pinia)
  5. 模块化目录与命名约定
  6. [Store 组合与跨 Store 协作](#Store 组合与跨 Store 协作)
  7. [补丁、重置与订阅 API](#补丁、重置与订阅 API)
  8. 持久化:手写与插件
  9. [插件体系:日志、重置、多 Tab 同步](#插件体系:日志、重置、多 Tab 同步)
  10. 性能:细粒度订阅与反模式
  11. [与 Router、HTTP、权限联动](#与 Router、HTTP、权限联动)
  12. 中后台实战场景
  13. [SSR / Nuxt 注意事项](#SSR / Nuxt 注意事项)
  14. [测试 Pinia Store](#测试 Pinia Store)
  15. 踩坑清单与最佳实践

一、导读:Pinia 在架构中的位置

1.1 与 Vuex、Composables 的关系

方案 适用场景 特点
组件内 ref / computed 仅本组件使用的 UI 状态 最简单,无共享成本
Composables 可复用逻辑、轻量共享 无 DevTools 时间旅行、无统一持久化入口
Pinia 跨路由/跨模块的全局业务状态 类型友好、模块化、插件、DevTools
服务端状态库(TanStack Query 等) 接口缓存、失效、重试 与 Pinia 互补,勿把 API 缓存全塞进 Store

前置阅读23-Vue 高阶技巧实战.md 第八章已有 Pinia 入门片段;本文在其基础上做体系化深化

1.2 Pinia 核心心智模型

text 复制代码
defineStore(id, setup | options)
       │
       ├─ state      → 响应式数据(ref / reactive)
       ├─ getters    → 派生状态(computed)
       ├─ actions    → 同步/异步修改(函数)
       └─ 插件       → $subscribe / $onAction / 持久化 / 日志

学习目标:能设计可维护的 Store 分层;掌握 Setup Store + TypeScript;用插件解决持久化与审计;避免「全量解构导致失去响应式」等常见坑。


二、Setup Store vs Option Store

2.1 Setup Store(推荐)

Setup Store 写法与 Composition API 一致,灵活度最高,适合复杂异步与组合逻辑。

typescript 复制代码
// stores/user.ts
import { ref, computed } from 'vue';
import { defineStore } from 'pinia';
import type { UserInfo, LoginForm } from '@/types/user';
import * as userApi from '@/api/user';

export const useUserStore = defineStore('user', () => {
  const token = ref('');
  const userInfo = ref<UserInfo | null>(null);
  const loading = ref(false);

  const isLoggedIn = computed(() => !!token.value);
  const displayName = computed(
    () => userInfo.value?.nickname ?? userInfo.value?.username ?? '访客'
  );

  async function login(form: LoginForm) {
    loading.value = true;
    try {
      const { token: t } = await userApi.login(form);
      token.value = t;
      await fetchProfile();
    } finally {
      loading.value = false;
    }
  }

  async function fetchProfile() {
    if (!token.value) return;
    userInfo.value = await userApi.getProfile();
  }

  function $reset() {
    token.value = '';
    userInfo.value = null;
    loading.value = false;
  }

  return {
    token,
    userInfo,
    loading,
    isLoggedIn,
    displayName,
    login,
    fetchProfile,
    $reset
  };
});

2.2 Option Store

Option Store 类似 Vuex,适合从 Vuex 迁移或团队更熟悉「对象式」写法。

typescript 复制代码
// stores/dict.ts
import { defineStore } from 'pinia';
import { fetchDictBatch } from '@/api/dict';

interface DictState {
  cache: Record<string, Record<string, string>>;
  loadingKeys: Set<string>;
}

export const useDictStore = defineStore('dict', {
  state: (): DictState => ({
    cache: {},
    loadingKeys: new Set()
  }),

  getters: {
    label: (state) => (type: string, value: string) => {
      return state.cache[type]?.[value] ?? value;
    },
    isLoaded: (state) => (type: string) => type in state.cache
  },

  actions: {
    async load(types: string[]) {
      const missing = types.filter((t) => !this.isLoaded(t));
      if (!missing.length) return;

      missing.forEach((t) => this.loadingKeys.add(t));
      try {
        const batch = await fetchDictBatch(missing);
        this.cache = { ...this.cache, ...batch };
      } finally {
        missing.forEach((t) => this.loadingKeys.delete(t));
      }
    }
  }
});

2.3 选型建议

维度 Setup Store Option Store
与 Composables 复用 直接 import 组合 需在 actions 里调用
$reset 需手写 $reset 函数 内置(基于初始 state)
复杂泛型 更自然 略繁琐
团队习惯 Vue 3 新项目首选 迁移 Vuex 时可用

三、TypeScript 类型设计

3.1 导出 Store 类型供组件与测试使用

typescript 复制代码
// stores/order.ts
export const useOrderStore = defineStore('order', () => {
  const list = ref<Order[]>([]);
  const filters = ref<OrderFilters>({ status: 'all', keyword: '' });
  // ...
  return { list, filters /* ... */ };
});

// 从 Store 实例推导类型(无需手写 interface 重复维护)
export type OrderStore = ReturnType<typeof useOrderStore>;

3.2 在组件中保持类型安全

vue 复制代码
<script setup lang="ts">
import { storeToRefs } from 'pinia';
import { useOrderStore } from '@/stores/order';

const orderStore = useOrderStore();
const { list, filters } = storeToRefs(orderStore);

// ✅ 直接调用 action,不丢类型
function onSearch() {
  orderStore.fetchList();
}
</script>

3.3 为插件扩展 Store 自定义属性

typescript 复制代码
// types/pinia.d.ts
import 'pinia';

declare module 'pinia' {
  export interface PiniaCustomProperties {
    $api: typeof import('@/api');
    $router: import('vue-router').Router;
  }
}

// main.ts --- 注入到每个 store
import { createPinia } from 'pinia';
import router from './router';
import api from './api';

const pinia = createPinia();
pinia.use(({ store }) => {
  store.$api = api;
  store.$router = router;
});

四、状态分层:什么该进 Pinia

4.1 推荐放入 Pinia 的状态

  • 登录态、用户信息、权限码、动态路由元数据
  • 全局 UI:侧边栏折叠、主题、语言、多标签页(Tabs)
  • 跨页面共享的业务草稿、购物车式选中集合
  • 字典/枚举缓存(读多写少)

4.2 不建议放入 Pinia 的状态

typescript 复制代码
// ❌ 反模式:把接口列表缓存当唯一数据源,却手写 loading/error/分页
const list = ref([]);
const loading = ref(false);
async function fetchList() { /* 与组件、其他页面重复 */ }

// ✅ 更优:服务端状态交给 Query,Pinia 只保留「筛选条件」等 UI 意图
// composables/useOrderListQuery.ts + store 仅存 filters

4.3 分层示意

text 复制代码
┌─────────────────────────────────────────┐
│  组件 / 页面                             │
├─────────────────────────────────────────┤
│  Composables(封装 Query、表单逻辑)      │
├─────────────────────────────────────────┤
│  Pinia(全局业务态、权限、Tabs、字典)     │
├─────────────────────────────────────────┤
│  API 层 / TanStack Query(请求与缓存)    │
└─────────────────────────────────────────┘

五、模块化目录与命名约定

5.1 推荐目录结构(中后台)

text 复制代码
src/stores/
├── index.ts              # 可选:统一导出
├── plugins/
│   ├── persist.ts
│   ├── logger.ts
│   └── tabSync.ts
├── modules/
│   ├── user.ts
│   ├── permission.ts
│   ├── app.ts            # 布局、主题
│   ├── tabs.ts           # 多标签
│   └── dict.ts
└── composables/
    └── useAppInit.ts     # 聚合初始化,不是 store

5.2 命名约定

项目 约定
Store id 短且唯一:'user''permission'
导出函数 useXxxStore(与 React 习惯对齐,便于搜索)
文件 单 Store 单文件;超大 Store 按 feature 拆分

5.3 按功能域拆分,避免「上帝 Store」

typescript 复制代码
// ❌ useAppStore 里塞 2000 行:用户 + 菜单 + 字典 + 通知
// ✅ 各司其职,在 composable 里组合
// composables/useAppBootstrap.ts
export async function bootstrapApp() {
  const user = useUserStore();
  const permission = usePermissionStore();
  const dict = useDictStore();

  if (user.token) {
    await Promise.all([
      user.fetchProfile(),
      permission.loadMenus(),
      dict.load(['orderStatus', 'payType'])
    ]);
  }
}

六、Store 组合与跨 Store 协作

6.1 在 action 内使用其他 Store

typescript 复制代码
// stores/cart.ts
import { useUserStore } from './user';

export const useCartStore = defineStore('cart', () => {
  const items = ref<CartItem[]>([]);

  async function checkout() {
    const user = useUserStore();
    if (!user.isLoggedIn) {
      user.$router?.push('/login');
      return;
    }
    await api.checkout({ items: items.value, userId: user.userInfo!.id });
    items.value = [];
  }

  return { items, checkout };
});

注意 :避免在 Store 初始化顶层 循环引用(A 引 B、B 引 A)。若必须双向依赖,将共享逻辑抽到 utils 或第三个 Store。

6.2 Facade:对外暴露统一入口

typescript 复制代码
// composables/useAppStore.ts --- 注意:这是 composable,不是 defineStore
import { useUserStore } from '@/stores/user';
import { usePermissionStore } from '@/stores/permission';

export function useAppFacade() {
  const user = useUserStore();
  const permission = usePermissionStore();

  const can = (code: string) => {
    if (!user.isLoggedIn) return false;
    return permission.codes.includes(code);
  };

  async function logout() {
    await api.logout();
    user.$reset();
    permission.$reset();
  }

  return { user, permission, can, logout };
}

七、补丁、重置与订阅 API

7.1 $patch:批量更新

typescript 复制代码
const orderStore = useOrderStore();

// 对象式 patch
orderStore.$patch({
  filters: { status: 'paid', keyword: '' },
  page: 1
});

// 函数式 patch(依赖旧状态)
orderStore.$patch((state) => {
  state.list.push(newOrder);
});

7.2 $reset 与 Option Store

Option Store 自带 $reset()。Setup Store 须在 return 中显式实现 $reset(见 2.1)。

7.3 $subscribe:监听状态变化

typescript 复制代码
// 在 store 定义外(如 main.ts 或插件内)
const userStore = useUserStore();

const stop = userStore.$subscribe(
  (mutation, state) => {
    // mutation.type: 'direct' | 'patch object' | 'patch function'
    // mutation.storeId, mutation.events (pinia 2.1+ 批量变更)
    console.log('user changed', mutation.type, state.token);
  },
  { detached: true } // 组件卸载后仍监听(全局副作用时用)
);

// 不再需要时
stop();

7.4 $onAction:监听 action 生命周期

typescript 复制代码
userStore.$onAction(({ name, args, after, onError }) => {
  const start = performance.now();
  after(() => {
    console.log(`${name} took ${performance.now() - start}ms`);
  });
  onError((err) => {
    reportError({ store: 'user', action: name, err });
  });
});

八、持久化:手写与插件

8.1 手写持久化(细粒度控制)

typescript 复制代码
// stores/user.ts 片段
import { watch } from 'vue';

const TOKEN_KEY = 'access_token';

export const useUserStore = defineStore('user', () => {
  const token = ref(localStorage.getItem(TOKEN_KEY) ?? '');

  watch(
    token,
    (val) => {
      if (val) localStorage.setItem(TOKEN_KEY, val);
      else localStorage.removeItem(TOKEN_KEY);
    },
    { flush: 'post' }
  );

  return { token /* ... */ };
});

8.2 通用持久化插件

typescript 复制代码
// stores/plugins/persist.ts
import type { PiniaPluginContext } from 'pinia';
import { watch, toRef } from 'vue';

type PersistOptions = {
  key?: string;
  paths?: string[];
  storage?: Storage;
};

export function persistPlugin(options: PersistOptions = {}) {
  const storage = options.storage ?? localStorage;

  return ({ store }: PiniaPluginContext) => {
    const key = options.key ?? store.$id;
    const saved = storage.getItem(key);
    if (saved) {
      try {
        store.$patch(JSON.parse(saved));
      } catch {
        storage.removeItem(key);
      }
    }

    store.$subscribe(
      () => {
        const state = options.paths
          ? options.paths.reduce<Record<string, unknown>>((acc, path) => {
              acc[path] = (store as any)[path];
              return acc;
            }, {})
          : store.$state;
        storage.setItem(key, JSON.stringify(state));
      },
      { detached: true }
    );
  };
}

// main.ts
pinia.use(
  persistPlugin({
    key: 'app-settings',
    paths: ['theme', 'sidebarCollapsed'],
    storage: localStorage
  })
);

8.3 使用 pinia-plugin-persistedstate(生产推荐)

bash 复制代码
npm i pinia-plugin-persistedstate
typescript 复制代码
// main.ts
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';

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

// stores/app.ts
export const useAppStore = defineStore('app', () => {
  const theme = ref<'light' | 'dark'>('light');
  return { theme };
}, {
  persist: {
    key: 'app',
    paths: ['theme'],
    storage: localStorage
  }
});

安全提示 :勿将 refresh token、敏感权限明文长期放在 localStorage;敏感态优先 httpOnly Cookie + 内存 Store。


九、插件体系:日志、重置、多 Tab 同步

9.1 开发环境 Action 日志插件

typescript 复制代码
// stores/plugins/logger.ts
import type { PiniaPluginContext } from 'pinia';

export function createLoggerPlugin(enabled = import.meta.env.DEV) {
  return ({ store }: PiniaPluginContext) => {
    if (!enabled) return;

    store.$onAction(({ name, args, after, onError }) => {
      const tag = `[pinia:${store.$id}/${name}]`;
      console.groupCollapsed(tag, args);
      after((result) => {
        console.log('→', result);
        console.groupEnd();
      });
      onError((err) => {
        console.error(err);
        console.groupEnd();
      });
    });
  };
}

9.2 多 Tab 登录态同步

typescript 复制代码
// stores/plugins/tabSync.ts
export function tabSyncPlugin(keys: string[] = ['token']) {
  return ({ store }: PiniaPluginContext) => {
    window.addEventListener('storage', (e) => {
      if (!e.key?.startsWith('pinia-')) return;
      if (e.newValue) {
        try {
          const partial = JSON.parse(e.newValue);
          store.$patch(partial);
        } catch { /* ignore */ }
      }
    });

    store.$subscribe(() => {
      const payload = keys.reduce<Record<string, unknown>>((o, k) => {
        o[k] = (store as any)[k];
        return o;
      }, {});
      localStorage.setItem(`pinia-${store.$id}`, JSON.stringify(payload));
    });
  };
}

十、性能:细粒度订阅与反模式

10.1 使用 storeToRefs 保持响应式

typescript 复制代码
// ❌ 解构丢失响应式
const { list, loading } = useOrderStore();

// ✅
import { storeToRefs } from 'pinia';
const orderStore = useOrderStore();
const { list, loading } = storeToRefs(orderStore);
const { fetchList } = orderStore; // action 可直接解构

10.2 在组件中按字段订阅(Pinia + Vue 自动追踪)

vue 复制代码
<script setup lang="ts">
import { useUserStore } from '@/stores/user';
import { computed } from 'vue';

const userStore = useUserStore();
// 仅当 permissions 变化时重新计算
const canEdit = computed(() => userStore.permissions.includes('order:edit'));
</script>

10.3 大列表:Store 存 ID,详情用 Map

typescript 复制代码
export const useOrderStore = defineStore('order', () => {
  const ids = ref<string[]>([]);
  const entities = ref<Record<string, Order>>({});

  const list = computed(() => ids.value.map((id) => entities.value[id]));

  function upsertMany(orders: Order[]) {
    orders.forEach((o) => {
      entities.value[o.id] = o;
      if (!ids.value.includes(o.id)) ids.value.push(o.id);
    });
  }

  return { ids, entities, list, upsertMany };
});

10.4 反模式速查

反模式 后果 改法
Store 存 DOM 引用 内存泄漏、难测试 放组件 ref
在 getter 里发请求 重复请求、难缓存 放 action + Query
处处 watch(store.$state) 性能差 精确 $subscribe 或 computed
复制一份 state 到组件 双源真相 storeToRefs

十一、与 Router、HTTP、权限联动

11.1 路由守卫中初始化 Store

typescript 复制代码
// router/index.ts
import { useUserStore } from '@/stores/user';
import { usePermissionStore } from '@/stores/permission';

router.beforeEach(async (to, from, next) => {
  const user = useUserStore();
  const permission = usePermissionStore();

  if (to.meta.public) return next();

  if (!user.token) {
    return next({ path: '/login', query: { redirect: to.fullPath } });
  }

  if (!permission.routesLoaded) {
    try {
      const routes = await permission.buildRoutes();
      routes.forEach((r) => router.addRoute(r));
      permission.routesLoaded = true;
      return next({ ...to, replace: true });
    } catch {
      user.logout();
      return next('/login');
    }
  }

  if (to.meta.permission && !permission.has(to.meta.permission as string)) {
    return next('/403');
  }

  next();
});

11.2 Axios 拦截器读写 Store

typescript 复制代码
// api/http.ts
import axios from 'axios';
import { useUserStore } from '@/stores/user';

const http = axios.create({ baseURL: import.meta.env.VITE_API_BASE });

http.interceptors.request.use((config) => {
  const user = useUserStore();
  if (user.token) {
    config.headers.Authorization = `Bearer ${user.token}`;
  }
  return config;
});

http.interceptors.response.use(
  (res) => res,
  async (error) => {
    if (error.response?.status === 401) {
      const user = useUserStore();
      user.$reset();
      window.location.href = '/login';
    }
    return Promise.reject(error);
  }
);

11.3 指令 v-permission 与 Store 配合

typescript 复制代码
// directives/permission.ts
import type { Directive } from 'vue';
import { usePermissionStore } from '@/stores/permission';

export const vPermission: Directive<HTMLElement, string | string[]> = {
  mounted(el, binding) {
    const permission = usePermissionStore();
    const need = Array.isArray(binding.value) ? binding.value : [binding.value];
    if (!need.some((code) => permission.has(code))) {
      el.parentNode?.removeChild(el);
    }
  }
};

十二、中后台实战场景

12.1 多标签页(Tabs)Store

typescript 复制代码
// stores/tabs.ts
import { defineStore } from 'pinia';

export interface TabItem {
  path: string;
  title: string;
  name: string;
  affix?: boolean;
}

export const useTabsStore = defineStore('tabs', () => {
  const visited = ref<TabItem[]>([]);
  const cachedNames = ref<string[]>([]);

  function addTab(tab: TabItem) {
    if (!visited.value.some((t) => t.path === tab.path)) {
      visited.value.push(tab);
    }
    if (tab.name && !cachedNames.value.includes(tab.name)) {
      cachedNames.value.push(tab.name);
    }
  }

  function closeTab(path: string) {
    const idx = visited.value.findIndex((t) => t.path === path);
    if (idx === -1) return;
    const tab = visited.value[idx];
    if (tab.affix) return;
    visited.value.splice(idx, 1);
    cachedNames.value = cachedNames.value.filter((n) => n !== tab.name);
  }

  function closeOthers(path: string) {
    visited.value = visited.value.filter((t) => t.affix || t.path === path);
    cachedNames.value = visited.value.map((t) => t.name);
  }

  return { visited, cachedNames, addTab, closeTab, closeOthers };
});

配合 <keep-alive :include="tabsStore.cachedNames"> 与路由 name 一致。

12.2 列表页筛选状态跨路由保留

typescript 复制代码
// stores/listFilters.ts
export const useListFiltersStore = defineStore('listFilters', () => {
  const filtersByRoute = ref<Record<string, Record<string, unknown>>>({});

  function getFilters(routeName: string) {
    return filtersByRoute.value[routeName] ?? {};
  }

  function setFilters(routeName: string, partial: Record<string, unknown>) {
    filtersByRoute.value[routeName] = {
      ...getFilters(routeName),
      ...partial
    };
  }

  function clear(routeName: string) {
    delete filtersByRoute.value[routeName];
  }

  return { filtersByRoute, getFilters, setFilters, clear };
}, {
  persist: { paths: ['filtersByRoute'] }
});

12.3 全局 Loading / 请求计数

typescript 复制代码
// stores/loading.ts
export const useLoadingStore = defineStore('loading', () => {
  const count = ref(0);
  const isLoading = computed(() => count.value > 0);

  function start() {
    count.value++;
  }
  function end() {
    count.value = Math.max(0, count.value - 1);
  }

  return { count, isLoading, start, end };
});

// http 拦截器
http.interceptors.request.use((config) => {
  if (!config.hideLoading) useLoadingStore().start();
  return config;
});
http.interceptors.response.use(
  (res) => {
    if (!res.config.hideLoading) useLoadingStore().end();
    return res;
  },
  (err) => {
    if (!err.config?.hideLoading) useLoadingStore().end();
    return Promise.reject(err);
  }
);

十三、SSR / Nuxt 注意事项

13.1 每请求创建 Pinia 实例

typescript 复制代码
// entry-server.ts / Nuxt 自动处理
import { createPinia } from 'pinia';

export async function render(url: string) {
  const pinia = createPinia();
  const app = createApp(App);
  app.use(pinia);

  // 预取数据
  const user = useUserStore(pinia);
  await user.fetchProfile();

  const html = await renderToString(app);
  const state = JSON.stringify(pinia.state.value);

  return { html, state };
}

13.2 避免在模块顶层访问 window / localStorage

typescript 复制代码
// ❌ 模块加载时就执行
const token = ref(localStorage.getItem('token') ?? '');

// ✅ 在 action 或 onMounted / 客户端插件中读取
function hydrateFromStorage() {
  if (import.meta.server) return;
  token.value = localStorage.getItem(TOKEN_KEY) ?? '';
}

13.3 Nuxt 3:@pinia/nuxt

typescript 复制代码
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@pinia/nuxt'],
  pinia: {
    storesDirs: ['./stores/**']
  }
});

十四、测试 Pinia Store

14.1 单元测试 Setup Store

typescript 复制代码
// stores/__tests__/user.spec.ts
import { setActivePinia, createPinia } from 'pinia';
import { beforeEach, describe, it, expect, vi } from 'vitest';
import { useUserStore } from '../user';
import * as userApi from '@/api/user';

vi.mock('@/api/user');

describe('useUserStore', () => {
  beforeEach(() => {
    setActivePinia(createPinia());
    vi.clearAllMocks();
  });

  it('login sets token and userInfo', async () => {
    vi.mocked(userApi.login).mockResolvedValue({ token: 't1' });
    vi.mocked(userApi.getProfile).mockResolvedValue({ id: '1', username: 'test' });

    const store = useUserStore();
    await store.login({ username: 'a', password: 'b' });

    expect(store.token).toBe('t1');
    expect(store.userInfo?.username).toBe('test');
    expect(store.isLoggedIn).toBe(true);
  });

  it('$reset clears state', async () => {
    const store = useUserStore();
    store.token = 'x';
    store.$reset();
    expect(store.token).toBe('');
  });
});

14.2 测试组件时 stub Store

typescript 复制代码
import { mount } from '@vue/test-utils';
import { createTestingPinia } from '@pinia/testing';
import OrderPage from '../OrderPage.vue';

it('shows empty when no orders', () => {
  const wrapper = mount(OrderPage, {
    global: {
      plugins: [
        createTestingPinia({
          initialState: {
            order: { list: [], loading: false }
          },
          stubActions: false
        })
      ]
    }
  });
  expect(wrapper.text()).toContain('暂无数据');
});

十五、踩坑清单与最佳实践

15.1 常见问题

现象 原因 解决
模板不更新 解构了 store 的 state 使用 storeToRefs
刷新后权限丢失 只持久化了 token 未拉菜单 路由守卫里 buildRoutes
HMR 后 action 重复执行 热更新边界 开发环境接受或全页刷新
两个 tab 登录态不一致 未监听 storage 使用 tabSync 插件
Option Store 中 this 类型报错 TS 严格模式 为 Store 声明 DefineStoreOptions 或改 Setup Store

15.2 最佳实践清单

  • 一域一 Store,用 composable 组合,避免上帝对象
  • Setup Store 为主 ,显式实现 $reset
  • 服务端数据优先 Query/SWR,Pinia 存「意图」与「会话」
  • 持久化白名单,不持久化敏感字段与大列表
  • 路由、HTTP、权限三处共用同一套 user/permission Store
  • 插件统一处理日志、持久化、Tab 同步
  • 为 Store 写测试,覆盖 login/logout、权限、$reset

15.3 与系列其他文章的衔接

文章 衔接点
12 前端路由与 SPA 动态路由、addRoute、History 回退
10 前端安全 Token 存储、XSS 与 localStorage
11 性能与可观测性 避免 Store 过大导致无效更新
23 Vue 高阶技巧 Composables 与 Pinia 分工
27 微前端 子应用是否共享 Pinia 实例需单独设计

十六、总结

Pinia 的价值不在于「把所有数据都放进去」,而在于为跨模块、跨路由、需持久化与可观测 的业务状态提供统一容器。掌握 Setup Store、类型推导、storeToRefs、插件与持久化之后,再配合 Router/HTTP/权限与中后台 Tabs、字典等场景,即可在真实项目中稳定落地。

相关推荐
英俊潇洒美少年1 小时前
前端 Jest 单元测试零基础实战:模板、提效、避坑、面试题(Vue 项目可用)
前端·vue.js·单元测试
和blue一起变得更好1 小时前
周三:Vue3高级组件特性
前端·javascript·vue.js
happyprince1 小时前
10-Hugging Face Transformers 量化系统深度分析
java·前端·数据库
AskHarries1 小时前
如何使用 OpenClaw Skill
前端
AI周红伟1 小时前
Agent Skills生产级Skills 案例实操-周红伟
前端·chrome·react.js·langchain
nickel3691 小时前
Qoder相关使用
java·开发语言·vue.js·spring boot
用户86284129549441 小时前
Flutter rxflare 响应式进阶:Map/List 精准字段更新(高性能实战)
前端·flutter
横木沉1 小时前
高并发场景下的前端缓存与降级策略
大数据·前端·缓存
三翼鸟数字化技术团队2 小时前
十万条数据怎么办?Vue3虚拟列表让你纵享丝滑
vue.js