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
解决方案架构
我们的解决方案包含三个核心部分:
wd-toast组件 - 基于 provide/inject 的函数式调用- Layout 插件 - 实现一次插入,全局可用
useGlobalToast- 基于 Pinia 的状态管理
实现详解
1. wd-toast 组件实现
wot ui 是一个当下流行的 uni-app vue3 UI 库,作者也是其重要维护者之一。
首先,我们使用 wot-ui 的 Toast 组件,它基于 provide/inject 实现函数式调用:
<!-- 在组件中使用 -->
<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 插件实现统一布局管理:
<!-- 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 组件实现
<!-- 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 只在正确的页面显示 - 支持支付宝小程序的兼容性处理
- 使用
virtualHost和styleIsolation优化结构和样式
4. useGlobalToast - Pinia 状态管理
// 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. 在组件中使用
<script setup>
const globalToast = useGlobalToast()
function handleClick() {
globalToast.success('操作成功!')
globalToast.error('操作失败!')
globalToast.info('提示信息')
globalToast.warning('警告信息')
}
</script>2. 在网络请求中使用
// 请求拦截器
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. 在路由拦截中使用
// 路由守卫
function routeGuard(to: string) {
const globalToast = useGlobalToast()
if (!isLogin()) {
globalToast.warning('请先登录')
uni.navigateTo({ url: '/pages/login/index' })
return false
}
return true
}扩展功能
基于同样的架构,我们还实现了:
1. GlobalLoading - 全局加载提示
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 - 全局弹窗
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 的核心痛点,核心思路是:
- 状态驱动 - 用 Pinia 管理全局状态
- 布局注入 - 通过 Layout 插件实现一次插入、全局可用
- 组件响应 - 组件监听状态变化,调用底层 API
- 页面隔离 - 通过 currentPage 确保显示在正确页面
这套模式可以推广到所有需要全局调用的反馈组件,形成统一的全局反馈体系。
