// Koa中间件引擎源码
function compose(middlewares = []) {
if (!Array.isArray(middlewares))
throw new TypeError('Middleware stack must be an array!');
for (const fn of middlewares) {
if (typeof fn !== 'function')
throw new TypeError('Middleware must be composed of functions!');
}
const { length } = middlewares;
return function callback(ctx, next) {
let index = -1;
function dispatch(i) {
let fn = middlewares[i];
if (i <= index)
return Promise.reject(new Error('next() called multiple times'));
index = i;
if (i === length) {
fn = next;
}
if (!fn) {
return Promise.resolve();
}
try {
return Promise.resolve(fn(ctx, dispatch.bind(null, i + 1)));
} catch (error) {
return Promise.reject(error);
}
}
return dispatch(0);
};
}
Fetch
概念(摘自 MDN): Fetch 的核心在于对 HTTP 接口的抽象,包括 Request,Response,Headers,Body,以及用于初始化异步请求的 global fetch。得益于 JavaScript 实现的这些抽象好的 HTTP 模块,其他接口能够很方便的使用这些功能。听着咋感觉有点像浏览器端的 Koa(joke)。但是使用上对我们业务编写还是不那么友好,虽然相较于 ajax,已经好太多。下面展示两个用 fetch 发送 get 请求和 post 请求的代码示例:
洋葱模型
学过或了解过 Node 服务框架 Koa 的,都或许听过洋葱模型和中间件。恩,就是吃的那个洋葱,见下图:
Koa 是通过洋葱模型实现对 http 封装,中间件就是一层一层的洋葱,这里推荐两个 Koa 源码解读的文章,当然其源码本身也很简单,可读性非常高。
我这里不过多讲关于 Koa 的设计模式与源码,理解 Koa 的中间件引擎源码就行了。写这篇文章的目的,是整理出我参照 Koa 设计一个 Http 构造类的思路,此构造类用于简化及规范日常浏览器端请求的书写:
Fetch
概念(摘自 MDN): Fetch 的核心在于对 HTTP 接口的抽象,包括 Request,Response,Headers,Body,以及用于初始化异步请求的 global fetch。得益于 JavaScript 实现的这些抽象好的 HTTP 模块,其他接口能够很方便的使用这些功能。听着咋感觉有点像浏览器端的 Koa(joke)。但是使用上对我们业务编写还是不那么友好,虽然相较于 ajax,已经好太多。下面展示两个用 fetch 发送 get 请求和 post 请求的代码示例:
从上面的示例,我们可以感觉到,每一个请求发起,都需要用完整的 url,遇到 post 请求,设置 Request Header 是一个比较大的工作,接收响应都需要判断 respones.ok 是否为 true(如果不清楚,请参见 mdn 链接),然后 response.json()得到返回值,有可能返回值中还包含了 status 与 message,所以要拿到最终的内容,我们还得多码两行代码。如果某一天,我们需要为每个请求加上凭证或版本号,那代码更改量将直接 Double, 所以希望设计一个基于 fetch 封装的,支持中间件的 Http 构造类来简化规范日常前后端的交互,比如像下面这样:
上面的代码,看起来是不是更直观,明了。
设计分析
从上面的分析,这个 Http 构造类需要包含以下特点:
Talk is Cheap
Http类
参照上面的理想化示例,首先尝试去实现 Http.create:
直接贴代码,也是一种无赖之举。每个方法功能都非常简单,但从use和_middlewareInit方法, 可以看出和koa的中间件有所区别,这里采用的中间件是一种尾触发方式(中间件按事先排好的顺序调用),在后面会进一步体现。
requestMethods
关于requestMethods,其类似于一种策略模式,这里将每一种请示类型,抽象成一个具体的策略,在实例化某个服务的请求时,将得到一系列策略,将resetful语义函数化:
Instance类
关于Instance, 每个实例的服务域名是一致的,所以其作用更多是每个服务创建一个执行上下文,用于存储request, response, 并做错误处理, 实现也非常简单:
关于Object.assign创建ctx, 是为了同一个服务多个请求发起时,上下文不相互影响。
默认中间件实现
正如设计分析时提到的,默认中间件包含了请求地址服务域名拼接,凭证携带,状态判断,内容提取,中间件可采用async/await,也可用常规函数,见示例代码:
每个中间件代码都非常简单易懂,这也是为什么要采用中间件的设计模型,因为将功能解耦,易于扩展。同时也能看到,next作为每个中间件的最后执行步骤,这种模式就是传说中的中间件尾调用模式。
测试用例
一个好的成熟的方法库,离不开好的测试用例,所以,我也很认真的写了五个测试用例,具体目的是:
结果如下:
写在最后
感谢你读到了这里,开始想写的非常多,但高考语文89分,不是偶然出现的。在实现一个用于日常生产的Http构造类,过程并不像这里写出来的这么简单,需要考虑和权衡的东西非常多,错误处理是关键。这里留了自己踩过的两个坑(更多是因为自己菜),这里没展开来讲,思考:
本文的源码可在此github地址下载,分支是http;
执行用例可在此github地址下载,分支是dva,或执行脚手架命令:
如果你有兴趣在你的项目尝试,可查阅npm使用指南