Back

如何在 Vue 中实现 Toast 通知

如何在 Vue 中实现 Toast 通知

你需要向用户展示反馈信息——表单已保存、API 调用失败、上传成功。Toast 通知优雅地解决了这个问题:简短的消息出现、传递信息,然后消失,不会中断工作流程。

本指南涵盖了 Vue 3 toast 通知的两种方法:构建基于 composable 的自定义系统,以及集成成熟的库如 Vue Toastification 或 Notivue。两种方法都使用现代 Composition API 模式和 <script setup> 语法。

核心要点

  • Vue 3 中的自定义 toast 系统只需要三个部分:共享响应式状态、composable 函数和基于 Teleport 的容器组件。
  • Vue Toastification 和 Notivue 等第三方库开箱即用地处理队列管理、动画、无障碍访问和主题。
  • 在 Nuxt 3 中,toast 通知需要仅在客户端执行——使用 <ClientOnly> 包装器或 .client.ts 插件后缀。
  • 无障碍的 toast 使用 aria-live 区域、带有清晰标签的关闭按钮,并尊重 prefers-reduced-motion 偏好设置。

使用 Vue Composables 构建自定义 Toast 系统

一个最小化的 Vue composable 通知系统需要三个部分:响应式状态、composable 函数和使用 Teleport 的渲染组件。

Toast Composable

创建一个任何组件都可以访问的共享响应式存储:

// composables/useToast.ts
import { ref, readonly } from 'vue'

interface Toast {
  id: number
  message: string
  type: 'success' | 'error' | 'info' | 'warning'
}

const toasts = ref<Toast[]>([])
let id = 0

export function useToast() {
  const add = (message: string, type: Toast['type'] = 'info', duration = 3000) => {
    const toast = { id: ++id, message, type }
    toasts.value.push(toast)

    if (duration > 0) {
      setTimeout(() => remove(toast.id), duration)
    }
  }

  const remove = (toastId: number) => {
    toasts.value = toasts.value.filter(t => t.id !== toastId)
  }

  return {
    toasts: readonly(toasts),
    success: (msg: string) => add(msg, 'success'),
    error: (msg: string) => add(msg, 'error'),
    info: (msg: string) => add(msg, 'info'),
    warning: (msg: string) => add(msg, 'warning'),
    remove
  }
}

toasts ref 声明在 composable 函数外部,这样每次调用 useToast() 都共享同一个响应式数组。readonly 包装器防止使用者直接修改状态——只有 addremove 可以修改列表。

Toast 容器组件

使用 Teleport 渲染 toast,将它们定位在组件树之外:

<!-- components/ToastContainer.vue -->
<script setup lang="ts">
import { useToast } from '@/composables/useToast'

const { toasts, remove } = useToast()
</script>

<template>
  <Teleport to="body">
    <div
      class="toast-container"
      role="region"
      aria-live="polite"
      aria-label="Notifications"
    >
      <div
        v-for="toast in toasts"
        :key="toast.id"
        :class="['toast', `toast--${toast.type}`]"
        role="status"
      >
        {{ toast.message }}
        <button @click="remove(toast.id)" aria-label="Dismiss notification">×</button>
      </div>
    </div>
  </Teleport>
</template>

<ToastContainer /> 放在你的 App.vue 中,然后从任何组件调用 useToast() 来触发通知。

关于 ARIA 角色的说明: 容器使用 aria-live="polite",这样屏幕阅读器会在不打断用户的情况下宣布新的 toast。单个 toast 使用 role="status" 而不是 role="alert",这与 polite 实时区域正确配对。将 role="alert"(隐含 aria-live="assertive")保留给需要立即关注的紧急错误消息。

使用第三方库

对于生产应用程序,维护良好的库可以处理你原本需要自己构建的边缘情况:队列管理、动画、无障碍访问和主题。

Vue Toastification 设置

安装并注册插件:

// main.ts
import { createApp } from 'vue'
import Toast from 'vue-toastification'
import 'vue-toastification/dist/index.css'
import App from './App.vue'

const app = createApp(App)
app.use(Toast, {
  position: 'top-right',
  timeout: 5000
})
app.mount('#app')

在组件中使用 composable:

<script setup>
import { useToast } from 'vue-toastification'

const toast = useToast()

const handleSave = async () => {
  try {
    await saveData()
    toast.success('Changes saved')
  } catch {
    toast.error('Save failed')
  }
}
</script>

Notivue 替代方案

Notivue 提供类似的 composable API,具有额外的自定义选项和内置的无障碍访问功能。

Nuxt 3 中的 Toast 通知

Nuxt 3 中的 toast 通知必须在客户端上下文中触发(例如,在事件处理器或 onMounted 中),因为它们操作 DOM,不应在服务器端渲染期间运行:

<!-- app.vue -->
<template>
  <NuxtPage />
  <ClientOnly>
    <ToastContainer />
  </ClientOnly>
</template>

如果你使用 Nuxt UI,它提供了自己的 useToast composable——无需额外的库。对于其他设置,创建一个 Nuxt 插件:

// plugins/toast.client.ts
import Toast from 'vue-toastification'
import 'vue-toastification/dist/index.css'

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.use(Toast)
})

.client.ts 后缀确保插件仅在浏览器中运行。

无障碍访问注意事项

Toast 必须对屏幕阅读器用户可访问:

  • 在 toast 容器上使用 aria-live="polite",这样新消息会在不中断的情况下被宣布
  • 提供带有描述性 aria-label 属性的关闭按钮
  • 通过在请求时禁用动画来尊重 prefers-reduced-motion
  • 避免自动关闭错误消息——用户需要时间阅读并对其采取行动
@media (prefers-reduced-motion: reduce) {
  .toast {
    animation: none
  }
}

结论

当你需要最小的打包体积或高度特定的行为时,构建自定义 toast 系统。当你想要经过验证的无障碍访问、动画和配置选项而无需维护开销时,使用 Vue Toastification 或 Notivue。

两种方法都适用于 Vue 3 和 Nuxt 3。composable 模式使你的代码可测试,并使你的组件与通知逻辑解耦。从自定义 composable 开始以理解机制,然后评估库是否更好地满足项目的长期需求。

常见问题

可以。因为 toasts ref 在模块级别的 useToast 函数外部声明,所以每个调用 useToast 的组件都共享相同的响应式状态。这意味着任何组件都可以触发或关闭 toast,而无需通过父组件传递属性或发出事件。

Teleport 将 toast DOM 节点移动到 body 元素,在组件层次结构之外。这可以防止父级 CSS(如 overflow hidden 或 z-index 堆叠上下文)裁剪或隐藏你的 toast。它还使 toast 定位保持一致,无论容器组件挂载在哪里。

在自定义 composable 中,将零或负数作为 duration 参数传递以跳过 setTimeout 调用。使用 Vue Toastification 时,为特定 toast 将 timeout 设置为 false。错误消息应该持续存在,直到用户手动关闭它们,这样他们有足够的时间阅读和响应。

需要。Toast 通知操作 DOM,而 DOM 在服务器端渲染期间不可用。将你的 toast 容器包装在 ClientOnly 组件中,或使用 .client.ts 文件后缀将 toast 库注册为仅客户端的 Nuxt 插件。这确保 toast 逻辑仅在浏览器中执行。

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay