手把手带你造轮子系列之Koa篇(二)
概览:Koa是一个基于Node.js平台的下一代Web框架,Express的原班人马打造, 旨在为现代Web应用程序提供更有表达力更健壮的支撑。Koa的核心思想是使用ES6中的Async/Await语法解决异步流程控制问题,并采用了独特的中间件机制来处理请求和响应,使得Koa非常适合用于编写高效简洁、灵活、可扩展、易维护的Web应用程序。
此篇是Koa系列的第二篇,上一篇我们虽然实现了MiniKoa的最基本需求,但是存在很多问题,例如,在构造函数中我们用一个成员变量接收用户注册的回调事件,如果用户注册多个回调,那么我们只会保存最后一次的回调,还有我们在listen方法里面先执行了用户注册的回调函数,然后再通过handleRequest处理请求,如果用户的请求是一个异步事件,那么我们的执行顺序会出现问题,如果用户注册多个回调函数,我们又如何一次执行这些回调函数,接下来这篇主要解决上面提到的问题,较为完善的构建Koa的核心中间件系统(在Koa中注册的回调函数被称为中间件)。同样我们从一个例子出发,构建今天的需求场景。
koa示例
我们构建了一个Koa服务,添加了三个中间件,如下:
const Koa = require('./lib/application.js');
const app = new Koa();
app.use(async (ctx, next) => {
console.log(1);
await next();
console.log(2);
});
app.use(async (ctx, next) => {
console.log(3);
await next();
console.log(4);
});
app.use(async ctx => {
ctx.body = "hello world";
});
app.listen(3000, () => console.log('The service started at port 3000'));
看到上面代码的小伙伴,能够猜出代码运行,响应客户端请求的log结果是怎样的呢?这就是Koa里面最最核心的特性---中间件系统,也就是所谓的洋葱模型。接下来我们一起构建我们自己的中间件系统。
存储所有的用户回调函数
为了存储所有的用户注册回调,我们需要修改上篇中存储的代码,如下:
const http = require("http");
module.exports = class Application {
constructor() {
// this.callback = null;
this.middleware = [];
}
};
相应的use方法也需要进行修改:
use(fn) {
if(typeof fn !== 'function') throw new TypeError('middleware must be a function');
//this.callback = fn;
this.middleware.push(fn);
return this;
}
当用户使用use方法的时候我们push到this.middleware里面,然后返回this,以便我们可以链式调用。接下来我们需要修改listen方法,以便它可以处理所有中间件。
修改listen方法
listen(...args) {
// const server = http.createServer((req, res) => {
// const ctx = this.createContext(req, res);
// this.callback && this.callback(ctx);
// this.handleRequest(ctx);
// });
const server = http.createServer(this.callback())
return server.listen(...args);
}
添加callback方法
我们添加了一个callback方法,目的是把所有处理请求的逻辑封装到一起,方便后期的维护。接下来看下callback的实现。
callback() {
const fn = compose(this.middleware);
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res)
return this.handleRequest(ctx, fn)
}
return handleRequest;
}
callback是一个高阶函数,返回请求的处理方法,compose的核心逻辑是组织处理存储的所有中间件,然后同样封装ctx,而最终的处理逻辑封装在了类的handleRequest方法上。接下来我们要看最最核心的compose方法了,它是如何将所有的方法组织到一起并由用户控制执行的逻辑。
添加compose方法
module.exports = compose
function compose (middleware) {
/*
* 对参数middleware进行校验,必须是数组,并且数组的每一项必须是函数
* 否则直接抛出错误
*/
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}
return function (context, next) {
// 防止用户在一个中间件中多次调用next,index 从-1开始小于0
let index = -1
return dispatch(0)
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
/* 将i赋值给index,然后下面调用的时候会加1,所以下次调用dispatch时i会大于index,
*如果重复调用,第二次i的值没有增加,所以index会等于i,上面的判断即会抛出错误
*/
index = i
let fn = middleware[i]
// 用户注册的所有中间件执行完成之后,可再执行用户传入的next方法
if (i === middleware.length) fn = next
// 如果fn不存在返回一个立即成的Promise
if (!fn) return Promise.resolve()
// 添加try..catch捕获每个中间件的执行的错误
try {
/* fn执行的结果用Promise.resolve包裹,统一同步异步的处理,也方便这个函数的的使用
* @param context 依然是之前我们组装的上下文
* @param dispatch.bind(null, i + 1) 将下一个中间件的执行权交给用户,即中间件中的next参 * 数,i+1是中间件在中间件数组中的序号
*/
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}
执行compose方法返回一个返回Promise的方法fn,通过执行fn便可以启动的中间件的执行,我们把从第二个开始的中间件包装起来当做next参数传递给用户,用户可以自主调用,所以用户就可以自己控制中间件的运行流程,是不是很神奇。接下来我们要修改类的handleRequest方法:
修改handleRequest方法
// 添加参数fnMiddleware,即我们上面compose方法返回的fn
handleRequest(ctx, fnMiddleware) {
const { res, body } = ctx;
res.statusCode = body ? 200 : 404;
//return res.end(body);
const handleResponse = () => respond(ctx);
return fnMiddleware(ctx).then(handleResponse)
.catch((err) => {throw new Error(err)})
}
我们封装了respond方法专门处理所有中间件执行完成之后的逻辑,fnMiddleware方法执行返回Promise,suoyi我们可以通过then和catch分别处理成功和失败的情况。我们添加一个简单的处理逻辑,直接将用户设置的body数据调用原生的res.end方法返回给客户端。如下:
添加respond方法
function respond(ctx) {
const {res} = ctx
return res.end(ctx.body)
}
至此,我们就完成了本篇的目的,自己动手完成了Koa的核心中间件系统,小伙伴们可以试下最开始的例子,看运行的结果是否和你预期的一致。今天我们的目标已经达成,后续还会带领大家继续完善我们的MiniKoa。
转载自:https://juejin.cn/post/7202859133048356925