逻辑构思
最近在JD开发自研IDE的时候遇到个有趣的问题,因为项目面向iot的独特性,有一部分接口是针对不同甲方订制的,那么自然不同的甲方所需要的功能,提供的接口(比如加密sign和token签名)自然也是不同的,以往的 「请求根封装 + 接口列表 」 的方式难以满足其灵活多变的需求,在处理单项目多交付并行的情况时也捉襟见肘,故此重新设计了针对这类问题对于网络请求的黑箱化封装方式,无论vue还是react或是node环境都可以用。
首先我们先确认要达到的目标:整个项目关于网络部分应该变成配置式的,并且一个甲方对应一个配置文件,配置文件要能直接配置或处理不同的baseURL,headers等定制服务,除此之外还应当能够支持向后兼容老项目,并保留原来常见的接口封装模式,保持可读性和几乎不需要学习成本
常见的接口封装方式如下
export async function getTest(params) { // 这里是一个简单demo
const res = await request < ResponseParam > ({
url: '/getInfo/test',
method: 'POST',
data: params,
});
return res;
}
文件结构
设计出的文件结构大致如下
src -
| ...
|- config
|- serve.ts(serve核心)
|- serve
|-a.ts(甲方a)
|-b.ts(甲方b)
| ...
| ...
依照这个设计,如果之后有新的甲方需要交付,那么只需在config/serve文件夹下新增一个文件即可,既不会影响到其他交付,也便于统一配置。
Serve核心封装
为了更便于介绍代码逻辑和涉及的众多知识点,我会在这一章详细解释下是如何封装的,当然您可以边翻阅下一章的UML图边看这一章节来辅助理解(讲解在代码下面)
serve.ts
import request from '../xxxxxxx/request'; // 这里是项目的请求根路径
// 默认值,这里除了platform之外也可以定制项目公共的一些配置
const defaultConfig = {
platform: 'default',
configuration: 'independent' // independent 独立配置,overall 整体配置
}
/** 这两个interface实际上应该写在外面,文章中写在这里了便于观看*/
interface magicType { // 这里需要写的非常详细
_base_url: {
release: string,
dev: string,
test: string,
},
_header: {
sign: Function,
// ...
}
}
interface encType {
platform: string,
configuration: string
}
class ServeCore {
platform: string; // 平台标识
request: any; // 魔改请求根
[prop: string]: any;
constructor(ENC:encType = defaultConfig) {
this.platform = ENC.platform;
(async () => {
const export_list = await import(`./serve/${this.platform}`);
// [名称是 config] 整体配置
ENC.configuration === 'overall' && (this.request = this.magicRequest(export_list['config']));
for (let __function__ in export_list) {
// 魔改配置这里应该有两个判断规则?(按照名字区分,按照类型区分)
if (typeof export_list[__function__] === 'function' || __function__ !== 'config') {
// 这里是需要注册到租用与的接口函数,[是function类型 || 名称不是 config]
this[__function__] = export_list[__function__].bind({
request: this.request
})
} else {
// [既不是function类型,名称又叫 config] 独立配置
ENC.configuration === 'independent' &&
(this.request = this.magicRequest(export_list[__function__]));
}
}
console.log('========== this', this);
delete this.then;
return this
})();
}
// 魔改request的方法
magicRequest(config: magicType) {
// 这里的做法是把配置梆到request的全局
return request.bind({config})
}
}
export {
ServeCore, // 这里是用来重新自定义配置的
magicType,
encType
}
export default new ServeCore
首先我们定义了一个名叫 ServeCore 的 class,其中platform是平台标识(甲方),用来区分不同的平台;request是处理后的封装请求根,这个实际上是可以一直在变的,后文拓展会讲到。
按需加载文件
在构造函数中我们需要把目标甲方平台对应的文件按需动态引入到这个calss中并进行处理,但是很遗憾js中的class构造器目的是返回构造后的实例的,不支持等待异步操作(因为那样的话返回的将是一个promise对象,这里在运行时会报错),但幸运的是,我们知道js的class只是一个function的语法糖,底层还是function实现,所以我在构造函数中写了这样一个结构:
constructor(ENC:encType = defaultConfig) {
(async () => {
// ...
delete this.then;
return this
})();
}
目的是立即执行一个异步箭头函数作为返回值, 实际上返回的是一个Promise实例, 这个Promise在resolve时才会将创建的对象实例(this)返回, 于是我们外部的await就得到了完成异步构造后的实例。巧妙的绕过了constructor的机制
而接下来await import把不同甲方对应的文件引入进行处理,下面是举例的一个demo
a.ts(甲方a)
// 配置 应该放在第一个?
export const config = {
// TODO:当前需要讨论不过个人认为配置项应作为直接字面量,而不应该是一个函数
_base_url: {
release: 'https://xxx.com',
dev: 'https://xxx.com',
test: 'https://xxx.com',
},
_header: {
sign: (param) => { // 这里实际上的调用环境是在request中,所以是函数更好?
// ...
}
}
}
// 这里是一个简单demo
export async function getTest(params) {
const res = await request < ResponseParam > ({
url: '/getInfo/test',
method: 'POST',
data: params,
});
return res;
}
// ...
有事情先去忙了过会儿回来补一下,这里还有很多东西没写清楚
xxx.tsx
import Service from '../../config/serve'
// ...
// 某个函数内:
const res = await Service.getTest(param);
console.log(res)
// 这里ide会报错但其实可以正常运行的
图形化理解(如果有兴趣可以搞一份思维导图)
UML图如下
相关链接
参考文献:https://www.blackglory.me/async-constructor/