美文网首页
koa 源码通读

koa 源码通读

作者: Napster99 | 来源:发表于2018-09-08 19:57 被阅读0次
    一、构建http服务器

    NodeJS原生写法

    const http = require('http')
    http.createServer(function(req, res) {
        res.writeHead(200, { 'Content-Type': 'text/html' });
        res.write('Hello World')
        res.end()
    }).listen(3000)
    

    koa写法

    const Koa = require('koa')
    const app = new Koa()
    app.use(async (ctx, next) => {
        ctx.body = 'Hello World'
    })
    app.listen(3000)
    

    相比这段代码你们都非常熟悉了,都是构建一个http服务器的代码
    虽然原生写法实现起来也比较简洁,但相比之下还是繁琐了点,我相信很多同学都不想自己去配置状态码及还要手动调用end()方法,否则就没有数据响应给客户端,而koa的做法就简明很多,只需给ctx.body赋值即可返回

    二、koa源码目录

    ok,让我们推开koa的大门,首先熟悉下它的代码结构

    koa
      |--benchmarks
      |--docs
      |--lib
          |--application.js
          |--context.js
          |--request.js
          |--response.js
      |--node_modules
      |--test
      |--package.json
    

    koa源码结构很简单,核心代码就在lib下的四个js文件,共1750行代码,这里也着重展开了一下

    三、核心文件详解

    通过package.json 文件的main启动文件可以看出 application.js是koa的主程序入口,我们把以上代码的引入改写成这样,就可以得到koa的引用了

    const Koa = require('./koa/lib/application')
    

    1、application.js

    /**
     * Module dependencies.
     */
    const isGeneratorFunction = require('is-generator-function');
    const debug = require('debug')('koa:application');
    const onFinished = require('on-finished');
    const response = require('./response');
    const compose = require('koa-compose');
    const isJSON = require('koa-is-json');
    const context = require('./context');
    const request = require('./request');
    const statuses = require('statuses');
    const Emitter = require('events');
    const util = require('util');
    const Stream = require('stream');
    const http = require('http');
    const only = require('only');
    const convert = require('koa-convert');
    const deprecate = require('depd')('koa');
    module.exports = class Application extends Emitter{
        //do something ...
    }
    
    function respond(ctx) {
        //do something ...
    }
    

    整个文件目录很清晰,就长这样,来看下它的构造函数

      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);
        if (util.inspect.custom) {
          this[util.inspect.custom] = this.inspect;
        }
      }
    

    看到这里,是不是瞬间想起了koa2需要new来实例化对象了,koa1是不需要的,直接调用koa()方法就是了,第一次this肯定指向执行环境NodeJS 中就是global,故此koa1() 相当于 new Koa1(),看明白了吧

    //koa1 入口函数
    function Application() {
      if (!(this instanceof Application)) return new Application;
      this.env = process.env.NODE_ENV || 'development';
      this.subdomainOffset = 2;
      this.middleware = [];
      this.proxy = false;
      this.context = Object.create(context);
      this.request = Object.create(request);
      this.response = Object.create(response);
    }
    

    回到koa2的讲解,构造函数初始化了一些变量,也分别继承了context\request\response对象,到此为止,还没有启动服务器,
    需要手动调用listen()方法,那我们顺着来看一下listen到底是个啥?

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

    就两行,调用的还是原生的方法,是的,你没看错,万变不离其宗,还是少不了原生的支持,那小伙伴会问为啥还用koa啊,原因在于this.callback(),接下来我们深入看下这个callback方法到底是个啥?直接上代码

      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;
      }
    

    首先会看到compose一个中间件的数组,compose方法引用于koa-compose组件,有什么用呢?官方给的解释是“Compose the given middleware and return middleware”把中间件函数合成一个函数用于执行,this.callback()相当于

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

    是不是很熟悉,一看就是原生的回调函数,两个参数分别是request和response,又将通过createContext函数封装了koa自己的上下文环境ctx,再将ctx和fn共同传递给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);
      }
    
    

    当发生一个请求时,就会调用上面的👆方法,经过报错响应处理最终会执行fnMiddleware方法,那该方法到底是啥呢,通过前面的代码可以得出是一个compose方法的返回值,为了更加的深入了解这里我从koa-compose包里把这段源码抓取了出来

    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)
          }
        }
      }
    

    这段代码显然是一个dispatch方法的递归调用,对于Promise.resolve都很熟悉吧,这里要注意一点就是dispatch.bind(null, i + 1)并不会立刻执行是要等上一个中间件调用了next()方法才会执行下一个中间件,比如目前有两个中间件,ma&mb

    async ma(context, next) => {
     //do something
     next()
    }
    async mb(context, next) => {
     //do something
     next()
    }
    fn(context, dispatch.bind(null, i+1))
    

    i作为闭包的变量,会自增加1,调用middleware数组的下一个元素,这里的next方法相当于dispatch.bind(null, i+1),如果中间件不调用next()方法的话,程序执行流将会中断,说的直白一点就是下面一个中间件只有声明的机会却没有执行的机会
    一系列的中间件执行完毕之后,就开始执行then方法,也就是以下代码

    const handleResponse = () => respond(ctx);
    fnMiddleware(ctx).then(handleResponse).catch(onerror);
    

    看到respond函数了吧,还记不记得application.js文件里就是这么两块东西,一块是定义Application的类,另一块就是定义了function 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);
    }
    

    主要处理了以下几种类型的响应

    string 写入
    Buffer 写入
    Stream 管道
    Object || Array JSON-字符串化
    null 无内容响应

    以上通过对body的值类型分析,返回对应格式的响应数据,这里还是调用了原生的end()方法,application.js就到此为止,接下来我们分析下context.js

    2、context.js

    
    const util = require('util');
    const createError = require('http-errors');
    const httpAssert = require('http-assert');
    const delegate = require('delegates');
    const statuses = require('statuses');
    const Cookies = require('cookies');
    
    const COOKIES = Symbol('context#cookies');
    
    /**
     * Context prototype.
     */
    
    const proto = module.exports = {}
    
    
    delegate(proto, 'response')
      .method('attachment')
      .method('redirect')
      .method('remove')
      .method('vary')
      .method('set')
      .method('append')
      .method('flushHeaders')
      .access('status')
      .access('message')
      .access('body')
      .access('length')
      .access('type')
      .access('lastModified')
      .access('etag')
      .getter('headerSent')
      .getter('writable');
    
    /**
     * Request delegation.
     */
    
    delegate(proto, 'request')
      .method('acceptsLanguages')
      .method('acceptsEncodings')
      .method('acceptsCharsets')
      .method('accepts')
      .method('get')
      .method('is')
      .access('querystring')
      .access('idempotent')
      .access('socket')
      .access('search')
      .access('method')
      .access('query')
      .access('path')
      .access('url')
      .access('accept')
      .getter('origin')
      .getter('href')
      .getter('subdomains')
      .getter('protocol')
      .getter('host')
      .getter('hostname')
      .getter('URL')
      .getter('header')
      .getter('headers')
      .getter('secure')
      .getter('stale')
      .getter('fresh')
      .getter('ips')
      .getter('ip');
    
    

    context.js代码核心的也就三块,delegates这个模块很有意思,它可以把内部的变量或者方法直接挂载到指定的对象上去,比如这里的response && request

    //delegates.js
    getter()  //只暴露对该变量的只读权限
    setter()  //只暴露对该变量的只写权限
    access() //暴露读写权限
    methond() //挂载方法
    

    至此request 及 response上的属性或方法都被挂载到了context上了,日常看到的ctx啥都能获取到,原因就在此

    3、request.js

    
    const URL = require('url').URL;
    const net = require('net');
    const accepts = require('accepts');
    const contentType = require('content-type');
    const stringify = require('url').format;
    const parse = require('parseurl');
    const qs = require('querystring');
    const typeis = require('type-is');
    const fresh = require('fresh');
    const only = require('only');
    const util = require('util');
    
    const IP = Symbol('context#ip');
    
    /**
     * Prototype.
     */
    console.log('request...')
    module.exports = {
      //do something
    
    }
    
    

    定义了一些列的工具方法,比如

    //get类
    header/headers/url/origin/href/methond/path/query/querystring
    /search/host/hostname/URL/fresh/stale/idempotent/socket/charset
    /length/protocol/secure/ips/ip/subdomains/accept/type
    
    //set类
    header/headers/url/methond/path/query/querystring/search/ip/accept
    

    4.response.js

    const contentDisposition = require('content-disposition');
    const ensureErrorHandler = require('error-inject');
    const getType = require('cache-content-type');
    const onFinish = require('on-finished');
    const isJSON = require('koa-is-json');
    const escape = require('escape-html');
    const typeis = require('type-is').is;
    const statuses = require('statuses');
    const destroy = require('destroy');
    const assert = require('assert');
    const extname = require('path').extname;
    const vary = require('vary');
    const only = require('only');
    const util = require('util');
    
    /**
     * Prototype.
     */
    console.log('response...')
    
    module.exports = {
    //do something
    }
    
    

    同样定义了一些列的工具方法,比如

    //get类
    socket/header/headers/status/message/body/length/headerSent/
    lastModified/etag/type/writable
    //set类
    status/message/body/length/type/lastModified/etag
    
    四、路由

    服务器启动了,接下来讲解下怎么响应url请求,返回对应数据
    比如实现:http://localhost:3000/get_user_info

    app.use(async(ctx, next) => {
        if (ctx.path === '/get_user_info') {
            return ctx.body = {
                name: 'zs',
                age: 10
            }
        }
        ctx.body = 'no content'
    })
    

    原始的写法太过繁琐,目前都有koa-router 来支持

    const route = require('koa-route');
    
    const userInfo = ctx => {
      ctx.response.body = {
                name: 'zs',
                age: 10
            }
    };
    
    const main = ctx => {
      ctx.response.body = 'Main Page';
    };
    
    app.use(route.get('/', main));
    app.use(route.get('/get_user_info', userInfo));
    

    相关文章

      网友评论

          本文标题:koa 源码通读

          本文链接:https://www.haomeiwen.com/subject/soelgftx.html