Koa2源碼分析

源碼結(jié)構(gòu)

Koa的源碼中主要為lib目錄下的application.js, context.js, request.js與response.js文件

.
├── AUTHORS
├── CODE_OF_CONDUCT.md
├── History.md
├── LICENSE
├── Makefile
├── Readme.md
├── benchmarks
├── docs
├── lib
│   ├── application.js
│   ├── context.js
│   ├── request.js
│   └── response.js
├── package.json
└── test

application.js: 框架入口,導(dǎo)出Application類成玫,即使用時(shí)倒入的Koa類买羞。
context.js: context對(duì)象的原型熊杨,代理request與response對(duì)象因悲。
request.js: request對(duì)象的原型,提供請(qǐng)求相關(guān)的數(shù)據(jù)與操作。
response,js: response對(duì)象的原型,提供響應(yīng)相關(guān)的數(shù)據(jù)與操作冀偶。

Application

  • proxy: 是否信任proxy header參數(shù),默認(rèn)為false
  • middleware: 保存通過app.use(middleware)注冊(cè)的中間件
  • subdomainOffset: 保存通過app.use(middleware)注冊(cè)的中間件
  • env: 環(huán)境參數(shù)渔嚷,默認(rèn)為NODE_ENV或'development'
  • context: context模塊进鸠,通過context.js創(chuàng)建
  • request: request模塊,通過request.js創(chuàng)建
  • response: response模塊形病,通過response.js創(chuàng)建

Application@listen

Koa通過app.listen(port)函數(shù)在某個(gè)端口啟動(dòng)服務(wù)客年。
listen函數(shù)通過http模塊開啟服務(wù):

/**
 * shorthand for:
 *  http.createServer(app.callback()).listen(...)
 *
 * @param {Mixed} ...
 * @return {Server}
 * @api public
 */
listen(...args) {
  debug('listen');
  const server = http.createServer(this.callback());
  return server.listen(...args);
}

實(shí)際上app.listen()為http.createServer(app.callback()).listen(...)的速記寫法。http.createServer()用于創(chuàng)建Web服務(wù)器漠吻,接受一個(gè)請(qǐng)求監(jiān)聽函數(shù)量瓜,并在得到請(qǐng)求時(shí)執(zhí)行。app.callback()用于處理請(qǐng)求途乃,合并中間件與創(chuàng)建請(qǐng)求上下文對(duì)象等等绍傲。

Application@use

Koa通過app.use()添加中間件,并將中間件存儲(chǔ)在app.middleware中欺劳。在執(zhí)行app.callback()時(shí)會(huì)將app,middleware中的中間件合并為一個(gè)函數(shù)唧取。

/**
 * Use the given middleware 'fn',
 *
 * Old-style middleware will be converted.
 *
 * @param {Function} fn
 * @return {Application} self
 * @api public
 */
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/blob/master/docs/migration.md');
    fn = convert(fn);
  }
  debug('use %s', fn._name || fn.name || '-');
  this.middleware.push(fn);
  return this;
}

