Skip to content

uni-app 全局 Toast 实现

一、问题背景

在 uni-app 开发中,经常需要在任何地方(如网络请求拦截器、路由守卫等)显示 Toast 提示,但传统方案有以下局限性:

传统方案的局限性

方案优点缺点
uni.showToast()原生API,简单功能有限,样式单一,无法自定义
传统 Toast 组件可自定义样式只能在当前组件使用,无法跨组件调用
Wot UI 方案(provide/inject)函数式调用必须在 setup 顶层调用,无法在路由拦截和请求拦截中使用

核心难点

  1. uni-app 无法像 Vue 3 那样全局挂载组件
  2. 组件实例无法在非 Vue 上下文中访问
  3. 需要在网络请求和路由拦截中使用 Toast

二、解决方案架构

整体方案由三个核心部分组成:

┌─────────────────────────────────────────────────────────┐
│                   Pinia 状态管理层                        │
│              useGlobalToast (全局状态)                    │
│          - toastOptions (显示配置)                         │
│          - currentPage (当前页面)                         │
└────────────────────┬────────────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│                  Layout 插件层                           │
│         @uni-helper/vite-plugin-uni-layouts              │
│        (一次插入,全局可用)                              │
└────────────────────┬────────────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│                   组件渲染层                             │
│              GlobalToast + wd-toast                      │
│          (监听状态变化,调用 wd-toast API)                 │
└─────────────────────────────────────────────────────────┘

三、实现详解

1. Layout 插件 - 一次插入,全局可用

通过 @uni-helper/vite-plugin-uni-layouts 插件实现统一布局管理:

vue
<!-- src/layouts/default.vue -->
<template>
  <wd-config-provider :theme-vars="themeVars" :theme="theme">
    <slot />
    <!-- 全局组件一次性插入 -->
    <wd-notify />
    <wd-message-box />
    <wd-toast />
    <global-loading />
    <global-toast />
    <global-message />
  </wd-config-provider>
</template>

优点: 所有页面都会包含这些全局组件,实现"一次插入,全局可用"


2. useGlobalToast - Pinia 状态管理

typescript
// src/composables/useGlobalToast.ts
import { defineStore } from 'pinia'
import type { ToastOptions } from 'wot-design-uni/components/wd-toast/types'

interface GlobalToast {
  toastOptions: ToastOptions
  currentPage: string
}

const defaultOptions: ToastOptions = {
  duration: 2000,
  show: false,
}

export const useGlobalToast = defineStore('global-toast', {
  state: (): GlobalToast => ({
    toastOptions: defaultOptions,
    currentPage: '',
  }),
  actions: {
    // 显示 Toast
    show(option: ToastOptions | string) {
      this.currentPage = getCurrentPath()
      const options = CommonUtil.deepMerge(
        defaultOptions,
        typeof option === 'string' ? { msg: option } : option
      ) as ToastOptions

      this.toastOptions = CommonUtil.deepMerge(options, {
        show: true,
        position: options.position || 'middle',
      }) as ToastOptions
    },

    // 成功提示
    success(option: ToastOptions | string) {
      this.show(CommonUtil.deepMerge({
        iconName: 'success',
        duration: 1500,
      }, typeof option === 'string' ? { msg: option } : option) as ToastOptions)
    },

    // 错误提示
    error(option: ToastOptions | string) {
      this.show(CommonUtil.deepMerge({
        iconName: 'error',
        duration: 2000,
      }, typeof option === 'string' ? { msg: option } : option) as ToastOptions)
    },

    // 关闭 Toast
    close() {
      this.toastOptions.show = false
    },
  },
})

核心功能:

  • 提供统一的 API(show/success/error/close)
  • 维护全局状态(toastOptions、currentPage)
  • 支持字符串和对象两种调用方式

3. GlobalToast 组件实现

vue
<!-- src/components/GlobalToast.vue -->
<script lang="ts" setup>
const { toastOptions, currentPage } = storeToRefs(useGlobalToast())
const { close: closeGlobalToast } = useGlobalToast()

const toast = useToast('globalToast')
const currentPath = getCurrentPath()

// 支付宝小程序兼容性处理
// #ifdef MP-ALIPAY
const hackAlipayVisible = ref(false)
nextTick(() => {
  hackAlipayVisible.value = true
})
// #endif

// 监听全局状态变化
watch(() => toastOptions.value, (newVal) => {
  if (newVal && newVal.show) {
    // 只在当前页面显示 Toast
    if (currentPage.value === currentPath) {
      toast.show(toastOptions.value)
    }
  }
  else {
    toast.close()
  }
})
</script>

