1. 背景及目的
1.1 需求背景
随着应用的拆分,目前子应用有12个,这些子应用都使用的是同一个request实例。
前端支持后端切流,增加多个拦截器用于灰度
经手动梳理:
目前所有应用中有26个在使用的拦截器,
其中用于灰度切流的拦截器有13个,
Request 16个
Response 10个
各个模块使用到的拦截器及是否可以下线的详情看所有应用中拦截器使用情况
1.2 现状分析 及 问题
根据背景我们发现目前 request为单实例,这个request实例在主应用创建并添加到window.APP_CONTEXT中暴露给子应用获取,所有应用使用的都是同一个request实例。
且 interceptor修改没有加限制,可能会造成下列的问题:
1.2.1 增加接口rtt耗时:
前端为了支持后端接口切流使用了20+个拦截器去做灰度处理,每个接口的请求,都要经过20+个拦截器逻辑,增加接口请求、响应耗时。
1.2.2 范围模糊:
这些拦截器有些写在主应用,有些写在子应用,即使接口只需要子模块范围内拦截的,也会作用到所有模块。
1.2.3 不可控:
随着应用的拆分,目前1个主应用,12个子应用使用的都是同一个request实例。
任一应用对request的修改会影响其他所有应用,且其中有部分应用是跨团队在维护,增加了request的不可控性。
1.3 需求目的
- 梳理现状
- 整理现有 interceptors,做好归类以及标志,为后续的解决方案提供参考材料
- 探索有效解决 Request-Intercepor 现有问题、可行性较高 且 改造成本低的优化方案
2. 整体设计
通过源码阅读得知:Interceptor 的绑定及执行逻辑
2.1 目前request绑定interceptor情况
2.2.1 主应用初始化过程中,生成Request实例之后
在main.js 加载时调用.
2.2.2 子应用初始化过程中
在lazy-module中调用拦截器注册函数
在initModule中MF提供的afterRegisteration(所有子模块注册后)与beforeRegisteration(所有子模块注册前)方法中调用.
2.2.3 子应用实际发起请求时
根据使用情况use拦截器
无论什么时机调用拦截器,影响的是挂载拦截器,拦截器生效时机,最终还是作用于全局。
2.2 目前的request实例可能会出现的interceptor使用情况
a.主应用内使用主应用内定义的interceptor
b.子应用调用子应用内部的interceptor
c.子应用调用主应用定义的interceptor
c.子应用间互相调用interceptor
2.3 目标: 优化后request使用情况
2.3.1 Request 实例独立
加载时机
主应用:主应用初始化,使用主应用生成request实例的方法,生成request实例。request实例挂载interceptor
子应用:进入子模块a,拿到只属于本模块requestA实例。requestA实例挂载interceptor.
卸载时机
无需卸载,离开子模块a,进入子模块b后,使用的是子模块b的拦截器。
2.3.2 Interceptor 独立
a. 主应用内使用主应用内定义的interceptor
b.子应用调用子应用内部的interceptor
c.子应用可以调用主应用共享出来的interceptor,私有的无法访问
d. 子应用间可以实现互相调用interceptor
3. 详细设计
3.1 思路一: 私有化request实例
- 提供生成request实例的方法,使各子应用只能基于主应用提供的方法生成自己应用内的实例。
- 子应用间request实例互不影响
- 子应用只能修改模块内部的interceptor
私有变量方式:提供生成request实例的方法,使各子应用只能基于主应用提供的方法生成自己应用内的实例
主应用:
import axios from 'axios';
// 通过内部变量
let _instance = null;
const baseURL = '';
const defaultConfig = {
baseURL,
};
class Request {
constructor() {
_instance = axios.create(defaultConfig);
// 全局 interceptor
_instance.interceptors.request.use(
() => {},
() => {}
);
_instance.interceptors.response.use(
() => {},
() => {}
);
}
async doRequest(config) {
console.log(config.method);
try {
const response = await _instance(config);
return response;
} catch (e) {
// error report
}
}
get() {
return this.doRequest({
method: 'get',
});
}
post() {
return this.doRequest({
method: 'post',
});
}
// 子模块通过主应用 Request 实例 提供的方法进行新的 Request 孵化
incubation(config) {
const request = axios.create({
...defaultConfig,
...config,
});
// 拷贝全局 interceptor
const baseRequestInterceptor = _instance.interceptors.request.handlers || [];
baseRequestInterceptor.forEach((i) => {
request.interceptors.request.use(i.fulfilled, i.rejected);
});
const baseResponseInterceptor = _instance.interceptors.response.handlers || [];
baseResponseInterceptor.forEach((i) => {
request.interceptors.response.use(i.fulfilled, i.rejected);
});
return request;
}
}
Request.getInstance = function () {
if (!this.instance) {
this.instance = new Request();
}
return this.instance;
};
export const request = Request.getInstance();
/*
* -----------
* expose function
* -----------
*/
export const NewRequest = Request.incubation;
子应用:
/*
* -----------
* call expose function NewRequest to get a new request instance
* -----------
*/
const request = getAppCtx().NewRequest();
- 子模块不能添加全局 interceptor,子应用只能修改模块内部的interceptor
删除现有expose到挂载在window下的APP_CONTEXT中的request实例,子应用无法直接操作主应用的request实例
基于以上思路 进行修改,经验证方法可行:
主应用页面中,子应用的interceptor没有执行。
子应用中,子应用中绑定的interceptor和主应用分享出来的interceptor被执行.
- 子应用间交叉依赖
由于是多实例,可能会出现子应用a会调用子应用b中的业务接口。若该业务接口在b中有拦截器对之进行处理,子应用a也需要使用该拦截器。
上面的修改无法兼容子应用间交叉依赖的场景。
为了达到子模块只能修改本模块的request interceptor,但又要互通,对此,可以考虑通过主应用生成对应模块的request,只派发该模块的request实例。通过map[module]=requestInstance的方式存储管理。
若子模块a需要使用子模块b的interceptor,则子应用告诉主应用需要哪个模块的request实例,由主应用派发模块b的request实例给模块a使用。如上图所示
子应用间交叉依赖的场景比较少见,且是不规范行为,此方案只为兼容特殊场景。
为兼容上述场景,将做下面的调整
- 基于当前已有的逻辑,通过主应用以私有变量方式,生成每个模块的实例。当模块修改其request实例时,主应用中的moduleRequest管理每个module对应的request实例
主应用:
Request.incubation = function (module) {
/*
* 当需要用到其他模块的reuqest时,直接返回已有的request实例
*/
if(this.moduleRequest[module]) return this.moduleRequest[module];
const mainRequest = Request.getInstance();
const instance = new Request();
const baseRequestInterceptor =
(mainRequest.axiosInstance.interceptors.request && mainRequest.axiosInstance.interceptors.request.handlers) || [];
baseRequestInterceptor.forEach((i) => {
instance.getAxiosInstance().interceptors.request.use(i.fulfilled, i.rejected);
});
const baseResponseInterceptor =
(mainRequest.axiosInstance.interceptors.response && mainRequest.axiosInstance.interceptors.response.handlers) || [];
baseResponseInterceptor.forEach((i) => {
instance.getAxiosInstance().interceptors.response.use(i.fulfilled, i.rejected);
});
this.moduleRequest[module] = instance;
return this.moduleRequest[module];
};
export const NewRequest = Request.incubation;
子应用使用
const request = getAppCtx().NewRequest('instation');
const requestOtherModule = (otherModule) => getAppCtx().NewRequest(otherModule);//业务中用到时才调用,防止获取时早于模块的初始化
- 当子模块正常使用时
request.get('/api/a-scope/...')
- 当子模块需要依赖其他模块时
requestOtherModule('b').get('/app-b/...')
3.2 思路二:共用request同一实例,通过来源分发拦截器
各域给request传入自己的config标识,主应用通过判断标识区分来源
- 通过interceptors.xx.use中提供的options runWhen + 获取子应用传入request的标识,判断是否需要绑定拦截器到request实例中。
然而 添加runWhen属性需要开发的自觉性,可以被跳过
且 项目中的axios版本没有升级,没有新加的synchronous与runWhen属性,若需要使用,要考虑升级版本.
项目中的axios包代码
- 通过来源加载、卸载拦截器
基于 ,目前已有可以获取当前页面来源于哪个模块的支持。
3.3 思路三:改造Axios
将axios执行拦截器的方式由推进队列改为用map对应key执行key中的拦截器。key为子应用标识。
方案权衡:
思路 | 优劣势 |
---|---|
思路一: 私有化request实例,子应用间request实例互不影响 | 优势:满足各域拦截器私有化问题满足各域request独立管理目的。劣势:多实例下,子应用间互相依赖时,使用者需要额外说明多实例风险较高,可能会存在没有意识到的问题 |
思路二:共用request同一实例,通过来源分发拦截器 | 优势: 满足各域拦截器私有化问题逻辑简单,修改起来方便。劣势:只解决了拦截器绑定的问题,没有解决request的独立管控问题,目前项目中的axios没有支持runWhen,需要查看依赖包版本 |
思路三:改造Axios | 优势:满足各域拦截器私有化问题 。劣势:改造成本较高不满足各域request独立管理目的 |