Koa部分源码学习笔记

Koa是什么

  • Koa是基于Node.js的Web框架,它不同于Express,它非常的小巧,整个原理也不复杂,很多企业会基于Koa,根据自己的业务需求再进行一层包装,形成自己的企业级框架,Koa自己的官网上也是介绍自己致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。

如何使用

  • 用Koa其实起一个服务非常简单:
const Koa = require('koa');
// 创建一个Koa对象表示web app本身:
const app = new Koa();

app.use(async (ctx, next) => {
    await next();
    ctx.response.type = 'text/html';
    ctx.response.body = '<h1>Hello, koa!</h1>';
});

// 在端口3000监听:
app.listen(3000);
console.log('app started at port 3000...');
  • Koa的核心API就是use和listen了。
  • listen方法就是监听一个端口,而use方法是相当于一个注册中间件的作用,Koa可拓展性其实就是因为它可以灵活的承载各种中间件,没有那么多的局限性,你可以根据自己写或者外部调用的中间件来拓展你的服务,比如最常见的路由处理我们就可以调用koa-router这个中间件,其实不管VSCode亦或是Webpack都是这样,就是因为它们这种丰富的插件机制,会形成一个良好的社区氛围,更多的人会参与进来维护开发。

源码解析部分(只看核心代码)

  • 源码地址:https://github.com/koajs/koa/tree/master/lib
  • 打开源码地址我们可以看到就四个文件:application.js,context.js,request.js和response.js,主要看application.js。
  • application.js可以看到里面的核心代码
module.exports = class Application extends Emitter {
    constructor(options) {
    super();
    this.middleware = [];
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);
  }
}
  • 其中middleware就是我们的中间件数组,context,request以及response都是我们之前说的context.js,request.js和response.js导出的一些处理request,response,context的方法,它把它们挂在this.context,this.request,this.response的原型上。
  • 再来看看我们的use方法
use(fn) {
  this.middleware.push(fn);
  return this;
}
  • 可以看到其实就是把我们的函数push进中间件数组完成注册
  • 目光聚焦到listen方法
listen(...args) {
    const server = http.createServer(this.callback());
    return server.listen(...args);
}
callback() {
   const fn = compose(this.middleware);

   if (!this.listenerCount('error')) this.on('error', this.onerror);

   const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res);
      return this.handleRequest(ctx, fn);
   };

   return handleRequest;
}
handleRequest(ctx, fnMiddleware) {
  const res = ctx.res;
  res.statusCode = 404;
  const onerror = err => ctx.onerror(err);
  const handleResponse = () => respond(ctx);
  onFinished(res, onerror);
  return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
  • 可以看到listen方法其实就是Node起服务的语法糖,跳到callback函数里,我们看到Koa做的首先是把我们的中间件数组compose了一下,这个compose的过程就是Koa洋葱模型的来源,稍后再讲,handleRequest里面其实就是先把我们的req和res挂载到我们的上下文对象ctx然后注入到中间件里,因为 http.createServer里面函数传的两个参数就是req和res,相当于callback的主要作用其实就是对我们的中间件compose了一下然后在函数里可以调用这个compose后的函数,等价于下面这个样子:
listen(...args) {
  const fn = compose(this.middleware);
  const server = http.createServer((req,res)=>{
    const ctx = this.createContext(req, res);
    return this.handleRequest(ctx, fn);
  });
  return server.listen(...args);
}
  • 然后再跳到handleRequest,他所做的也很简单,也就是先执行我们的中间件函数然后最后处理我们的上下文,你去看respond函数,其实里面也就是对你的返回的请求做了一些处理,然后最后进行发送,就是我们平时写原生Node.js的服务的方式。
  • 最后来看一下我们的compose方法,compose方法其实就是所谓的洋葱模型的实现了,洋葱圈模型:

    需要知道Koa里面的调用中间件的方法就是一个链式调用的过程,他把你注册的所有中间件函数串了起来,然后按顺序调用:
module.exports = compose

function compose (middleware) {

  return function (context, next) {
    // last called middleware #
    let index = -1
    return dispatch(0)
    function dispatch (i) {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      let fn = middleware[i]
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}
  • 核心代码其实就是这么多,它对我们的middleware数组做了什么呢,其实就是返回了一个递归的Promise链,这是为了兼容有些函数可能是async函数,不管它是不是异步都给它包一层Promise以便能正常的调用,从Promise.resolve(fn(context, dispatch.bind(null, i + 1)));我们就可以看到这就是为什么中间件函数传的参数是(ctx,next)了,通过闭包的方式不断把index加一然后把我们下一个中间件函数注入到当前中间件函数的参数里,把它们串了起来,最后配合中间件函数里调用next的时机来执行,它最后其实就是
dispatch(0)
  return fn(ctx, dispatch(1))
    return fn(ctx, dispatch(2))
      ...

总结

  • Koa核心源码其实不难,它真正迷人的地方还是在于它的可拓展性,也就是对中间件的利用,而所谓的洋葱模型也不难实现,就是一个递归调用的过程。
  • 本文没有对Koa处理错误的代码进行阅读解析,有时间可以去看看。
  • 理解Koa源码有助于理解我们自己团队的一些基于Koa搭建的企业级框架。

发表评论

电子邮件地址不会被公开。 必填项已用*标注