Koa1.x版本使用Generator Function的方式寫中間件,而Koa2改用ES6 async/await划提。所以在use()函數(shù)中會(huì)判斷是否為舊風(fēng)格的中間件寫法枫弟,并對(duì)舊風(fēng)格寫法得中間件進(jìn)行轉(zhuǎn)換(使用koa-convert進(jìn)行轉(zhuǎn)換)。
可以注意到這里use()函數(shù)返回了this鹏往,這使得在添加中間件的時(shí)候能鏈?zhǔn)秸{(diào)用淡诗。

app
  .use(function(ctx, next) {
    //  do some thing
  })
  .use(function(ctx, next) {
    //  do some thing
  })
  // ...

Application@callback

app.callback()負(fù)責(zé)合并中間件,創(chuàng)建請(qǐng)求上下文對(duì)象以及返回請(qǐng)求處理函數(shù)等伊履。

/**
 * Return a request handler callback
 * for node's native http server
 *
 * @return {Function}
 * @api public
 */

callback() {
  const fn = compose(this.middleware);

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

  const handleRequest = (req, res) => {
    res.statusCode = 404;
    const ctx = this.createContext(req, res);
    const onerror = err => ctx.onerror(err);
    const handlerResponse = () => respond(ctx);
    onFinished(res, onerror);
    return fn(ctx).then(handleResponse.catch(onerror));
  };

  return handleRequest;
}

通過compose函數(shù)(koa-compose)合并app.middleware中的所有中間件韩容。查看關(guān)于koa-compose的分析。
app.callback()函數(shù)返回一個(gè)請(qǐng)求處理函數(shù)handleRequest唐瀑。該函數(shù)即為http.createServer接收的請(qǐng)求處理函數(shù)群凶,在得到請(qǐng)求時(shí)執(zhí)行。

handleRequest

handleRequest函數(shù)首先將響應(yīng)狀態(tài)設(shè)置為404哄辣,接著通過app.createContext()創(chuàng)建請(qǐng)求的上下文對(duì)象请梢。
onFinished(res, onerror)通過第三方庫(kù)on-finished監(jiān)聽http response,當(dāng)請(qǐng)求結(jié)束時(shí)執(zhí)行回調(diào)力穗,這里傳入的回調(diào)是context.onerror(err)毅弧,即當(dāng)錯(cuò)誤發(fā)生時(shí)才執(zhí)行。
最后返回fn(ctx).then(handleResponse).catch(onerror)当窗,即將所有中間件執(zhí)行(傳入請(qǐng)求上下文對(duì)象ctx)够坐,之后執(zhí)行響應(yīng)處理函數(shù)(app.respond(ctx)),當(dāng)拋出異常時(shí)同樣使用cintext,onerror(err)處理。

createContext

app.createContext()用來創(chuàng)建請(qǐng)求上下文對(duì)象元咙,并代理Koa的request和response模塊梯影。

/**
 * Initialize a new context
 *
 * @api private
 */

createContext(req, res) {
  const context = Object.create(this.context);
  const request = context.request = Object.create(this.response);
  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.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;
}

這里對(duì)請(qǐng)求都對(duì)應(yīng)在上下文對(duì)象中添加對(duì)應(yīng)的cookies。

respond

app.respond(ctx)函數(shù)蛾坯,這就是app.createContext()函數(shù)中的handleResponse光酣,在所有中間件執(zhí)行完之后執(zhí)行。
在koa中可以通過設(shè)置ctx.respond = false來跳過這個(gè)函數(shù)脉课,但不推薦這樣子。另外财异,當(dāng)上下文對(duì)象不可寫時(shí)也會(huì)退出該函數(shù):

if (false === ctx.respond) return;
// ...
if (!ctx.writable) return;

當(dāng)返回的狀態(tài)碼表示沒有響應(yīng)主體時(shí)倘零,將響應(yīng)主體置空:

// ignore body
if (statues.empty[code]) {
  // strip headers
  ctx.body = null;
  return res.end();
}

當(dāng)請(qǐng)求方法為HEAD時(shí),判斷響應(yīng)頭是否發(fā)送以及響應(yīng)主體是否為JSON格式戳寸,若滿足則設(shè)置響應(yīng)Content-Length:

if('HEAD' == ctx.method) {
  if(!res.headersSent && isJSON(body)) {
    ctx.length = Buffer.byteLength(JSON.stringify(body));
  }
  return res.end();
}

當(dāng)返回的狀態(tài)碼表示有響應(yīng)主體呈驶,但響應(yīng)主體為空時(shí),將響應(yīng)主體設(shè)置為響應(yīng)信息或狀態(tài)碼疫鹊。并當(dāng)響應(yīng)頭未發(fā)送時(shí)設(shè)置Content-Type與Content-Length:

if (null == body) {
  body = ctx.message || String(code);
  if (!res.headersSent) {
    ctx.type = 'text';
    ctx.length = Buffer.byteLength(body);
  }
  return res.end(body);
}

最后袖瞻,對(duì)不同的響應(yīng)主體進(jìn)行處理:

// response
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);

