thumbnail
在当今瞬息万变的软件开发领域中,项目规模如同滚雪球一般持续壮大,代码复杂度也随之节节攀升。面对这一挑战,解耦代码已然成为我们提升代码可维护性与可扩展性的重中之重。此刻,就让我们一同深入探寻如何巧妙借助发布订阅模式达成代码解耦的目标。
来自AI生成

洞悉发布订阅模式

发布订阅模式,通俗来讲,恰似广播电台与众多听众之间的互动模式。存在一个“发布者”角色,每当特定事件触发之际,它便义不容辞地向所有预先订阅该事件的“订阅者”发送通知。而订阅者们呢,事先主动向发布者登记在册,表明自身对特定事件饶有兴趣,如此一来,一旦这些事件被触发,它们便能即时接收相应消息,并迅速做出反应。

不妨以社交媒体平台为例,当用户分享了一条崭新动态,这一行为就相当于一个“发布”事件。而那些关注该用户的其他用户则扮演着“订阅者”的角色,他们满心期待能够第一时间知悉这条新动态,此时平台便会发挥桥梁作用,将动态精准推送给这些关注者,圆满完成整个发布订阅流程。

解耦代码的必要性

1、助力可维护性飞跃

当代码呈现高度耦合状态时,牵一发而动全身的困境便会频繁上演,对某一模块的细微修改,都极有可能在整个代码库中掀起轩然大波,使其变得脆弱不堪。反之,若实现了解耦,各个模块各司其职、职责清晰,修改其中一个模块时,对其他模块造成的影响微乎其微,维护工作自然变得轻松惬意。

2、赋能可扩展性升级

如今,新的功能诉求如潮水般不断涌来,倘若代码已然解耦,我们便能从容不迫地添加新的订阅者或发布者,而无需大动干戈地对已有代码进行大规模重构,轻松自如地应对业务的风云变幻。

代码实战:发布订阅模式的应用

import axios, { AxiosResponse } from 'axios';
import router from './router';
import { message } from 'ant-design-vue';
const ins = axios.create({
  baseURL: 'http://localhost:3000',
});

const successHandler = (res: AxiosResponse): any => {
  // 略
};

const errorHandler = (error: any): any => {
  if (error.response.status === 401) {
    message.error('登录失效,请重新登录');
    router.push('/login');
  }
};

ins.interceptors.response.use(successHandler, errorHandler);

代码审查与分析

我们来做一个简单的代码审查,看一下这段代码里边会潜藏着怎样的一个隐患。

这是一段非常常见的前端代码,就是给axios定义一个拦截器,一个成功的拦截器,一个失败的拦截器。先看一下这个失败的拦截器,当接口返回401的响应码时,弹出了一个登录过期的消息框,然后又去利用路由去跳转到了登录页。

逻辑分析与优化

按照逻辑来说,这是没有任何问题的,但是这样做好不好呢?不好,为什么呢?因为这个模块呢是在做网络请求的,不管什么拦截器,这些东西都是跟网络相关的。网络的模块里边怎么会出现组件呢?为什么会有组件库的依赖呢?网络里边为什么会有路由有依赖呢?这就很奇怪了,这样的依赖就造成了耦合,也就是网络跟我们的组件,也就是界面耦合了,以及呢跟我们的路由耦合了。

那这样的逻辑是会有什么问题?耦合带来的问题一定是你耦合了什么东西。那那个东西一变,你这里很有可能会跟着变化。这会导致一个诡异的现象,将来界面上需求变了,你要去动网络。比方说有一天界面的逻辑不仅仅要处理401,还要处理400,就是验证失败。续要弹出个消息表示提交表单的时候数据验证失败了,你还要去代码里加上一个判断。比方说400的时候你还要去谈一些消息,因为这个跟界面是关联的呀。那如果说存在的代理界面以后会变得更加复杂。将来有一天呢我们可能有些地方需要跳到登录页,有些地方不跳。它可能弹出一个层,让你在层上登录,而不是跳转路由。那这个时候呢你会发现这个问题更加麻烦了。你还要在这里去判断一下,你目前的路由是不是等于某一个特殊的界面。

