koa 源码通读

一、构建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));
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,591评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,448评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,823评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,204评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,228评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,190评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,078评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,923评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,334评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,550评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,727评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,428评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,022评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,672评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,826评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,734评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,619评论 2 354

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,077评论 25 707
  • 曾经见过刘国梁练球,在球网上方约五公分处放了一个标记,他必须让球从网和标记之间的空隙穿过,就这样把一筐球都打过去。...
    搬砖的赵工阅读 457评论 0 0
  • 你是否得到过陌生人的帮助或者帮助过陌生人?你踩过哪些坑?有着什么样的经历和经验?下面是两个真实的故事…… 1 重庆...
    清溪草阅读 308评论 5 6
  • 1 或许是因为放假了,空间里充斥着“每逢佳节倍思钱”的哀叹。 想要出去吃好吃的,可是没钱; 想要出去游山玩水,可是...
    安易_阅读 660评论 7 33
  • 〞 打渣子〞在老家睢县这块是骂着玩的意思,这让我想起一人来,我们叫他老徐,老徐六十多岁,身宽体胖,笑起像弥尼勒...
    演文居士阅读 379评论 0 1