<template>
  <!-- 支付宝小程序特殊处理 -->
  <!-- #ifdef MP-ALIPAY -->
  <wd-toast v-if="hackAlipayVisible" selector="globalToast" :closed="closeGlobalToast" />
  <!-- #endif -->
  <!-- #ifndef MP-ALIPAY -->
  <wd-toast selector="globalToast" :closed="closeGlobalToast" />
  <!-- #endif -->
</template>

关键特性:

  • 通过 currentPage 确保 Toast 只在正确的页面显示
  • 支持支付宝小程序的兼容性处理
  • 使用 virtualHoststyleIsolation 优化结构和样式

四、使用方式

1. 在 Vue 组件中使用

vue
<script setup>
import { useGlobalToast } from '@/composables/useGlobalToast'

const toast = useGlobalToast()

const handleClick = () => {
  toast.show('这是一个全局 Toast')
  toast.success('操作成功')
  toast.error('操作失败')
}
</script>

2. 在网络请求拦截器中使用

typescript
// src/utils/request.ts
import { useGlobalToast } from '@/composables/useGlobalToast'

const toast = useGlobalToast()

axios.interceptors.response.use(
  response => response,
  error => {
    toast.error(error.message)
    return Promise.reject(error)
  }
)

3. 在路由守卫中使用

typescript
// src/router/guards.ts
import { useGlobalToast } from '@/composables/useGlobalToast'

const toast = useGlobalToast()

router.beforeEach((to, from, next) => {
  if (!checkAuth()) {
    toast.error('请先登录')
    next('/login')
  } else {
    next()
  }
})

五、设计亮点

亮点说明
分层架构状态层、布局层、组件层职责清晰
全局可用通过 Layout 插件一次插入,所有页面可用
非 Vue 上下文调用支持 Pinia 状态管理,可在拦截器中使用
页面隔离通过 currentPage 确保 Toast 只在正确页面显示
跨平台兼容支持支付宝小程序等平台的特殊处理
类型安全完整的 TypeScript 类型定义

六、可能的面试追问

Q1:为什么需要 currentPage 字段?

"uni-app 是单页应用,页面切换时组件不会重新挂载。如果不记录 currentPage,Toast 可能在错误的页面显示。通过对比 currentPage 和 currentPath,确保 Toast 只在触发时所在的页面显示。"


Q2:为什么不直接在 Pinia action 里调用 wd-toast 的 API?

"因为 wd-toast 的 useToast 必须在 Vue 组件的 setup 中调用(依赖 Vue 上下文),而 Pinia action 可以在任何地方调用。所以需要通过全局状态 + 组件监听的间接方式来实现。"


Q3:如果同时调用多个 Toast,怎么处理?

"当前实现是简单的覆盖逻辑,后调用的 Toast 会覆盖前面的。如果需要队列机制,可以在 Pinia state 中维护一个数组,组件中依次显示。不过实际业务中,Toast 覆盖是常见做法,因为同时显示多个提示用户体验并不好。"


Q4:性能如何?会不会频繁创建组件?

"不会。GlobalToast 组件是通过 Layout 插件在所有页面插入的,整个应用只有一个实例。watch 监听的是 ref 变化,性能开销很小。wd-toast 本身有防抖处理,频繁调用也不会造成问题。"


Q5:这个方案能否推广到 Loading、MessageBox 等组件?

"完全可以,这也是我在项目中实践的。Loading 和 MessageBox 的实现方式完全一致:

  • 在 Pinia 中维护全局状态
  • 创建对应的 Global 组件监听状态
  • 在 Layout 中统一插入

这样就形成了一套完整的全局反馈组件体系。"


七、相关技术栈

  • uni-app - 跨端开发框架
  • Vue 3 - 前端框架
  • Pinia - 状态管理
  • Wot Design Uni - UI 组件库
  • @uni-helper/vite-plugin-uni-layouts - Layout 插件
  • TypeScript - 类型系统

八、总结

这套方案解决了 uni-app 中全局 Toast 的核心痛点,核心思路是:

  1. 状态驱动 - 用 Pinia 管理全局状态
  2. 布局注入 - 通过 Layout 插件实现一次插入、全局可用
  3. 组件响应 - 组件监听状态变化,调用底层 API
  4. 页面隔离 - 通过 currentPage 确保显示在正确页面

这套模式可以推广到所有需要全局调用的反馈组件,形成统一的全局反馈体系。

Released under the MIT License.