Koa原理學習路徑與設計哲學

Koa原理學習路徑與設計哲學

本文基於Koa@2.5.0

Koa簡介(廢話篇)

Koa是基於Node.jsHTTP框架,由Express原班人馬打造。是下一代的HTTP框架,更簡潔,更高效。javascript

咱們來看一下下載量(2018.3.4)java

Koa:471,451 downloads in the last month
Express:18,471,701 downloads in the last month

說好的Koa是下一代框架呢,爲何下載量差異有這麼大呢,Express必定會說:你大爺仍是你大爺!webpack

確實,好多知名項目仍是依賴Express的,好比webpack的dev-server就是使用的Express,因此仍是看場景啦,若是你喜歡DIY,喜歡絕對的控制一個框架,那麼這個框架就應該什麼功能都不提供,只提供一個基礎的運行環境,全部的功能由開發者本身實現。git

正是因爲Koa的高性能和簡潔,好多知名項目都在基於Koa,好比阿里的eggjs,360奇舞團的thinkjsgithub

因此,雖然從使用範圍上來說,Express對於Koa你大爺仍是你大爺!,可是若是Express很好,爲何還要再造一個Koa呢?接下來咱們來了解下Koa到底帶給咱們了什麼,Koa到底作了什麼。web

如何着手分析Koa

先來看兩段demo。數組

下面是Node官方給的一個HTTP的示例。promise

const http = require('http');

const hostname = '127.0.0.1';
const port = 3000;

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello World\n');
});

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

下面是最簡單的一個Koa的官方實例。cookie

const Koa = require('koa');
const app = new Koa();

app.use(async ctx => {
  ctx.body = 'Hello World';
});

app.listen(3000);

Koa是一個基於Node的框架,那麼底層必定也是用了一些Node的API。app

jQuery很好用,可是jQuery也是基於DOM,逃不過也會用element.appendChild這樣的基礎API。Koa也是同樣,也是用一些Node的基礎API,封裝成了更好用的HTTP框架。

那麼咱們是否是應該看看Koahttp.createServer的代碼在哪裏,而後順藤摸瓜,瞭解整個流程。

Koa核心流程分析

Koa的源碼有四個文件

  • application.js // 核心邏輯
  • context.js // 上下文,每次請求都會生成一個
  • request.js // 對原生HTTP的req對象進行包裝
  • response.js // 對原生HTTP的res對象進行包裝

咱們主要關心application.js中的內容,直接搜索http.createServer,會搜到

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

恰好和Koa中的這行代碼app.listen(3000);關聯起來了。

找到源頭,如今咱們就能夠梳理清楚主流程,你們對着源碼看我寫的這個流程

fn:listen
∨
fn:callback
∨
[fn:compose] // 組合中間件 會生成後面的 fnMiddleware
∨
fn:handleRequest // (@closure in callback)
∨
[fn(req, res):createContext] // 建立上下文 就是中間件中用的ctx
∨
fn(ctx, fnMiddleware):handleRequest // (@koa instance)
∨
code:fnMiddleware(ctx).then(handleResponse).catch(onerror);
∨
fn:handleResponse
∨
fn:respond
∨
code:res.end(body);

從上面能夠看到最開始是listen方法,到最後HTTP的res.end方法。

listen能夠理解爲初始化的方法,每個請求到來的時候,都會通過從callbackrespond的生命週期。

在每一個請求的生命週期中,作了兩件比較核心的事情:

  1. 將多箇中間件組合
  2. 建立ctx對象

多箇中間件組合後,會前後處理ctx對象,ctx對象中既包含的req,也包含了res,也就是每一箇中間件的對象均可以處理請求和響應。

這樣,一次HTTP請求,接連通過各個中間件的處理,再到返回給客戶端,就完成了一次完美的請求。

Koa中的ctx

app.use(async ctx => {
  ctx.body = 'Hello World';
});

上面的代碼是一個最簡單的中間件,每一箇中間件的第一個參數都是ctx,下面咱們說一下這個ctx是什麼。

建立ctx的代碼:

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

直接上代碼,Koa每次請求都會建立這樣一個ctx對象,以提供給每一箇中間件使用。

參數的req, res是Node原生的對象。

