前面三节实现了简单的静态资源服务器,实现了文件读取、目录读取、压缩、缓存几个功能,可以看到代码在逐渐庞大,虽然已经把缓存相关代码抽到了独立文件中,但 index.js 内依旧有层层 if-else 嵌套,代码逐渐走上失控
koa 写法
Koa 是目前 Node.js 社区较为流行的一个 Web 框架,使用 Koa 实现自定义服务器一样要实现大部分 HTTP 协议,但 Koa 实现这些功能代码书写相当优雅
const Koa = require('koa');const app = new Koa();// x-response-timeapp.use(async (ctx, next) => {const start = Date.now();await next();const ms = Date.now() - start;ctx.set('X-Response-Time', `${ms}ms`);});// loggerapp.use(async (ctx, next) => {const start = Date.now();await next();const ms = Date.now() - start;console.log(`${ctx.method} ${ctx.url} - ${ms}`);});// responseapp.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 />这样的模型再处理 Web 请求时候是非常有用的,比如想统计一次请求服务器处理时间,计算逻辑应该是从请求进入开始计时,到响应最后完成结束,可以写一个中间件,做为服务器第一个中间件 use 就达到预期效果了```javascriptasync (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 后