Compose

在application.js中,callback()函數(shù)通過koa-compose組合所有的中間件拆吆,組合成單個(gè)函數(shù)聋迎。koa-compose的實(shí)現(xiàn)很簡(jiǎn)單:

function compose (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) {
    // 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, function next() {
          return dispatch(i + 1);
        }));
      } catch (err) {
        return Promise.reject(err);
      }
    }
  }
}

首先判斷傳入的中間件參數(shù)是否為數(shù)組,并檢查且該數(shù)組的元素是否為函數(shù)枣耀,然后返回了一個(gè)將中間件組合起來的函數(shù)霉晕。
重點(diǎn)關(guān)注返回的函數(shù)中的dispatch(i)函數(shù),這個(gè)函數(shù)將獲取第一個(gè)中間件捞奕,并在返回的Promise中執(zhí)行牺堰。當(dāng)中間件await next()時(shí)執(zhí)行下一個(gè)中間件,即:dispatch(i+1)颅围。
執(zhí)行流程可以簡(jiǎn)單看做:

async function middleware1() {
  console.log('middleware1 begin');
  await middleware2();
  console.log('middleware1 end');
}

async function middleware2() {
  console.log('middleware2 begin');
  await middleware3();
  console.log('middleware2 end');
}

async function middleware3() {
  console.log('middleware3 begin');
  console.log('middleware3 end');
}

middleware1();
//  執(zhí)行結(jié)果
middleware1 begin
middleware2 begin
middleware3 begin
middleware3 end
middleware2 end
middleware1 end

compose()函數(shù)通過Promise將這個(gè)過程串聯(lián)起來伟葫,從而返回單個(gè)中間件函數(shù)。

Context

Koa中的Context模塊封裝了request與response院促,代理了這兩個(gè)對(duì)象的方法與屬性筏养。其中使用了Tj寫的node-delegates庫(kù),用于代理context.request與context.response上的方法與屬性一疯。

/**
 * Response delegation
 */

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')
// ...

context除了代理這兩個(gè)模塊外撼玄,還包含一個(gè)請(qǐng)求異常時(shí)的錯(cuò)誤處理函數(shù)。在application.js的callback()眾使用了這個(gè)函數(shù)墩邀。

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

Context@onerror

context.onerror(err)首先對(duì)傳入的err變量進(jìn)行判斷掌猛,當(dāng)err為空時(shí)退出函數(shù),或者當(dāng)err不為空且不為Error類型時(shí)拋出異常。

if (null == err) return;
if (!(err instanceof Error))  err = new Error('non-error thrown: ${err}');

接著觸發(fā)app自身的error事件荔茬,將錯(cuò)誤拋給app废膘。
在此之前,設(shè)置headerSent變量表示響應(yīng)頭是否發(fā)送慕蔚,若響應(yīng)頭已發(fā)送丐黄,或者不可寫(即無法在響應(yīng)中添加錯(cuò)誤信息等),則退出該函數(shù)孔飒。

let headerSent = false;
if (this.headerSent || !this.writable) {
  headerSent = err.headerSent = true;
}

//  delegate
this.app.emit('error', err, this);

// nothing we can do here other
// than delegate to the app-level
// handler and log.
if (headerSent) {
  return;
}

因?yàn)榘l(fā)生了錯(cuò)誤灌闺,所以必須將之前中間設(shè)置的響應(yīng)頭信息清空。
這里使用了Node提供的http.ServerResponse類上的getHeaderNames()與removeHeader()方法坏瞄。但getHeaderNames()這個(gè)函數(shù)在Node 7.7版本時(shí)加入的桂对,所以當(dāng)沒有提供該方法時(shí)需要使用_header來清空響應(yīng)頭。詳情可見:Node.js#10805鸠匀。

//  first unset all headers
if (typeof res.getHeaderNames === 'function') {
  res.getHeaderNames().forEach(name => res.removHeader(name));
} else {
  res._headers = {};  //  Node < 7.7
}

