likes
comments
collection
share

手把手带你造轮子系列之Koa篇(二)

作者站长头像
站长
· 阅读数 15

手把手带你造轮子系列之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。