下面解釋下這三個的含義:

  • context:Koa封裝的帶有一些和請求與相應相關的方法和屬性
  • request:Koa封裝的req對象,好比提了供原生沒有的host屬性。
  • response:Koa封裝的res對象,對返回的bodyhook了getter和setter。

其中有幾行一堆 xx = xx = xx,這樣的代碼。

是爲了讓ctx、request、response,可以互相引用。

舉個例子,在中間件裏會有這樣的等式

ctx.request.ctx === ctx
ctx.response.ctx === ctx

ctx.request.app === ctx.app
ctx.response.app === ctx.app

ctx.req === ctx.response.req
// ...

爲何會有這麼奇怪的寫法?其實只是爲了互相調用方便而已,其實最經常使用的就是ctx。

打開context.js,會發現裏面寫了一堆的delegate

/**
 * 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');

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

是爲了把大多數的requestresponse中的屬性也掛在ctx下,咱們爲了拿到請求的路徑須要ctx.request.path,可是因爲代理過path這個屬性,ctx.path也是能夠的,即ctx.path === ctx.request.path

ctx模塊大概就是這樣,沒有講的特別細,這塊是重點不是難點,你們有興趣本身看看源碼很方便。

一個小tip: 有時候我也會把 context.js中最下面的那些 delegate當成文檔使用,會比直接看文檔快一點。

Koa中間件機制

中間件函數的參數解釋

  • ctx:上面講過的在請求進來的時候會建立一個給中間件處理請求和響應的對象,好比讀取請求頭和設置響應頭。
  • next:暫時能夠理解爲是下一個中間件,其實是被包裝過的下一個中間件。

一個小栗子

咱們來看這樣的代碼:

// 第一個中間件
app.use(async(ctx, next) => {
  console.log('m1.1', ctx.path);
  ctx.body = 'Koa m1';
  ctx.set('m1', 'm1');
  next();
  console.log('m1.2', ctx.path);
});

// 第二個中間件
app.use(async(ctx, next) => {
  console.log('m2.1', ctx.path);
  ctx.body = 'Koa m2';
  ctx.set('m2', 'm2');
  next();
  debugger
  console.log('m2.2', ctx.path);
});

// 第三個中間件
app.use(async(ctx, next) => {
  console.log('m3.1', ctx.path);
  ctx.body = 'Koa m3';
  ctx.set('m3', 'm3');
  next();
  console.log('m3.2', ctx.path);
});

會輸出什麼呢?來看下面的輸出:

m1.1 /
m2.1 /
m3.1 /
m3.2 /
m2.2 /
m1.2 /

來解釋一下上面輸出的現象,因爲將next理解爲是下一個中間件,在第一個中間件執行next的時候,第一個中間件就將執行權限給了第二個中間件,因此m1.1後輸出的是m2.1,在以後是m3.1

那麼爲何m3.1後面輸出的是m3.2呢?第三個中間件以後已經沒有中間件了,那麼第三個中間件裏的next又是什麼?

我先偷偷告訴你,最後一箇中間件的next是一個馬上resolve的Promise,即return Promise.resolve(),一會再告訴你這是爲何。

因此第三個中間件(即最後一箇中間件)能夠理解成是這樣子的:

app.use(async (ctx, next) => {
    console.log('m3.1', ctx.path);
    ctx.body = 'Koa m3';
    ctx.set('m3', 'm3');
    new Promise.resolve(); // 原來是next
    console.log('m3.2', ctx.path);
});

從代碼上看,m3.1後面就會輸出m3.2

那爲何m3.2以後又會輸出m2.2呢?,咱們看下面的代碼。

let f1 = () => {
  console.log(1.1);
  f2();
  console.log(1.2);
}

let f2 = () => {
  console.log(2.1);
  f3();
  console.log(2.2);
}

let f3 = () => {
  console.log(3.1);
  Promise.resolve();
  console.log(3.2);
}

f1();

/*
  outpout
  1.1
  2.1
  3.1
  3.2
  2.2
  1.2
*/

這段代碼就是純函數調用而已,從這段代碼是否是發現,和上面一毛同樣,對一毛同樣,若是將next理解成是下一個中間件的意思,就是這樣。

中間件組合的過程分析