清空之前中間件設(shè)置的響應(yīng)頭之后蕉斜,將響應(yīng)頭設(shè)置為err.headers,并設(shè)置Content-Type與狀態(tài)碼缀棍。
當(dāng)錯(cuò)誤碼為ENOENT時(shí)宅此,意味著找不到該資源,將狀態(tài)碼設(shè)置為404爬范;當(dāng)沒有狀態(tài)碼或狀態(tài)碼錯(cuò)誤時(shí)默認(rèn)設(shè)置為500父腕。

//  then set those specified
this.set(err.headers);

//  force text/plain
this.type = 'text';

//  ENOENT support
if ('ENOENT' == err.code) err.status = 404;

//  default to 500
if('number' != typeof err.status || !statuses[err.status]) err.status = 500;

Request

Request模塊封裝了請(qǐng)求相關(guān)的屬性及方法。通過application中的createContext()方法坦敌,代理對(duì)應(yīng)的request對(duì)象:

const request = context.request = Object.create(this.request);
// ...
context.req = request.req = response.req = req;
// ...
request.response = response;

Response

Response模塊封裝了響應(yīng)相關(guān)的屬性以及方法侣诵。與request相同,通過createContext()方法代理對(duì)應(yīng)的response對(duì)象:

const response = context.response = Object.create(this.response);
// ...
context.res = request.res = response.res = res;
// ...
response.request = request;
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末狱窘,一起剝皮案震驚了整個(gè)濱河市杜顺,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蘸炸,老刑警劉巖躬络,帶你破解...
    沈念sama閱讀 216,692評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異搭儒,居然都是意外死亡穷当,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,482評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門淹禾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來馁菜,“玉大人,你說我怎么就攤上這事铃岔⊥舸” “怎么了?”我有些...
    開封第一講書人閱讀 162,995評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)智嚷。 經(jīng)常有香客問我卖丸,道長(zhǎng),這世上最難降的妖魔是什么盏道? 我笑而不...
    開封第一講書人閱讀 58,223評(píng)論 1 292
  • 正文 為了忘掉前任稍浆,我火速辦了婚禮,結(jié)果婚禮上猜嘱,老公的妹妹穿的比我還像新娘衅枫。我一直安慰自己,他們只是感情好朗伶,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,245評(píng)論 6 388
  • 文/花漫 我一把揭開白布为鳄。 她就那樣靜靜地躺著,像睡著了一般腕让。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上歧斟,一...
    開封第一講書人閱讀 51,208評(píng)論 1 299
  • 那天纯丸,我揣著相機(jī)與錄音,去河邊找鬼静袖。 笑死觉鼻,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的队橙。 我是一名探鬼主播坠陈,決...
    沈念sama閱讀 40,091評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼捐康!你這毒婦竟也來了仇矾?” 一聲冷哼從身側(cè)響起慌洪,我...
    開封第一講書人閱讀 38,929評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤喻粹,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后绰播,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體花枫,經(jīng)...
    沈念sama閱讀 45,346評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡刻盐,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,570評(píng)論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了劳翰。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片敦锌。...
    茶點(diǎn)故事閱讀 39,739評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖佳簸,靈堂內(nèi)的尸體忽然破棺而出乙墙,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 35,437評(píng)論 5 344
  • 正文 年R本政府宣布伶丐,位于F島的核電站悼做,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏哗魂。R本人自食惡果不足惜肛走,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,037評(píng)論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望录别。 院中可真熱鬧朽色,春花似錦、人聲如沸组题。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,677評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)崔列。三九已至梢褐,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間赵讯,已是汗流浹背盈咳。 一陣腳步聲響...
    開封第一講書人閱讀 32,833評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留边翼,地道東北人鱼响。 一個(gè)月前我還...
    沈念sama閱讀 47,760評(píng)論 2 369
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像组底,于是被迫代替她去往敵國(guó)和親丈积。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,647評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容