它不仅跟界面关联,还跟某一个特殊的页面发生了关联,然后根据这个特殊的页面可能还要做不同的处理。那这个耦合以后会越越来越越来越直到后面你会发现这个模块的代码10%是在处理网络,90%是在处理路由,也就意味着你的工程将来很难维护。

如何处理

如果说耦合了,我要断开耦合怎么做呢?就是加中间层,没有什么是加中间层解决不了的,有的话就多加几个。我们可以这样加一个中间层,我们姑且把把它叫做事件中心。

现在的核心问题是一个模块发生的一些事情,我不知道该怎么处理。比如这里发生了401了,401过后我干嘛呢?我不知道,也不要去自作主张,这个事情该路由做做的路由去处理,该界面做的界面去处理,跟你没关系,你只需要扔出事件就可以了。

代码实现

Emitter.ts

const eventNames = ['API:UN_AUTH', 'API:INVALID'];
type EventNames = (typeof eventNames)[number];
class EventEmitter {
  private listeners: Record<string, Set<Function>> = {
    'API:UN_AUTH': new Set(),
    'API:INVALID': new Set(),
  };

  on(eventName: EventNames, listener: Function) {
    this.listeners[eventName].add(listener);
  }

  emit(eventName: EventNames,...args: any[]) {
    this.listeners[eventName].forEach((listener) => {
      listener(...args);
    });
  }
}

export default new EventEmitter();

这段代码定义了一个发布订阅模式的简单实现。

  • const eventNames = ['API:UN_AUTH', 'API:INVALID'];:定义了一个包含两个事件名称的数组eventNames。
  • type EventNames = (typeof eventNames)[number];:使用typeof和索引类型创建了一个新的类型EventNames,它表示eventNames数组中元素的类型(即'API:UN_AUTH'或'API:INVALID')。
  • class EventEmitter:定义了一个名为EventEmitter的类。
  • private listeners: Record> = {...};:在类中定义了一个私有属性listeners,它是一个对象,键是事件名称(字符串类型),值是一个Set,用于存储对应事件的回调函数(Function类型)。这里初始化了'API:UN_AUTH'和'API:INVALID'两个事件对应的空Set。
  • on(eventName: EventNames, listener: Function):这是一个方法,用于订阅事件。它接受一个事件名称(eventName,类型为EventNames)和一个回调函数(listener,类型为Function)作为参数,将回调函数添加到对应事件的Set中。
  • emit(eventName: EventNames,...args: any[]):这是一个方法,用于发布事件。它接受一个事件名称(eventName,类型为EventNames)和任意数量的参数(...args)。它遍历对应事件的回调函数Set,并使用forEach方法依次调用每个回调函数,将发布事件时传递的参数传递给回调函数。

request.ts

import axios, { AxiosResponse } from 'axios';
import emitter from './Emitter';
const ins = axios.create({
  baseURL: 'http://localhost:3000',
});

const successHandler = (res: AxiosResponse): any => {
  // 略
};

const errorHandler = (error: any): any => {
  if (error.response.status === 401) {
    emitter.emit('API:UN_AUTH')
  } else if(error.response.status === 400) {
    emitter.emit('API:INVALID')
  }
};

ins.interceptors.response.use(successHandler, errorHandler);

router.ts

import { createRouter, createWebHistory } from 'vue-router'
import emitter from './Emitter';

const router = createRouter({
  history: createWebHistory(),
  routes: []
})

emitter.on('API:UN_AUTH', () => {
  router.push('/login)
})

export default router;

像事件中心发出的通知,这件事发生了但是不知道该怎么处理。可以让其他的模块去监听相应的事件,你只需要发布事件就可以了。比如这个界面他去监听这个事件,就是告诉事件中心,将来有一天如果说这个权限不足或者是登录过期了,你告诉我一声,我这里要在界面上要做一个更新。

由界面去控制界面是最好的,那么路由也去监听这个事件,告诉这个事件中心如果说登录失效了,那么路由这一块可能要跳转到登录页,要做一些特殊处理。那么它本身就是路由去管路由的事,这样子就把它们之间的耦合断开了。