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

大家好,我是不如摸鱼去,wot-ui的主要维护者,欢迎来到我的 uni-app 分享专栏。

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

本文实现的全局 Toast、Loading、MessageBox 等组件均已在现代化 uni-app 模板,基于vitesse-uni-app的深度整合 Wot UI 组件库的快速起手项目 wot-demo 中提供,模板地址在文章末尾,作者是其主要维护者。

问题分析

传统方案的局限性

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

  1. uni.showToast() - 功能有限,样式单一,无法自定义
  2. 传统Toast - 只能在当前组件使用,无法跨组件调用
  3. 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 只在正确的页面显示
  • 支持支付宝小程序的兼容性处理
  • 使用 virtualHost 和 styleIsolation 优化结构和样式

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. 在网络请求中使用

typescript 复制代码
// 请求拦截器
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. 在路由拦截中使用

typescript 复制代码
// 路由守卫
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 的难题:

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

这个方案具有以下优势:

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

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

相关资源


如有问题或者更好的方案,欢迎评论区,交流讨论👇!欢迎点赞、关注、转发。

相关推荐
jay神4 分钟前
基于 YOLOv8 的森林火灾检测系统(Web 版)
前端·yolo
code2cat8 分钟前
HTML基础篇之第一张网页:从index.html到浏览器里的任务清单
前端·html
miss1 小时前
AI 改完不敢上线?OpenSpec 规约驱动开发实战
前端·代码规范·前端工程化
AiMan2 小时前
腾讯ima还是不可以导出左侧所有对话,只能单条导出,方法不对吗
前端
去伪存真2 小时前
并发量一高就装死?Agent 全链路“防猝死”手册
前端·agent
Moment2 小时前
AI 编程时代,聊聊我的 Mac mini 双屏桌面
前端·后端
AiMan2 小时前
先确认能不能一次把Microsofat Copilot所有内容打包一个文件导出
前端
总要学点什么2 小时前
第97篇:三维数据加载-城市建筑白模自定义着色器——让一条光带沿高度扫上去,快慢却由帧率说了算
前端·gis·webgl·cesium·着色器·空间分析·白模
上海懒图科技2 小时前
工厂厂区电子地图定位导航:厂区人车精准定位,安全合规双保障
前端·arcgis·vue
西安小哥3 小时前
端侧跑大模型,真正的门槛不在参数量:一次 596 秒到 10 秒的压缩路径复盘
前端·人工智能