逐行分析Koa源碼

Koa 簡(jiǎn)介

官網(wǎng)介紹:Koa 是一個(gè)新的 web 框架舆乔,由 Express 幕后的原班人馬打造领猾, 致力于成為 web 應(yīng)用和 API 開(kāi)發(fā)領(lǐng)域中的一個(gè)更小跳座、更富有表現(xiàn)力杯巨、更健壯的基石珍德。 通過(guò)利用 async 函數(shù)练般,Koa 幫你丟棄回調(diào)函數(shù),并有力地增強(qiáng)錯(cuò)誤處理锈候。 Koa 并沒(méi)有捆綁任何中間件薄料, 而是提供了一套優(yōu)雅的方法,幫助您快速而愉快地編寫(xiě)服務(wù)端應(yīng)用程序泵琳。

其中有幾個(gè)關(guān)鍵字:更小摄职、更富有表現(xiàn)力誊役、更健壯、錯(cuò)誤處理谷市、中間件蛔垢、快速。

用過(guò) Express迫悠,再來(lái)用 Koa鹏漆,肯定會(huì)有以上幾點(diǎn)感受。

優(yōu)秀的作品创泄,總是忍不住想通過(guò)源碼看看它是怎么實(shí)現(xiàn)的艺玲。

從 require('koa') 開(kāi)始看 Koa 源碼

const Koa = require('koa');

在 Node.js 里導(dǎo)入是通過(guò) require 函數(shù)調(diào)用進(jìn)行的。 Node.js 會(huì)根據(jù) require 的是相對(duì)路徑還是非相對(duì)路徑做出不同的行為鞠抑。

require('koa') 是非相對(duì)路徑饭聚,Node.js 會(huì)在一個(gè)特殊的文件夾 node_modules 里查找你的模塊。node_modules 可能與當(dāng)前文件在同一級(jí)目錄下碍拆,或者在上層目錄里若治。 Node.js 會(huì)向上級(jí)目錄遍歷,查找每個(gè) node_modules 直到它找到要加載的模塊感混。寫(xiě) require 的時(shí)候端幼,vscode 會(huì)有提示。

在項(xiàng)目的 node_modules 目錄下找到 koa 目錄弧满,首先看 package.json婆跑,找到 main 字段:

{
  "main": "lib/application.js"
}

main 字段對(duì)應(yīng)的是 Koa 的入口文件。lib 目錄下只有4個(gè)文件庭呜,這也是 Koa 的所有源碼滑进。相比 Express 『更小』。

找到入口文件就好辦了募谎。

new Koa() 發(fā)生了什么

實(shí)例化一個(gè) Koa 對(duì)象來(lái)使用 Koa 對(duì)外提供的各種 API扶关。

const app = new Koa();

在 application.js 中找到 Application 類,看其 constructor 構(gòu)造函數(shù)数冬,了解其初始化過(guò)程节槐。

// ... 省略一些 require
const Emitter = require('events');

// Application 繼承了 events,也就有了事件發(fā)布訂閱的功能
// Koa 錯(cuò)誤處理功能就是以此為基礎(chǔ)
module.exports = class Application extends Emitter {
  constructor(options) {
    super();
    options = options || {};
    this.proxy = options.proxy || false;
    this.subdomainOffset = options.subdomainOffset || 2;
    this.env = options.env || process.env.NODE_ENV || 'development';
    if (options.keys) this.keys = options.keys;

    // 很重要:初始化 middleware 中間件數(shù)組
    this.middleware = [];

    // 很重要:初始化 context拐纱、request铜异、response 三個(gè)屬性,它們與 middleware 共同組成了 Koa 最核心的部分
    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;
    }
  }
}

Koa 實(shí)例化完成之后秸架,我們會(huì)使用以下方法正式創(chuàng)建一個(gè)應(yīng)用揍庄,讓它具有處理請(qǐng)求和響應(yīng)的能力。

app.use(async function (ctx, next) {
  // ...
  await next();
  // ...
});

// ... 或許還需要使用很多的 use 方法

app.listen(3000, '127.0.0.1', error => {
  console.log('app started at port 3000...');
});

use 方法:

module.exports = class Application extends Emitter {
  use(fn) {
    // 規(guī)定 use 方法的參數(shù)必須是一個(gè) function
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');

    // Koa1.x 是用 generator 函數(shù)來(lái)操作異步流程的东抹,Koa2在這里做了兼容蚂子,并將在 V3 版本中徹底棄用沃测。
    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');
      // 將 generator 形式的函數(shù)轉(zhuǎn)換成 async 形式。
      // https://www.npmjs.com/package/koa-convert
      fn = convert(fn);
    }
    debug('use %s', fn._name || fn.name || '-');
    // 將回調(diào)函數(shù)添加到中間件隊(duì)列中
    this.middleware.push(fn);
    return this;
  }
}

listen 方法:

module.exports = class Application extends Emitter {
  listen(...args) {
    debug('listen');
    // 下面兩段代碼很熟悉了缆镣,使用 Node.js 創(chuàng)建一個(gè) HTTP 服務(wù)
    // this.callback 方法就是每次接收到 HTTP 請(qǐng)求之后的具體操作
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }
}

