Skip to content

uni-app 无法实现全局 Toast?这个方法做到了!

在 uni-app 开发中,我们经常遇到需要在任何地方(如网络请求拦截器、路由守卫等)显示 Toast 提示的需求。然而,uni-app 的组件化架构使得全局 Toast 的实现变得复杂。本文将介绍一套完整的解决方案,让你轻松实现真正的全局 Toast。

问题分析

传统方案的局限性

在 uni-app 中,常见的 Toast 实现方式有以下几种:

  • uni.showToast() - 功能有限,样式单一,无法自定义
  • 传统Toast - 只能在当前组件使用,无法跨组件调用
  • wot ui 方案 - 基于provide/inject实现,需要在 setup 顶层调用useToast,无法在路由拦截和请求拦截中使用

核心难点

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

解决方案架构

我们的解决方案包含三个核心部分:

  1. wd-toast 组件 - 基于 provide/inject 的函数式调用
  2. Layout 插件 - 实现一次插入,全局可用
  3. useGlobalToast - 基于 Pinia 的状态管理

实现详解

1. wd-toast 组件实现

wot ui 是一个当下流行的 uni-app vue3 UI 库,作者也是其重要维护者之一。

首先,我们使用 wot-ui 的 Toast 组件,它基于 provide/inject 实现函数式调用:

vue
<!-- 在组件中使用 -->
<script setup>
const toast = useToast('myToast')

// 显示 Toast
toast.show({
  msg: '这是一个提示',
  duration: 2000
})
</script>

<template>
  <wd-toast selector="myToast" />
</template>

优点: 函数式调用,使用简单 缺点useToast 必须在 setup 顶层调用,toast.show 仅能在vue组件中使用

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

由于 uni-app 无法全局插入组件,我们通过 @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>

这样,所有页面都会包含这些全局组件,实现了“一次插入,全局可用”的效果。

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 优化结构和样式

4. 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',
        direction: 'vertical',
      }, typeof option === 'string' ? { msg: option } : option) as ToastOptions)
    },

    // 信息提示
    info(option: ToastOptions | string) {
      this.show(CommonUtil.deepMerge({
        iconName: 'info',
      }, typeof option === 'string' ? { msg: option } : option) as ToastOptions)
    },

    // 警告提示
    warning(option: ToastOptions | string) {
      this.show(CommonUtil.deepMerge({
        iconName: 'warning',
      }, typeof option === 'string' ? { msg: option } : option) as ToastOptions)
    },

    // 关闭 Toast
    close() {
      this.toastOptions = defaultOptions
      this.currentPage = ''
    },
  },
})

使用方式

1. 在组件中使用

vue
<script setup>
const globalToast = useGlobalToast()

function handleClick() {
  globalToast.success('操作成功!')
  globalToast.error('操作失败!')
  globalToast.info('提示信息')
  globalToast.warning('警告信息')
}
</script>

2. 在网络请求中使用

javascript
// 请求拦截器
uni.addInterceptor('request', {
  fail(err) {
    const globalToast = useGlobalToast()
    globalToast.error('网络请求失败')
  }
})

// 或在 API 封装中
export async function apiRequest(url: string, data: any) {
  try {
    const result = await uni.request({ url, data })
    return result
  }
  catch (error) {
    const globalToast = useGlobalToast()
    globalToast.error('请求失败,请重试')
    throw error
  }
}

3. 在路由拦截中使用

javascript
// 路由守卫
function routeGuard(to: string) {
  const globalToast = useGlobalToast()

  if (!isLogin()) {
    globalToast.warning('请先登录')
    uni.navigateTo({ url: '/pages/login/index' })
    return false
  }
  return true
}

扩展功能

基于同样的架构,我们还实现了:

1. GlobalLoading - 全局加载提示

typescript
export const useGlobalLoading = defineStore('global-loading', {
  actions: {
    loading(option: ToastOptions | string) {
      this.currentPage = getCurrentPath()
      this.loadingOptions = CommonUtil.deepMerge({
        iconName: 'loading',
        duration: 0,
        cover: true,
        position: 'middle',
        show: true,
      }, typeof option === 'string' ? { msg: option } : option)
    }
  }
})

2. GlobalMessage - 全局弹窗

typescript
export const useGlobalMessage = defineStore('global-message', {
  actions: {
    alert(option: GlobalMessageOptions | string) {
      const messageOptions = CommonUtil.deepMerge(
        { type: 'alert' },
        CommonUtil.isString(option) ? { title: option } : option
      )
      messageOptions.showCancelButton = false
      this.show(messageOptions)
    }
  }
})

总结

通过这套方案,我们成功解决了 uni-app 全局 Toast 的难题:

  • wd-toast 组件 提供了优秀的基础能力
  • @uni-helper/vite-plugin-uni-layouts 插件 实现了统一布局和组件的全局插入
  • Pinia 状态管理 让我们能在任何地方调用 Toast

这个方案具有以下优势:

  • 真正的全局调用,可在任何地方使用
  • 完整的类型支持
  • 多端兼容性
  • 页面隔离机制

如果你也在为 uni-app 的全局 Toast 而烦恼,不妨试试这个方案!

六、可能的面试追问

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 确保显示在正确页面

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