uni Toast与Loading只能同时显示一个,uni.showLoaing会关闭toast。在一些场景下会导致toast一闪而过。
Loader方案
因为小程序App.vue是不支持写Template页面的,所以无法全局共享改组件,也没有document.bodyappend方式在使用的地方操作。
发现都得手动注入wxml页面,然后才能调用。如果需要脱离具体页面,在请求时需要弹出Toast(跟H5弹窗类似)相似的效果,这种方式行不通。
我的设想是通过插件,在编译阶段把组件自动注入到每个页面,这样就可以直接使用了。
在社区中发现有人实现了一种loader方案,接下来我们尝试接入项目:
使用 vue-inset-loader 全局注入页面根元素下,通过selectComponent获取组件实例,然后调用组件内方法即可:
安装 vue-inset-loader 插件
npm install vue-inset-loader --save-dev
配置 page.json 信息
"insetLoader": {
"config":{
"toast": "<Toast id="t-toast"></Toast>"
},
"label":["toast"],
"rootEle":"view"
},
vue.config.js配置
const path = require('path');
module.exports = {
configureWebpack: {
module: {
rules: [
{
test: /.vue$/,
use: {
// loader: 'vue-inset-loader', // 实现全局组件注册入页面
// // 针对Hbuilder工具创建的uni-app项目 https://github.com/1977474741/vue-inset-loader/blob/main/utils.js
loader: path.resolve(
__dirname,
'./node_modules/vue-inset-loader'
),
// // 支持自定义pages.json文件路径
options: {
pagesPath: path.resolve(__dirname, './pages.json'),
},
},
},
],
},
},
};
// why ? 解决 uni.showToast 与 uni.loading 不能不同使用的问题;
// 参照 vant t-design 结合 Toast https://github.com/youzan/vant-weapp/blob/dev/packages/toast/toast.ts
// 使用 vue-inset-loader 全局注入页面根元素下 <Toast id="t-toast"></Toast> 可以在 page.json中查看insetLoader属性配置
// 目前仅支持部分功能 icon mask type(success,fail,loading) 功能暂不支持
function isObj(x) {
const type = typeof x;
return x !== null && (type === 'object' || type === 'function');
}
function getContext() {
const pages = getCurrentPages();
return pages[pages.length - 1];
}
const defaultOptions = {
type: 'text',
mask: false,
message: '',
visible: true,
zIndex: 1000,
duration: 3000,
placement: 'middle',
selector: '#t-toast',
};
let queue = [];
let currentOptions = { ...defaultOptions };
function parseOptions(message) {
return isObj(message) ? message : { message };
}
function Toast(toastOptions) {
const options = {
...currentOptions,
...parseOptions(toastOptions),
};
const context =
(typeof options.context === 'function'
? options.context()
: options.context) || getContext();
const toast = context?.selectComponent?.(options.selector);
if (!toast) {
console.warn(
'未找到 van-toast 节点,请确认 selector 及 context 是否正确'
);
return;
}
delete options.context;
delete options.selector;
toast.clear = () => {
toast.setData({ visible: false });
if (options.onClose) {
options.onClose();
}
};
let onHide = context.onHide;
// eslint-disable-next-line no-unused-vars
context.onHide = function (...arg) {
// console.log('onHide#######');
onHide.apply(this, arg);
toast.clear();
context.onHide = onHide;
};
queue.push(toast);
toast.setData({
visible: options.visible,
duration: options.duration,
message: options.message,
placement: options.placement,
preventScrollThrough: options.preventScrollThrough,
theme: options.theme,
});
clearTimeout(toast.timer);
if (options.duration != null && options.duration > 0) {
toast.timer = setTimeout(() => {
console.log('toast clear timeout ');
toast.clear();
queue = queue.filter((item) => item !== toast);
}, options.duration);
}
return toast;
}
const createMethod = (type) => (options) =>
Toast({
type,
...parseOptions(options),
});
Toast.loading = createMethod('loading');
Toast.success = createMethod('success');
Toast.fail = createMethod('fail');
Toast.clear = () => {
queue.forEach((toast) => {
toast.clear();
});
queue = [];
};
Toast.setDefaultOptions = (options) => {
Object.assign(currentOptions, options);
};
Toast.resetDefaultOptions = () => {
currentOptions = { ...defaultOptions };
};
export default Toast;
<template>
<!-- 自定义toast组件 -->
<view v-if="visible" :class="classNames" :style="styles">
<view :class="contentClass">
<slot name="icon" />
<view aria-role="alert" :class="alertClass">{{ message }} </view>
<slot name="message" />
</view>
</view>
</template>
<script>
// eslint-disable-next-line no-unused-vars
import _ from './utils';
export default {
// props: {
// direction: {
// type: String,
// value: 'row',
// },
// /** 弹窗显示毫秒数 */
// duration: {
// type: Number,
// value: 2000,
// },
// /** 弹窗显示文字 */
// message: {
// type: String,
// },
// /** 弹窗展示位置 */
// placement: {
// type: String,
// value: 'middle',
// },
// /** 防止滚动穿透,即不允许点击和滚动 */
// preventScrollThrough: {
// type: Boolean,
// value: false,
// },
// /** 是否显示遮罩层 */
// showOverlay: {
// type: Boolean,
// value: false,
// },
// /** 提示类型 */
// theme: {
// type: String,
// },
// },
data() {
return {
prefix: 't',
classPrefix: 't-toast',
visible: false,
direction: 'row',
duration: 2000,
message: '',
placement: 'middle',
preventScrollThrough: false,
showOverlay: false,
theme: '',
};
},
computed: {
classNames() {
return `${_.cls(this.classPrefix, [
this.direction,
this.theme,
['with-text', this.message],
])} class ${this.prefix}-class`;
},
styles() {
return _._style([
'top:' +
(this.placement === 'top'
? '25%'
: this.placement === 'bottom'
? '75%'
: '45%'),
]);
},
contentClass() {
return `${this.classPrefix}__content ${this.classPrefix}__content--${this.direction}`;
},
alertClass() {
return `${this.classPrefix}__text ${this.classPrefix}__text--${this.direction}`;
},
},
methods: {
// show(options) {
// const props = options;
// const defaultOptions = {
// duration: props.duration,
// message: props.message,
// placement: props.placement,
// preventScrollThrough: props.preventScrollThrough,
// theme: props.theme,
// };
// const data = {
// ...defaultOptions,
// ...options,
// visible: true,
// };
// this.setData(data);
// },
// hide() {
// this.setData({ visible: false });
// this.data?.close?.();
// this.$emit('close');
// },
// clear() {
// this.hide();
// this.$emit('clear');
// },
},
};
</script>
注入全局组件
import Vue from 'vue';
import App from './App';
import Toast from '@/components/base/toast/BaseToast.vue';
Vue.config.productionTip = false;
Vue.component('Toast', Toast);
const app = new Vue({
store,
...App,
});
app.$mount();
使用示例:
import Toast from '@/components/base/toast/toast';
t1 = Toast('Hello Toast'); // 3s 后自动关闭
t2 = Toast({message: 'Hello Toast2', duration: 0}); // 需要手动关闭
t3 = Toast('Hello Toast3');
t4 = Toast('Hello Toast4');
setTmieout(() => {
// 手动关闭
t2.clear();
}, 5000)
// 关闭所有Toast
Toast.clear()
补充 2.22.1版本
2.22.1之后可以根据hideLoading noConflict属性取消混用特性;Toast Loading目前还是只能显示一个,即Loading,Toast会关闭另一个弹窗,但是HideLoading可以通过该设置避免意外关闭正在显示的Toast,这在一些异步场景下很有帮助。
补充组件 onPageHide
发现uniapp有微信小程序(pageLifetime)类似方法可以实现在组件内监听页面hide、show,所以可以直接在组件内使用 onPageHide去关闭Toast,就不用重写page.onHide了。