前言
Vue项目中的组件通信方式,绝大多数的情况是可以被Vuex等方案代替的,但有一些特殊情况却非常适合使用EventBus,举个简单的例子:有A、B两个组件,用户在A上进行操作后,需要B执行某些逻辑。
由于Vue3中删除了$on、$once、$off等方法,官方推荐使用 mitt.js 替代已经移除的EventBus。
封装原因
1.若项目中大量使用mitt进行组件通信,容易造成数据混乱,以及逻辑分散,出现问题很难定位、溯源。基于以上原因,对mitt配置logger,达到操作显化的效果。
2.若emit抛出一个事件后,需要等待on捕捉事件并返回完结状态后(也就是异步操作),再执行相关逻辑,因此需要扩展一个emitAsync方法。举例:在组件A中点击按钮,通知组件B中的modal弹出给予用户进行二次确认,用户点击确认 => 组件A执行确认操作,用户点击取消 => 组件A执行取消操作
具体代码
import mitt from 'mitt'
// 当前文件名 需要同步修改
const CURRENT_FILE_NAME = 'useMitt'
// 基础配置
const defineOptions = {
// 是否禁用
logDisabled: false,
// 是否展开
logExpanded: false,
// console.group样式
logStyle: 'color: #fff; background: #51a2e4; font-size: 12px; padding: 4px; border-radius: 4px'
}
// uuid
let idCounter = 0;
const uniqueId = (prefix) => {
return prefix.toString() + ++idCounter;
}
// 操作时间 HH:mm:ss:ms
const formatTime = (date = new Date()) => {
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
const seconds = date.getSeconds().toString().padStart(2, '0');
const milliseconds = date.getMilliseconds().toString();
return `${hours}:${minutes}:${seconds}:${milliseconds}`;
}
// 获取堆栈
const getStackTrace = () => {
let stackStr = ''
let stackArr = []
// 通过Error获取
try {
throw new Error('');
}
// catch掉才不会被博睿监控到
catch (error) {
stackStr = error.stack || '';
}
// 字符串信息分割为数组 并去掉空格
stackArr = stackStr.split('\n').map((line) => line.trim());
return stackArr.splice(stackArr[0] === 'Error' ? 2 : 1);
}
// 在堆栈信息数组中 匹配触发事件的函数
const matchFnCaller = (stackTrace = []) => {
// 文件在src下的 且排除本文件 没有匹配的返回第0项
return stackTrace.find(item => item.includes('src') && !item.includes(CURRENT_FILE_NAME)) || stackTrace[0]
}
// 事件信息map结构 key: eventName value: { count: 触发次数, describe: 事件描述 }
const eventMap = new Map()
// 监听事件 配置logger
const handleOnEvent = (eventName, value, options) => {
const stackTrace = getStackTrace()
// 匹配事件源 去掉 at 字符
const source = matchFnCaller(stackTrace).substring(3)
const describe = eventMap.get(eventName).describe
const count = eventMap.get(eventName).count
const g = options.logExpanded ? console.group : console.groupCollapsed
g(`%cmittId => ${options.id}`, options.logStyle)
console.log('eventName:', eventName)
console.log('eventValue:', value)
console.log('eventDescribe:', describe)
console.log('eventSource:', source)
console.log('count:', count)
console.log('time:', formatTime())
console.groupEnd()
}
// 记录事件信息
const markEventMap = (type, describe) => {
if (eventMap.has(type)) {
let event = eventMap.get(type)
eventMap.set(type, {
count: ++event.count,
describe
})
} else {
eventMap.set(type, {
count: 1,
describe
})
}
console.log('eventMap', eventMap)
}
// emit事件 记录事件信息
const handleEmitEvent = (emitter) => {
const originEmit = emitter.emit
emitter.emit = (type, e, describe = '') => {
markEventMap(type, describe)
originEmit(type, e)
}
}
// 增加emitAsync方法
// https://github.com/developit/mitt/discussions/157
const emitAsync = async function(type, e, describe) {
// 记录事件信息
markEventMap(type, describe)
let handlers = this.all.get(type)
if (handlers) {
for (const f of handlers) {
await f(e)
}
}
handlers = this.all.get('*')
if (handlers) {
for (const f of handlers) {
await f(type, e)
}
}
}
const mittAsync = (all) => {
const instance = mitt(all)
instance.emitAsync = emitAsync
return instance
}
export default function useMitt(customOption) {
// 配置项
const options = Object.assign({
id: uniqueId('mitt-')
}, defineOptions, customOption)
// 实例化mitt
const emitter = mittAsync()
if (!options.disabled) {
// 监听所有事件,配置logger
emitter.on('*', (eventName, value) => handleOnEvent(eventName, value, options))
// 劫持emit,记录信息
handleEmitEvent(emitter)
}
return emitter
}
emitAsync异步调用
import useMitt from '@/hooks/useMitt'
const emitter = useMitt({id: 'listEmitter', logExpanded: true})
const handleClick = () => {
emitter.emitAsync('showModal', 'data', '此函数是激活confirm modal')
.then(() => {
console.log('confirm') // resolve后打印
})
.catch(() => {
console.log('cancel') // reject后打印
})
}
emitter.on('showModal', () => {
return new Promise((resolve, reject) => {
Modal.confirm({
title: '确认是否XXX',
onOk() {
resolve()
},
onCancel() {
reject()
}
})
})
})
logger效果
import useMitt from '@/hooks/useMitt'
const emitter = useMitt({ id: contract-edit, logExpanded: true })
const handleClick = () => {
emitter.emit('click', 'data', '这是一个点击事件')
}
点击eventSource中的链接,定位到触发位置
注意
需要将实例化的emitter通过provide或其他方式传给其他组件,才能相互通信。
也可以自行改造,加上单例模式解决上述问题