用戶使用中間件就是用app.use這個API,咱們看看作了什麼:

// 精簡後去掉非核心邏輯的代碼
  use(fn) {
    this.middleware.push(fn);
    return this;
  }

能夠看到,當咱們應用中間件的時候,只是把中間件放到一個數組中,而後返回this,返回this是爲了可以實現鏈式調用。

那麼Koa對這個數組作了什麼呢?看一下核心代碼

const fn = compose(this.middleware); // @callback line1
// fn 即 fnMiddleware 
return fnMiddleware(ctx).then(handleResponse).catch(onerror); // @handleRequest line_last

能夠看到用compose處理了middleware數組,獲得函數fnMiddleware,而後在handleRequest返回的時候運行fnMiddleware,能夠看到fnMiddleware是一個Promiseresolve的時候就會處理完請求,能猜到compose將多箇中間件組合成了一個返回Promise的函數,這就是奇妙之處,接下來咱們看看吧。

精簡後的compose源碼

// 精簡後去掉非核心邏輯的代碼
00    function compose (middleware) {
01      return function (context, next) { // fnMiddleware
02        return dispatch(0)
03        function dispatch (i) {
04          let fn = middleware[i] // app.use的middleware
05          if (!fn) return Promise.resolve()
06          return fn(context, function next () {
07            return dispatch(i + 1)
08          })
09        }
10      }
11    }

精簡後代碼只有十幾行,可是我認爲這是Koa最難理解、最核心、最優雅、最奇妙的地方。

看着各類function,各類return有點暈是吧,不慌,不慌啊,一行一行來。

compose返回了一個匿名函數,這個匿名函數就是fnMiddleware

剛纔咱們是有三個中間件,大家準備好啦,請求已通過來啦!

當請求過來的時候,fnMiddleware就運行了,即運行了componse返回的匿名函數,同時就會運行返回的dispatch(0),那咱們看看dispatch(0)作了什麼,仔細一看其實就是

// dispatch(0)的時候,fn即middleware[0]
return middleware[0](context, function next (){
  return dispatch(1);
})

// 上面的context和next即中間件的兩個參數
// 第一個中間件
app.use(async(ctx, next) => {
  console.log('m1.1', ctx.path);
  ctx.body = 'Koa m1';
  ctx.set('m1', 'm1');
  next(); // 這個next就是dispatch(1)
  console.log('m1.2', ctx.path);
});

同理,在第二個中間件裏面的next,就是dispatch(2),也就是用上面的方法被包裹一層的第三個中間件。

  • 如今來看第三個中間件裏面的next是什麼?

能夠看到精簡過的compose05行有個判斷,若是fn不存在,會返回Promise.resolve(),第三個中間件的nextdispatch(3),而一共就有三個中間件,因此middleware[3]是undefined,觸發了分支判斷條件,就返回了Promise.resolve()

再來複盤一下:

  1. 請求到來的事情,運行fnMiddleware(),即會運行dispatch(0)調起第一個中間件。
  2. 第一個中間件的nextdispatch(1),運行next的時候就調起第二個中間件
  3. 第二個中間件的nextdispatch(2),運行next的時候就調起第三個中間件
  4. 第三個中間件的nextdispatch(3),運行next的時候就調起Promise.resolve()。能夠把Promise.resolve()理解成一個空的什麼都沒有乾的中間件。

到此,大概知道了多箇中間件是如何被compose成一個大中間件的了吧。

中間件的類型

koa2中,支持三種類型的中間件:

  • common function:普通的函數,須要返回一個promise
  • generator function:須要被co包裹一下,就會返回一個promise
  • async function:直接使用,會直接返回promise

能夠看到,不管哪一種類型的中間件,只要返回一個promise就行了,由於這行關鍵代碼return fnMiddleware(ctx).then(handleResponse).catch(onerror);,能夠看到KoafnMiddleware的返回值認爲是promise。若是傳入的中間件運行後沒有返回promise,那麼會致使報錯。

結語

Koa的原理就解析到這裏啦,歡迎交流討論。
爲了更好地讓你們學習Koa,我寫了一個mini版本的Koa,你們能夠看一下 https://github.com/geeknull/t...

相關文章
相關標籤/搜索