Skip to content

结论速览

  • 最佳:在组合式内部方法外创建(推荐)。
  • 可用但不优:在方法内部每次调用时创建。
  • 不推荐:在模块顶层(组合式外)创建。

下面用最小可运行片段对比三种写法在实例化次数、依赖注入、SSR 隔离、HMR、可测试性、生命周期、订阅副作用上的差异。


基础准备

ts
// stores/modal.ts
import { defineStore } from "pinia";
export const useModalManagerStore = defineStore("modal", {
  state: () => ({ openCount: 0 }),
  actions: {
    open() {
      this.openCount++;
      return Promise.resolve({ ok: true, data: { modalId: "m1" } });
    },
    operationSuccess() {},
    operationFailed() {},
  },
});

A. 在组合式内部、方法外创建(✅ 推荐)

ts
// useCreateAccount.ts
import { useModalManagerStore } from "@/store";
import { ref } from "vue";

export const useCreateAccount = () => {
  // 复杂逻辑:依赖当前 app 的 active pinia;实例只取一次,复用且 SSR 安全
  const modalStore = useModalManagerStore();
  const createAccountLoading = ref(false);

  const handleCreateAccount = async (type: "internal" | "external") => {
    createAccountLoading.value = true;
    try {
      const resp = await modalStore.open();
      if (resp.ok) modalStore.operationSuccess("m1", { success: true });
    } finally {
      createAccountLoading.value = false;
    }
  };

  return { handleCreateAccount };
};

表现与优势

  • 实例化次数:每个组件实例 / 每次调用 useCreateAccount() 仅取一次 store。
  • 依赖注入:读取的是当前应用上下文的 active pinia(对微前端/多实例友好)。
  • SSR 隔离:每个请求各自的 pinia,不会串数据。
  • 可测试性:单测里 setActivePinia(createPinia()) 即可;易 Mock。
  • 性能与可读性:无重复获取,语义清晰。
  • 订阅副作用:在组件/组合式生命周期内创建,随组件销毁而释放。

B. 在方法内部创建(🟡 可用但不优)

ts
// useCreateAccount.ts
import { useModalManagerStore } from "@/store";
import { ref } from "vue";

export const useCreateAccount = () => {
  const createAccountLoading = ref(false);

  const handleCreateAccount = async (type: "internal" | "external") => {
    // 复杂逻辑:每次调用都执行一次 useModalManagerStore(),虽返回同一实例,但不必要
    const modalStore = useModalManagerStore();
    createAccountLoading.value = true;
    try {
      const resp = await modalStore.open();
      if (resp.ok) modalStore.operationSuccess("m1", { success: true });
    } finally {
      createAccountLoading.value = false;
    }
  };

  return { handleCreateAccount };
};

利弊

  • 实例化次数:方法每次被调用都会取一次(Pinia 会返回同一实例,但多余的函数调用与可读性负担)。
  • 依赖注入:仍能拿到当前 active pinia。
  • SSR:安全,但如果方法在异步回调中被频繁触发,会多次访问容器
  • 可测试性:Mock 仍可,但需要在调用点注入环境。
  • 适用场景:极少数需要延迟取用(只有用户触发时才访问 store)的场合。

C. 在模块顶层(组合式外)创建(❌ 不推荐)

ts
// useCreateAccount.ts
import { useModalManagerStore } from "@/store";

// 复杂逻辑:模块加载时就取 store —— 会绑定“当时的 active pinia”
// SSR/多应用实例/多路由应用下可能导致跨请求/跨实例共享状态
const modalStore = useModalManagerStore();

export const useCreateAccount = () => {
  const handleCreateAccount = async () => {
    const resp = await modalStore.open();
    if (resp.ok) modalStore.operationSuccess("m1", { success: true });
  };
  return { handleCreateAccount };
};

风险

  • 依赖注入时机错误:在 app 尚未 setActivePinia 前导入该模块,会拿到空上下文或错误上下文。
  • SSR 泄漏:Node 端同一模块单例被多个请求复用,导致状态串请求
  • HMR/多 App:热更或微前端里可能挂到不同根实例,但这里已绑定旧实例。
  • 订阅副作用:若 store 内有订阅/watch/定时器,顶层创建会常驻内存

维度对比清单

维度组合式内·方法外(A)方法内(B)模块顶层(C)
实例化次数✅ 1 次/组合式实例🟡 每次方法调用取一次❌ 模块加载即取
依赖注入时机✅ 正确(active pinia 就绪后)✅ 正确❌ 可能过早
SSR 请求隔离✅ 隔离良好✅ 隔离良好❌ 易串请求
HMR/多应用实例✅ 安全✅ 安全❌ 可能指向旧实例
可测试性/Mock✅ 最佳🟡 可🟡 需小心环境
性能/可读性✅ 清晰🟡 冗余调用🟡 全局副作用
订阅/副作用释放✅ 随组件生命周期✅ 随组件生命周期❌ 常驻、难释放

可观测小实验(验证“方法内多次获取”)

ts
// 在 store 里加个日志(仅调试用)
export const useModalManagerStore = defineStore("modal", {
  state: () => ({ openCount: 0 }),
  actions: {
    open() {
      this.openCount++;
      return Promise.resolve({ ok: true });
    },
  },
  // 复杂逻辑:Pinia 真实只会返回一个实例,但我们用日志统计调用 useModalManagerStore 的次数
});
ts
// 组件里
const { handleCreateAccount: A } = useCreateAccount(); // A 版本(组合式内·方法外)
const { handleCreateAccount: B } = useCreateAccount(); // B 版本(方法内取)

await A("internal");
await A("internal");
// 日志:useModalManagerStore() 调用 1 次

await B("internal");
await B("internal");
// 日志:useModalManagerStore() 调用 2 次(每次方法执行一次)

说明:Pinia 仍会返回同一 store 实例,但 B 多了函数调用与依赖获取开销;在高频交互路径中没有必要。


SSR 场景的典型坑(为什么 C 危险)

  • 服务端:import 时就执行了 useModalManagerStore()绑定了第一个请求的 pinia。
  • 第二个请求来时,模块不会重新执行顶层代码(Node 模块缓存),导致两个请求共享一个 store,出现数据串话。

最后给出你的目标组合式的推荐写法

ts
// ✅ useCreateAccount.ts —— 推荐模式(A)
import { useModalManagerStore } from "@/store";
import { message } from "ant-design-vue";
import { ref } from "vue";

export const useCreateAccount = () => {
  // 复杂逻辑:一次获取当前应用上下文下的 store,SSR/HMR 安全
  const modalStore = useModalManagerStore();
  const createAccountLoading = ref(false);

  const handleCreateAccount = async (type: "internal" | "external") => {
    try {
      const resp = await modalStore.open("AccountModal", {
        loading: createAccountLoading.value,
        mode: type,
      });
      if (resp?.ok)
        modalStore.operationSuccess(resp.data?.modalId, { success: true });
    } catch (e: any) {
      modalStore.operationFailed(e?.modalId, {
        message: e?.message || "操作失败",
        code: e?.code || "REQUEST_FAILED",
        retryable: true,
      });
      message.error(e?.message || "操作失败");
      throw e;
    }
  };

  return { handleCreateAccount };
};

选择建议(一句话版)

  • 普通 SPA/CSR/SSR:选 A(组合式内·方法外)
  • 必须“懒获取”依赖且方法很少被触发:B可接受。
  • 任何情况下:避免 C(模块顶层)

本站总访问