callback 方法:

module.exports = class Application extends Emitter {
  callback() {
    // 使用 compose 方法預(yù)處理 middleware:https://www.npmjs.com/package/koa-compose
    // 首先判斷 compose 方法的入?yún)⑹遣皇菙?shù)組芽突,如果不是,則 throw new TypeError('Middleware stack must be an array!')
    // 接著使用 for of 循環(huán)董瞻,判斷數(shù)組的每一項(xiàng)是不是 function,如果不是田巴,則 throw new TypeError('Middleware must be composed of functions!')
    // 最后返回一個(gè) function钠糊,這個(gè) function 是 Koa 中間件執(zhí)行流程的核心,后面會(huì)記錄壹哺。
    const fn = compose(this.middleware);

    // 因?yàn)?Application 類繼承了 events抄伍,所以也有 listenerCount 方法
    // 用來(lái)判斷開(kāi)發(fā)者是否有監(jiān)聽(tīng) error,如果沒(méi)有的話管宵,Koa 會(huì)內(nèi)置一個(gè)截珍,并執(zhí)行自定義的 onerror 方法
    // 當(dāng)錯(cuò)誤發(fā)生時(shí),console.log 一些信息
    if (!this.listenerCount('error')) this.on('error', this.onerror);

    // http.createServer 的入?yún)⑹且粋€(gè)方法箩朴,有兩個(gè)入?yún)⒏诤恚?req、res
    const handleRequest = (req, res) => {
      // 使用 createContext 將 req 和 res 包裝成一個(gè) ctx 上下文對(duì)象
      const ctx = this.createContext(req, res);
      // 正式處理接收到的 HTTP 請(qǐng)求
      return this.handleRequest(ctx, fn);
    };

    return handleRequest;
  }
}

先來(lái)看下 createContext 做了哪些事情炸庞?

module.exports = class Application extends Emitter {
  createContext(req, res) {
    // 代碼很清晰钱床,就是在 context 對(duì)象上掛載了一些屬性,然后返回
    // context 的初始內(nèi)容埠居,可參考 lib/context.js 文件
    // 做一個(gè) demo查牌,收到請(qǐng)求后,把 context 打印出來(lái)滥壕,結(jié)合代碼看纸颜,就都清楚了
    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.state = {};
    return context;
  }
}

再來(lái)看 handleRequest 方法:

module.exports = class Application extends Emitter {
  handleRequest(ctx, fnMiddleware) {
    const res = ctx.res;
    res.statusCode = 404;
    const onerror = err => ctx.onerror(err);
    const handleResponse = () => respond(ctx);
    // onFinish 方法是通過(guò)引入 on-finished 包得到的
    // 主要作用是:當(dāng) HTTP 請(qǐng)求 closes、finishes 或 errors 時(shí)執(zhí)行回調(diào)
    // https://www.npmjs.com/package/on-finished
    // 執(zhí)行的 onerror 方法绎橘,可參考 context.js 文件中的 onerror 方法胁孙,主要通過(guò) emit 觸發(fā) error 事件,最后 res.end(msg) 返回錯(cuò)誤信息
    onFinished(res, onerror);
    // fnMiddleware 就是上面通過(guò) compose 組合之后的一系列 middleware金踪,下面重點(diǎn)敘述
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
  }
}

compose 方法:

// https://github.com/koajs/compose/blob/master/index.js
function compose (middleware) {
  // 上面筆記記錄過(guò)的判斷一個(gè)中間件是否符合規(guī)范的邏輯
  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!')
  }

  // compose 最后返回的方法浊洞,即 handleRequest 中執(zhí)行的 fnMiddleware 方法。
  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
      // 每個(gè)中間件是一個(gè) async 函數(shù)胡岔,被上一個(gè)中間件的 next 方法調(diào)用(首個(gè)除外)
      if (!fn) return Promise.resolve()
      try {
        // 執(zhí)行 next 方法法希,起始就是再次執(zhí)行 dispatch,只是傳入的 index + 1靶瘸,表示執(zhí)行下一個(gè)中間件
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

Koa 使用 compose 組合之后的一系列中間件來(lái)處理 HTTP 的 request 和 response苫亦,而 compose 的實(shí)現(xiàn)原理很像是一個(gè)『洋蔥模型』:


koa.png

具體過(guò)程是:

  • 一個(gè)請(qǐng)求到一旦到后端毛肋,就開(kāi)始接觸洋蔥的最外層。

  • 遇到一個(gè) next()屋剑,就進(jìn)入下一層润匙。不過(guò)值得提醒的是,異步函數(shù)的 next() 與同步函數(shù)的 next(),不是在同一個(gè)空間的唉匾,我們可以假想一個(gè)“異步空間椩谢洌”,后入先出巍膘。

  • 什么時(shí)候到洋蔥中心厂财?就是遇到的第一個(gè)沒(méi)有next的中間件,或者遇到一個(gè)中間件報(bào)錯(cuò),就會(huì)把這個(gè)中間件當(dāng)成中心峡懈,因?yàn)橛龅藉e(cuò)誤了璃饱,不會(huì)再繼續(xù)往里面走。這個(gè)時(shí)候肪康,就開(kāi)始向洋蔥的外層開(kāi)始走了荚恶。如果第一個(gè)中間件就沒(méi)有 next,直接返回的磷支。那么就不存在洋蔥模型谒撼。

  • 一層一層外面走的時(shí)候,就先走位所有的同步中間件齐唆,再依次走“異步空間椸退ǎ”的中間件。

