前面三节实现了简单的静态资源服务器,实现了文件读取、目录读取、压缩、缓存几个功能,可以看到代码在逐渐庞大,虽然已经把缓存相关代码抽到了独立文件中,但 index.js 内依旧有层层 if-else 嵌套,代码逐渐走上失控

koa 写法

Koa 是目前 Node.js 社区较为流行的一个 Web 框架,使用 Koa 实现自定义服务器一样要实现大部分 HTTP 协议,但 Koa 实现这些功能代码书写相当优雅

  1. const Koa = require('koa');
  2. const app = new Koa();
  3. // x-response-time
  4. app.use(async (ctx, next) => {
  5. const start = Date.now();
  6. await next();
  7. const ms = Date.now() - start;
  8. ctx.set('X-Response-Time', `${ms}ms`);
  9. });
  10. // logger
  11. app.use(async (ctx, next) => {
  12. const start = Date.now();
  13. await next();
  14. const ms = Date.now() - start;
  15. console.log(`${ctx.method} ${ctx.url} - ${ms}`);
  16. });
  17. // response
  18. app.use(async ctx => {
  19. ctx.body = 'Hello World';
  20. });
  21. app.listen(3000);

每个功能模块(中间件)都是独立书写,使用 app.use() 方法注册应用到 Web Server

中间件

实现一个有业务功能的 Web Server 一般会写三种代码

  1. 最基本的 HTTP Web Server,也可以使用业界成熟的 Nginx、Apache、Tomcat 等
  2. 和具体业务无关,实现特定功能的模块,比如读写数据库、执行定时任务、统一日志管理等
  3. 具体的业务逻辑代码,比如添加购物车、获取用户订单列表等

在 Web 服务器编程中把第二部分特定功能模块称之为中间件,所谓中间是指在 Web Server 和业务逻辑代码之间,中间件有两个最重要特征

  1. 和具体业务逻辑无关,功能比较通用
  2. 可以方便接入 Web Server,供业务代码调用,降低复杂、重复开发工作

前面三节实现的 HTTP 内容压缩、缓存功能独立出来,如果可以被 Web Server 热插拔,就可以称之为中间件,使用中间件可以显著提升Web Server 灵活性,解耦代码降低 Web Server 维护成本

Koa2 中间件支持 async 函数,一个中间件可以同时处理请求和响应,中间件有两个参数

  1. ctx:当前请求的上下文,包含 http 模块的 req、res 等对象
  2. next() 当前中间件的上一个中间件,一般使用 next() 来分隔当前中间件请求处理部分和响应处理部分

    1. async function customMiddleware(ctx, next) {
    2. // ctx.request 请求部分处理
    3. // ...
    4. await next();
    5. // ctx.response 响应处理部分
    6. // ...
    7. }

    洋葱模型

    HTTP Web Server 最主要的逻辑都在处理请求和响应,Koa 针对这种特征设计器其中间件执行模型——洋葱模型
    image.png
    如果在代码中注册了三个中间件 ```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. 打印结果不是 1 2 3 4 5 6,而是 1 3 5 6 4 2
  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)
  3. 这样的模型再处理 Web 请求时候是非常有用的,比如想统计一次请求服务器处理时间,计算逻辑应该是从请求进入开始计时,到响应最后完成结束,可以写一个中间件,做为服务器第一个中间件 use 就达到预期效果了
  4. ```javascript
  5. async (ctx, next) => {
  6. const start = Date.now();
  7. await next();
  8. const ms = Date.now() - start;
  9. ctx.set('X-Response-Time', `${ms}ms`);
  10. }

koa-compose

Koa 中间件模型使用模块 koa-compose 实现,第一次读都会被其优雅的设计折服,模块没有任何依赖,算上空行、异常兼容、注释只有 48 行代码,移除异常处理后代码相当简单

  1. function compose(middleware) {
  2. return function (context) {
  3. // 从第一个中间件开始调用
  4. return dispatch(0);
  5. /**
  6. * 调用指定 index 的中间件,为其传入 next 参数为下一个中间件的 dispatch
  7. * @param {Number} i 中间件 index
  8. * @return {Promise} resolve 后意味着上一个中间件 next() 后的代码可以继续执行
  9. */
  10. function dispatch(i) {
  11. // 当前中间件函数
  12. let fn = middleware[i];
  13. // 中间件都被调用后
  14. if (i === middleware.length) {
  15. return Promise.resolve();
  16. }
  17. try {
  18. // 调用当前中间件,next 参数设置为下一个中间件的 dispatch
  19. // 程序执行到 await next() 时进入下一个中间件调用
  20. const ret = fn(context, dispatch.bind(null, i + 1));
  21. // 将本次调用结果返回给上一个中间件,也就是 await next()
  22. return Promise.resolve(ret);
  23. } catch (ex) {
  24. return Promise.reject(ex);
  25. }
  26. }
  27. }
  28. }

写个测试

  1. const compose = require('../src/util/compose');
  2. const middleware = [];
  3. middleware.push(async (ctx, next) => {
  4. console.log('第 1 个中间件 next 前');
  5. await next();
  6. console.log('第 1 个中间件 next 后');
  7. });
  8. middleware.push(async (ctx, next) => {
  9. console.log('第 2 个中间件 next 前');
  10. await next();
  11. console.log('第 2 个中间件 next 后');
  12. });
  13. middleware.push(async (ctx, next) => {
  14. console.log('第 3 个中间件 next 前');
  15. await next();
  16. console.log('第 3 个中间件 next 后');
  17. });
  18. const ctx = {};
  19. compose(middleware)(ctx);
  20. 1 个中间件 next
  21. 2 个中间件 next
  22. 3 个中间件 next
  23. 3 个中间件 next
  24. 2 个中间件 next
  25. 1 个中间件 next