Pinia 状态管理设计与大型 Vue 项目实践
Pinia Store 设计模式、模块化拆分、持久化策略与 Vuex 迁移的完整指南。
Composable 设计模式、逻辑复用边界、与 Options API 的迁移策略及大型项目模块组织。
我们迁移 Vue 3 时,OrderList.vue 第一个版本把 Options API 原样塞进 setup(),500 行单文件、三个 watch 互相触发。拆成 useOrderFilters + useOrderList + useOrderExport 后,单测覆盖率从 0 到 62%,bug 率明显下降。Composition API 是逻辑组织范式,不是大 setup 函数。
一个好的 Composable 应该:
// ✅ 好的 Composable 设计
export function usePagination(options: {
fetchFn: (page: number, size: number) => Promise<PaginatedResult>;
pageSize?: number;
}) {
const page = ref(1);
const pageSize = ref(options.pageSize ?? 20);
const total = ref(0);
const data = ref<any[]>([]);
const loading = ref(false);
async function load() {
loading.value = true;
try {
const result = await options.fetchFn(page.value, pageSize.value);
data.value = result.items;
total.value = result.total;
} finally {
loading.value = false;
}
}
watch([page, pageSize], load, { immediate: true });
return { page, pageSize, total, data, loading, refresh: load };
}
src/
├── composables/ # 通用 Composable
│ ├── usePagination.ts
│ ├── usePermission.ts
│ └── useWebSocket.ts
├── features/ # 业务功能模块
│ ├── order/
│ │ ├── composables/useOrderForm.ts
│ │ ├── components/OrderForm.vue
│ │ └── api/orderApi.ts
│ └── user/
│ ├── composables/useUserProfile.ts
│ └── components/UserCard.vue
规则:通用 Composable 放 composables/,业务 Composable 放 features/*/composables/。
| 维度 | Vue Composable | React Hook |
|---|---|---|
| 调用限制 | 无限制(可在条件分支中调用) | 不可在条件/循环中调用 |
| 依赖追踪 | 自动(ref/reactive) | 手动(deps 数组) |
| 命名约定 | use 前缀 | use 前缀 |
| 状态隔离 | 每次调用独立 ref | 每次调用独立 state |
Vue Composable 的一个独特优势:可以在任何地方调用,不受 Hooks 规则限制。
不要一次性重写,按模块渐进迁移:
Phase 1: 新功能全部用 Composition API + <script setup>
Phase 2: 高频修改的旧模块逐步迁移
Phase 3: 稳定模块保持 Options API,不强制迁移
<script setup> 是推荐的默认写法:
<script setup lang="ts">
import { useOrderForm } from "./composables/useOrderForm";
const props = defineProps<{ orderId?: string }>();
const emit = defineEmits<{ submit: [order: Order] }>();
const { form, validate, submit, loading } = useOrderForm(props.orderId);
async function handleSubmit() {
if (!(await validate())) return;
const order = await submit();
emit("submit", order);
}
</script>
useApp() 返回所有状态 → 拆分为领域 ComposableuseElementSize 等专用 ComposableonUnmounted 中清理定时器、事件监听、WebSocketBefore:一个 setup 里 fetchOrders、handleFilter、exportCsv、四个 watch。
After:
// features/order/composables/useOrderListPage.ts
export function useOrderListPage() {
const filters = useOrderFilters(); // URL sync
const { data, loading, refresh } = useOrderQuery(filters);
const { exportCsv, exporting } = useOrderExport(filters);
return { filters, data, loading, refresh, exportCsv, exporting };
}
页面组件只剩布局与事件绑定 < 80 行。
Composable 用 @vue/test-utils + vi.fn() mock API:
it('usePagination loads on mount', async () => {
const fetchFn = vi.fn().mockResolvedValue({ items: [1], total: 1 });
const { data, loading } = usePagination({ fetchFn });
await flushPromises();
expect(loading.value).toBe(false);
expect(data.value).toEqual([1]);
});
业务 Composable 单测;纯展示组件用 Storybook。
Phase 3 稳定模块(如设置页)保持 Options API,ROI 不够。新功能强制 <script setup>,Code Review 拦截巨型 setup。