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 只在正确的页面显示
  • 支持支付宝小程序的兼容性处理
  • 使用 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. 在网络请求中使用

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 而烦恼,不妨试试这个方案!

相关资源


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

相关推荐
Danny_FD19 分钟前
Vue2 + Node.js 快速实现带心跳检测与自动重连的 WebSocket 案例
前端
uhakadotcom20 分钟前
将next.js的分享到twitter.com之中时,如何更新分享卡片上的图片?
前端·javascript·面试
韦小勇21 分钟前
el-table 父子数据层级嵌套表格
前端
奔赴_向往23 分钟前
为什么 PWA 至今没能「掘进」主流?
前端
小小愿望23 分钟前
微信小程序开发实战:图片转 Base64 全解析
前端·微信小程序
掘金安东尼26 分钟前
2分钟创建一个“不依赖任何外部库”的粒子动画背景
前端·面试·canvas
电商API大数据接口开发Cris26 分钟前
基于 Flink 的淘宝实时数据管道设计:商品详情流式处理与异构存储
前端·数据挖掘·api
小小愿望27 分钟前
解锁前端新技能:让JavaScript与CSS变量共舞
前端·javascript·css
程序员鱼皮30 分钟前
爆肝2月,我的 AI 代码生成平台上线了!
java·前端·编程·软件开发·项目
天生我材必有用_吴用43 分钟前
一文搞懂 useDark:Vue 项目中实现深色模式的正确姿势
前端·vue.js