前面三节实现了简单的静态资源服务器,实现了文件读取、目录读取、压缩、缓存几个功能,可以看到代码在逐渐庞大,虽然已经把缓存相关代码抽到了独立文件中,但 index.js 内依旧有层层 if-else 嵌套,代码逐渐走上失控
koa 写法
Koa 是目前 Node.js 社区较为流行的一个 Web 框架,使用 Koa 实现自定义服务器一样要实现大部分 HTTP 协议,但 Koa 实现这些功能代码书写相当优雅
const Koa = require('koa');
const app = new Koa();
// x-response-time
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
ctx.set('X-Response-Time', `${ms}ms`);
});
// logger
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}`);
});
// response
app.use(async ctx => {
ctx.body = 'Hello World';
});
app.listen(3000);
每个功能模块(中间件)都是独立书写,使用 app.use() 方法注册应用到 Web Server
中间件
实现一个有业务功能的 Web Server 一般会写三种代码
- 最基本的 HTTP Web Server,也可以使用业界成熟的 Nginx、Apache、Tomcat 等
- 和具体业务无关,实现特定功能的模块,比如读写数据库、执行定时任务、统一日志管理等
- 具体的业务逻辑代码,比如添加购物车、获取用户订单列表等
在 Web 服务器编程中把第二部分特定功能模块称之为中间件,所谓中间是指在 Web Server 和业务逻辑代码之间,中间件有两个最重要特征
- 和具体业务逻辑无关,功能比较通用
- 可以方便接入 Web Server,供业务代码调用,降低复杂、重复开发工作
前面三节实现的 HTTP 内容压缩、缓存功能独立出来,如果可以被 Web Server 热插拔,就可以称之为中间件,使用中间件可以显著提升Web Server 灵活性,解耦代码降低 Web Server 维护成本
Koa2 中间件支持 async 函数,一个中间件可以同时处理请求和响应,中间件有两个参数
- ctx:当前请求的上下文,包含 http 模块的 req、res 等对象
next() 当前中间件的上一个中间件,一般使用 next() 来分隔当前中间件请求处理部分和响应处理部分
async function customMiddleware(ctx, next) {
// ctx.request 请求部分处理
// ...
await next();
// ctx.response 响应处理部分
// ...
}
洋葱模型
HTTP Web Server 最主要的逻辑都在处理请求和响应,Koa 针对这种特征设计器其中间件执行模型——洋葱模型
如果在代码中注册了三个中间件 ```javascript const Koa = require(‘koa’); 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, next) => { console.log(5); await next(); console.log(6); });
app.listen(9527);
打印结果不是 1 2 3 4 5 6,而是 1 3 5 6 4 2
中间件被 next() 分隔,每次中间件调用执行完 next() 之前逻辑进入下一个中间件调用,所有中间件被递归调用完成后再逆序调用中间件 next() 之后的逻辑。这就是 Koa 中间件模型被称为洋葱模型的原因,下面这张图能更好的理解<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/87727/1590305105006-90cf8994-1447-4fa6-aa65-4d09a5d9b0c0.png#align=left&display=inline&height=309&margin=%5Bobject%20Object%5D&name=image.png&originHeight=435&originWidth=478&size=119627&status=done&style=none&width=340)
这样的模型再处理 Web 请求时候是非常有用的,比如想统计一次请求服务器处理时间,计算逻辑应该是从请求进入开始计时,到响应最后完成结束,可以写一个中间件,做为服务器第一个中间件 use 就达到预期效果了
```javascript
async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
ctx.set('X-Response-Time', `${ms}ms`);
}
koa-compose
Koa 中间件模型使用模块 koa-compose 实现,第一次读都会被其优雅的设计折服,模块没有任何依赖,算上空行、异常兼容、注释只有 48 行代码,移除异常处理后代码相当简单
function compose(middleware) {
return function (context) {
// 从第一个中间件开始调用
return dispatch(0);
/**
* 调用指定 index 的中间件,为其传入 next 参数为下一个中间件的 dispatch
* @param {Number} i 中间件 index
* @return {Promise} resolve 后意味着上一个中间件 next() 后的代码可以继续执行
*/
function dispatch(i) {
// 当前中间件函数
let fn = middleware[i];
// 中间件都被调用后
if (i === middleware.length) {
return Promise.resolve();
}
try {
// 调用当前中间件,next 参数设置为下一个中间件的 dispatch
// 程序执行到 await next() 时进入下一个中间件调用
const ret = fn(context, dispatch.bind(null, i + 1));
// 将本次调用结果返回给上一个中间件,也就是 await next()
return Promise.resolve(ret);
} catch (ex) {
return Promise.reject(ex);
}
}
}
}
写个测试
const compose = require('../src/util/compose');
const middleware = [];
middleware.push(async (ctx, next) => {
console.log('第 1 个中间件 next 前');
await next();
console.log('第 1 个中间件 next 后');
});
middleware.push(async (ctx, next) => {
console.log('第 2 个中间件 next 前');
await next();
console.log('第 2 个中间件 next 后');
});
middleware.push(async (ctx, next) => {
console.log('第 3 个中间件 next 前');
await next();
console.log('第 3 个中间件 next 后');
});
const ctx = {};
compose(middleware)(ctx);
第 1 个中间件 next 前
第 2 个中间件 next 前
第 3 个中间件 next 前
第 3 个中间件 next 后
第 2 个中间件 next 后
第 1 个中间件 next 后