有沒(méi)有一種『遞歸』的感覺(jué)箍邮。

之前模擬實(shí)現(xiàn)了上述 compose 方法茉帅,可參考:https://github.com/zymfe/test-code/blob/master/test93.js

整體流程就是這樣,Koa 的核心就是提供了一套簡(jiǎn)單的中間件(一些自定義方法)使用方法锭弊,可以攔截堪澎、處理 HTTP 請(qǐng)求和響應(yīng),方便我們處理業(yè)務(wù)味滞。

request.js 和 response.js 沒(méi)有重點(diǎn)說(shuō)樱蛤,它們是在 createContext 方法被添加了一些新的屬性,原有屬性和方法可以參考對(duì)應(yīng)文件中的代碼剑鞍,就是一些設(shè)置昨凡、讀取請(qǐng)求頭、請(qǐng)求體的方法和屬性等蚁署。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末便脊,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子光戈,更是在濱河造成了極大的恐慌哪痰,老刑警劉巖遂赠,帶你破解...
    沈念sama閱讀 221,635評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異晌杰,居然都是意外死亡跷睦,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)肋演,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)抑诸,“玉大人,你說(shuō)我怎么就攤上這事惋啃『喵蓿” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,083評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵边灭,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我健盒,道長(zhǎng)绒瘦,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,640評(píng)論 1 296
  • 正文 為了忘掉前任扣癣,我火速辦了婚禮惰帽,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘父虑。我一直安慰自己该酗,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,640評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布士嚎。 她就那樣靜靜地躺著呜魄,像睡著了一般。 火紅的嫁衣襯著肌膚如雪莱衩。 梳的紋絲不亂的頭發(fā)上爵嗅,一...
    開(kāi)封第一講書(shū)人閱讀 52,262評(píng)論 1 308
  • 那天,我揣著相機(jī)與錄音笨蚁,去河邊找鬼睹晒。 笑死,一個(gè)胖子當(dāng)著我的面吹牛括细,可吹牛的內(nèi)容都是我干的伪很。 我是一名探鬼主播,決...
    沈念sama閱讀 40,833評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼奋单,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼锉试!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起辱匿,我...
    開(kāi)封第一講書(shū)人閱讀 39,736評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤键痛,失蹤者是張志新(化名)和其女友劉穎炫彩,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體絮短,經(jīng)...
    沈念sama閱讀 46,280評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡江兢,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,369評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了丁频。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片杉允。...
    茶點(diǎn)故事閱讀 40,503評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖席里,靈堂內(nèi)的尸體忽然破棺而出叔磷,到底是詐尸還是另有隱情,我是刑警寧澤奖磁,帶...
    沈念sama閱讀 36,185評(píng)論 5 350
  • 正文 年R本政府宣布改基,位于F島的核電站,受9級(jí)特大地震影響咖为,放射性物質(zhì)發(fā)生泄漏秕狰。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,870評(píng)論 3 333
  • 文/蒙蒙 一躁染、第九天 我趴在偏房一處隱蔽的房頂上張望鸣哀。 院中可真熱鬧,春花似錦吞彤、人聲如沸我衬。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,340評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)挠羔。三九已至,卻和暖如春懂盐,著一層夾襖步出監(jiān)牢的瞬間褥赊,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,460評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工莉恼, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留拌喉,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,909評(píng)論 3 376
  • 正文 我出身青樓俐银,卻偏偏與公主長(zhǎng)得像尿背,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子捶惜,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,512評(píng)論 2 359

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

  • 一田藐、基本用法 1.1 架設(shè) HTTP 服務(wù) // demos/01.jsconst Koa = require('...
    majun00閱讀 1,364評(píng)論 0 5
  • 原文鏈接:http://www.reibang.com/p/6b816c609669 前傳 出于興趣最近開(kāi)始研究k...
    懸筆e絕閱讀 7,220評(píng)論 1 11
  • koa 的源碼文件很少,lib 文件夾下只有4個(gè)文件 application、context汽久、request鹤竭、re...
    夢(mèng)想成真213閱讀 292評(píng)論 0 0
  • 一、背景 Koa 是一個(gè)新的 web 框架景醇,由 Express 幕后的原班人馬打造臀稚, 致力于成為 web 應(yīng)用和 ...
    bayi_lzp閱讀 10,579評(píng)論 6 26
  • 早晨7點(diǎn)半,被樓下除草劑的聲音吵醒三痰。簡(jiǎn)單而健康的早餐之后吧寺,開(kāi)啟了今天的美樂(lè)家之旅。現(xiàn)在是下午5點(diǎn)半散劫,除了午飯時(shí)間稚机,...
    稀稀公主閱讀 253評(píng)論 0 0