zoukankan      html  css  js  c++  java
  • Koa2 源码解析(1)

    Koa2 源码解析

    其实本来不想写这个系列文章的,因为Koa本身很精简,一共就4个文件,千十来行代码。
    但是因为想写 egg[1] 的源码解析,而egg是基于Koa2的,所以就先写个Koa2的吧,用作承上启下。

    [1] egg 是阿里巴巴团队开源的企业级web开发框架

    面向读者

    我们假定读者具备javascript基础知识,简单了解promise、generator和async。

    入口

    我们以 koajs中文官网 的例子作为入口。

    const Koa = require('koa');
    const app = new Koa();
    
    // response
    app.use(ctx => {
      ctx.body = 'Hello Koa';
    });
    
    app.listen(3000);
    

    这样就启动起来了一个Koa2网站,可以看到只做了3件事: Koa的构造函数、app实例的use函数、app实例的listen函数。

    查看Koa源码的package.json文件得知,默认入口是 application.js文件,也就是上面代码的Koa,那么让我们来看看里面是什么?

    Application.js

    module.exports = class Application extends Emitter {
    

    我们可以看到Application是继承自 Event 模块的事件监听器,并且 Koa2 已经使用了 ES6 的 class 语法。

    构造函数

    constructor() {
      super();
    
      this.proxy = false;
      this.middleware = [];
      this.subdomainOffset = 2;
      this.env = process.env.NODE_ENV || 'development';
      this.context = Object.create(context);
      this.request = Object.create(request);
      this.response = Object.create(response);
    }
    

    下面来看看构造函数,也没什么稀奇的。
    调用父类的构造函数,然后4个属性的初始化,我们先不管它们都是干什么的。

    接下来是创建3个对象context、request和response,其实这就是koa2的核心了,构造出的context表示本次请求的上下文,request和response这个大家都知道。

    use函数

    use(fn) {
      if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
      if (isGeneratorFunction(fn)) {
        deprecate('Support for generators will be removed in v3. ' +
                  'See the documentation for examples of how to convert old middleware ' +
                  'https://github.com/koajs/koa/tree/v2.x#old-signature-middleware-v1x---deprecated');
        fn = convert(fn);
      }
      debug('use %s', fn._name || fn.name || '-');
      this.middleware.push(fn);
      return this;
    }
    

    use函数更简单:判断是不是function,判断是不是generator,如果是generator那么转换一下,将fn放入middleware数组。

    listen函数

    listen() {
      debug('listen');
      const server = http.createServer(this.callback());
      return server.listen.apply(server, arguments);
    }
    

    listen看似也没什么,其实不然,获取createServer的callback函数是个核心的东西,我们来看下

    callback() {
      const fn = compose(this.middleware);
    
      if (!this.listeners('error').length) this.on('error', this.onerror);
    
      return (req, res) => {
        res.statusCode = 404;
        const ctx = this.createContext(req, res);
        const onerror = err => ctx.onerror(err);
        onFinished(res, onerror);
        fn(ctx).then(() => respond(ctx)).catch(onerror);
      };
    }
    

    首先,把所有middleware进行了组合,使用了koa-compose,我们也不用去管他的内部实现,简单来说就是返回了一个promise数组的递归调用。

    然后,我们看看这个匿名函数,把http code默认设置为404,接着利用createContext函数把node返回的req和res进行了组合创建出context,可以看看createContext函数

    createContext(req, res) {
      const context = Object.create(this.context);
      const request = context.request = Object.create(this.request);
      const response = context.response = Object.create(this.response);
      context.app = request.app = response.app = this;
      context.req = request.req = response.req = req;
      context.res = request.res = response.res = res;
      request.ctx = response.ctx = context;
      request.response = response;
      response.request = request;
      context.originalUrl = request.originalUrl = req.url;
      context.cookies = new Cookies(req, res, {
        keys: this.keys,
        secure: request.secure
      });
      request.ip = request.ips[0] || req.socket.remoteAddress || '';
      context.accept = request.accept = accepts(req);
      context.state = {};
      return context;
    }
    

    这里面都是一堆的组合和赋值,例如把req挂到context下面啦、把request挂到reponse下面啦、获取下accept啦、ip啦。

    回头来看看创建完context又干了些什么

    const onerror = err => ctx.onerror(err);
    onFinished(res, onerror);
    fn(ctx).then(() => respond(ctx)).catch(onerror);
    

    前两行没什么,给res注册了一个onerror事件,然后第三行就是具体所有middleware的执行了,所有middleware都执行完后,调用respond(ctx)函数

    function respond(ctx) {
      // allow bypassing koa
      if (false === ctx.respond) return;
    
      const res = ctx.res;
      if (!ctx.writable) return;
    
      let body = ctx.body;
      const code = ctx.status;
    
      // ignore body
      if (statuses.empty[code]) {
        // strip headers
        ctx.body = null;
        return res.end();
      }
    
      if ('HEAD' == ctx.method) {
        if (!res.headersSent && isJSON(body)) {
          ctx.length = Buffer.byteLength(JSON.stringify(body));
        }
        return res.end();
      }
    
      // status body
      if (null == body) {
        body = ctx.message || String(code);
        if (!res.headersSent) {
          ctx.type = 'text';
          ctx.length = Buffer.byteLength(body);
        }
        return res.end(body);
      }
    
      // responses
      if (Buffer.isBuffer(body)) return res.end(body);
      if ('string' == typeof body) return res.end(body);
      if (body instanceof Stream) return body.pipe(res);
    
      // body: json
      body = JSON.stringify(body);
      if (!res.headersSent) {
        ctx.length = Buffer.byteLength(body);
      }
      res.end(body);
    }
    

    这个respond函数里面也不过是一些收尾工作,例如判断http code为空如何输出啦,http method是head如何输出啦,body返回是流或json时如何输出啦。

    完事,你看koa是不是很简单,只不过是把res和req组合成了context,并提供了一些便利的函数,以及最重要的把middleware promise化了,写异步更爽了,加上es2017的async/await语法,跟C#也很像了。

    接下来我们还会对context、request和response对象进行一番解析,敬请期待。

  • 相关阅读:
    Spring boot mybatis : Error creating bean with name 'com.github.pagehelper.autoconfigure.MapperAutoConfiguration': Invocation of init method failed;
    方法调用
    初识MQ
    Shell 变量
    Eclipse 导入本地 Git 项目
    IDEA 常用快捷键
    Xshell 配色方案
    冒泡排序
    递归
    安卓服务Service详解
  • 原文地址:https://www.cnblogs.com/Rwing/p/koa2-source-code-overview.html
Copyright © 2011-2022 走看看