博客 / 詳情

返回

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 實現函數式調用:

<!-- 在組件中使用 -->
<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 的難題:

  1. wd-toast 組件 提供了優秀的基礎能力
  2. @uni-helper/vite-plugin-uni-layouts 插件 實現了統一佈局和組件的全局插入
  3. Pinia 狀態管理 讓我們能在任何地方調用 Toast

這個方案具有以下優勢:

  • 真正的全局調用,可在任何地方使用
  • 完整的類型支持
  • 多端兼容性
  • 頁面隔離機制

如果你也在為 uni-app 的全局 Toast 而煩惱,不妨試試這個方案!

如果對您有所幫助,歡迎您點個關注,我會定時更新技術文檔,大家一起討論學習,一起進步。

user avatar hefty 頭像 axuicn 頭像 wadelau 頭像
3 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.