全局弹窗

全局弹窗(Global Confirm / Sonner)的实现模式与代码示例

#type / howto #status / evergreen #tech / dev / component-patterns

[!info] related notes

全局弹窗 (Global Confirm / Sonner)

  • 背后的核心模式:单例模式 (Singleton) + 观察者模式 (Observer)

目标

在 Vue/React 应用中实现全局弹窗,任何组件均可调用,无需手动管理 DOM 挂载与销毁。

前置条件

  • Vue 3 + Composition API 或 React 18+
  • 已安装 UI 库(如 Element Plus / shadcn-ui / Sonner)

步骤

1. 单例挂载(App.vue / App.tsx)

在根组件挂载唯一实例,确保全局只有一个弹窗 DOM 节点:

<!-- Vue 3 -->
<template>
  <router-view />
  <GlobalConfirmDialog />
  <Toaster position="top-center" />
</template>
// React
function App() {
  return (
    <>
      <Router />
      <Toaster position="top-center" />
    </>
  );
}

2. 封装 useConfirm composable

// composables/useConfirm.ts
import { ref } from 'vue';

const isVisible = ref(false);
const message = ref('');
let resolvePromise: ((value: boolean) => void) | null = null;

export function useConfirm() {
  function confirm(msg: string): Promise<boolean> {
    message.value = msg;
    isVisible.value = true;
    return new Promise<boolean>((resolve) => {
      resolvePromise = resolve;
    });
  }

  function handleConfirm() {
    isVisible.value = false;
    resolvePromise?.(true);
  }

  function handleCancel() {
    isVisible.value = false;
    resolvePromise?.(false);
  }

  return { isVisible, message, confirm, handleConfirm, handleCancel };
}

3. 业务组件调用

<script setup>
import { useConfirm } from '@/composables/useConfirm';

const { confirm } = useConfirm();

async function handleDelete() {
  const ok = await confirm('确定要删除该记录吗?');
  if (ok) {
    await deleteRecord(id);
  }
}
</script>

4. Sonner / Toast 模式

import { toast } from 'sonner';

// 成功提示
toast.success('保存成功');

// 错误提示
toast.error('网络异常,请重试');

// Promise 模式
toast.promise(saveData(), {
  loading: '保存中...',
  success: '保存成功',
  error: '保存失败',
});

验证

  1. 在任意深层组件调用 confirm()toast.success(),确认弹窗/提示正常显示
  2. 打开 DevTools Elements 面板,确认全局只有一个弹窗 DOM 节点
  3. 快速连续调用,确认 Promise 各自独立 resolve,不会串扰
创建于 2026/2/21 更新于 2026/5/27