前端小纠结–Event Bus模块约定
本文只是给一个针对某一类问题的处理思路。
背景
标题有点大,本文产生的背景是,因为在封装Axios的时候,对于服务端的异常或者需要根据业务上异常code,进行不同的处理逻辑。但是基于Axios做的封装作为跨项目、跨框架模块使用所以不能和具体的router或者store等模块耦合,所以使用Event Bus,在整个Web范围中来解耦各个组件。
为什么使用Event Bus?
Event Bus有特殊的使用场景,不止在view组件之间的通信使用;Event Bus设计作为整个SPA应用的事件(消息)投递层使用。
模块通信
解决模块之间的通信问题,view组件层面,父子组件、兄弟组件通信都可以使用event bus处理。
 
模块解耦
storage change事件,cookie change事件,view组件的事件等,全部转换
使用Event Bus来订阅和发布,这样就统一了整个应用不同模块之间的通信接口问题。
 
父子页面通信
window.postMessage + Event Bus
 
多页面通信
storage change + Event Bus
 
Event Bus模块封装
Event Bus接口设计
参考jQuery和Vue
方法: on, off, once, publish(可选trigger/dispatch/emit)
BusEvent元数据模型设计
参考DOM中的Event对象
1 2 3 4 5 6 7 8 9 10 11 12
   |  export interface ResponseResult<T = any> {   message: string;   code: number;   data: T; }
  export interface BusEvent<T = any> extends ResponseResult<T> {   type: string; }
  export type BusEventHandler = (data: BusEvent) => any;
 
  | 
 
这里的message是参考Error来设计的,因为当我们程序异常或者业务上异常时,就可以统一直接使用new Error()进行处理。
封装实现
项目中实现选择一种即可。
基于Vue封装实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
   | import EventBus from 'dynamic-vue-bus'; import { isFunction } from 'lodash-es';
 
  export const VueBus = {   originBus: EventBus,   on(topic: string | string[], handler: BusEventHandler): string[] {          this.originBus.$on(topic, handler)     return [];   },
    off(topic: string | string[], handler?: any) {     const length = arguments.length;     switch (length) {       case 1:         this.originBus.$off(topic);         break;       case 2:         this.originBus.$off(topic, handler);         break;       default:         this.originBus.$off();     }   },
    once(topic: string, handler: BusEventHandler) {          return this.originBus.$once(topic, handler);   },
    publish(topic: string, data: BusEvent) {     return this.originBus.$emit(topic, data);   }, };
   | 
 
使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
   | function wsHandler(evt: BusEvent) {     console.log(evt); } const topic = 'ws.100021'; VueBus.on(topic, wsHandler);
 
  VueBus.publish(topic, {    code: 100021,    type: 'ws',    message: '',    data: {         result: [{             id: 1,             name: 'junna'         }]     } });
 
  VueBus.off(topic);
 
 
  const eventBus = PubSubBus.originBus;
  | 
 
基于PubSubJS实现
以下代码并没测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
   |  export const PubSubBus = {   originBus: PubSub,   on(topic: string | string[], handler: BusEventHandler): string[] {     if (!topic) {       return [];     }          let events: string[] = [].concat(topic);     return events.map(evt =>       PubSub.subscribe(evt, () => (topic: string, data: BusEvent) =>         handler(data)       )     );   },
    
 
 
    off(tokenOrHandler?: () => void | string | string[]) {     let length = arguments.length,       evts: string[],       listener;     if (length === 0) {       PubSub.clearAllSubscriptions();     } else {              if (isFunction(tokenOrHandler)) {         PubSub.unsubscribe(listener);       } else {                  evts = [].concat(tokenOrHandler);         evts.forEach(evt => PubSub.unsubscribe(evt));       }     }   },
    once(topic: string, handler: BusEventHandler) {          return PubSub.subscribeOnce(topic, handler);   },
    publish(topic: string, data: BusEvent, sync = false) {     return sync ? PubSub.publishSync(topic, data) : PubSub.publish(topic, data);   }, };
 
  | 
 
使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
   | function wsHandler(evt: BusEvent) {     console.log(evt); } const topic = 'ws.100021'; const tokens: string[] = PubSubBus.on(topic, wsHandler);
 
  PubSubBus.publish(topic, {    code: 100021,    type: 'ws',    message: '',    data: {         result: [{             id: 1,             name: 'junna'         }]     } });
 
  PubSubBus.off(topic);
 
 
 
  const PubSub = PubSubBus.originBus;
  | 
 
Event相关约定
BusEvent#type类型约定
事件类型的约定参考:
ws: WebSocket事件 
- storage: 储存事件, 
sessionStorage, localStorage 
- cs: cache storage
 
ap: application cache 
- cookie: cookie事件
 
- biz: 业务事件
 
- system: 系统事件
 
ui: 界面 
cmp: components事件 
BusEvent#code范围约定
code范围约定只是参考,因为这个约定需要和服务端小伙伴,甚至系统设计时规划决定。
(瞎写)
ws: 10000~19999 
- storage: 20000~29999, 
sessionStorage: 22000~22999, localStorage: 23000~23999 
- cs: 24000~24999
 
ap: 25000~25999 
- cookie: 26000~26999
 
- biz: 30000~31000
 
- system: 40000~41000
 
ui: 42000~42999 
cmp: 43000~44999 
统一处理EventBus事件
有了约定,就可以统一发布相关的事件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
   |  const data = [{    code: 100021,    type: 'ws',    message: '',    data: {         result: [{             id: 1,             name: 'junna'         }]     } },{    code: 100022,    type: 'ws',    message: '',    data: {         result: [{             id: 1,             name: 'junna'         }]     } }];
  data.forEach(event => PubSubBus.publish(`${event.type}.${event.code}`), event));
 
  | 
 
总结
通过对Event Bus的统一封装,对外提供统一的接口,统一整个SPA事件系统(非DOM层面),完成了模块之间的解耦。
缺点:
- 基于
Vue封装实现的不支持namespace 
- 基于
Vue封装实现和PubSubJS接口参数和返回值有差异,所以选择一种即可 
参考
global-event-bus
PubSubJS
dynamic-vue-bus
让在Vue中使用的EventBus也有生命周期
Web Storage Support Test
Working with quota on mobile browsers
Browser Storage Abuser
BrowserStorageAbuser github