🧑💻 寫在開頭
點贊 + 收藏 === 學會🤣🤣🤣
大家好,我是不如摸魚去,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 實現方式有以下幾種:
- 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 而煩惱,不妨試試這個方案!