Koa2第一篇: 圖解Hello World學習原始碼

小諾哥發表於2019-01-03

初衷

個人覺得學習原始碼需要帶著目的去看, 才能達到效果, 但是公司又沒有上Node, 沒有實踐怎麼辦呢?最近發現通過除錯Koa2原始碼也是個不錯的法子。

準備工作

- 安裝node
- 安裝vscode
- 學習如何在vscode下除錯
複製程式碼

關於Node下除錯推薦閱讀下:《Node.js 除錯指南》

我們這裡只需要學習如何在vscode下除錯即可。 具體就不詳情說了, 見連結, 有問題我們可以討論。

從Hello World開始學習

// 安裝koa2, nodemon等後, 來個入門的hello world

const Koa = require('koa')
const app = new Koa()
const port = 3000

app.use(async (ctx, next) => {
    await next()
    ctx.response.status = 200
    ctx.response.body = 'hello world'
})

app.listen(port, () => {
    console.log(`server is running on the port: ${port}`)
})

複製程式碼

是的沒錯,通過上述這個入門程式碼也能學習到Koa2的原始碼知識。

首先觀察上面用到了的一些API。
new Koa()
app.use()
app.listen()
複製程式碼

我們現在開始進入node_modules目錄下找到Koa。

koa2

通過package.json得知Koa的入口檔案為

"main": "lib/application.js"
複製程式碼
// lib目錄下模組
- application.js  例項
- context.js      上下文物件
- request.js      請求物件
- response.js     響應物件
複製程式碼

現在我們當然從入口檔案application.js開始。我們的例項程式碼第一行是new koa(); 我們肯定是有一個類,這裡是開始的突破點。

// 建構函式繼承node的EventEmitter類 
// http://nodejs.cn/api/events.html
module.exports = class Application extends Emitter {
    ...
}
複製程式碼

然後我們去打三個斷點, 分別如下:

構造器

app.use()

app.listen()

之所以在這三個地方打斷點是對應前面提到的執行了Koa2的三個api.通過這三個斷點我們一步步去了解Koa2內部是怎麼執行的。

最開始肯定是執行constructor();部分註釋見上述截圖。

this.middleware = []; 這個是用來存放通過app.use()註冊的中介軟體的。
複製程式碼

重點接下來看app.use()。

app.use()

很明顯fn指的就是:

async (ctx, next) => {
    await next()
    ctx.response.status = 200
    ctx.response.body = 'hello world'
}
複製程式碼

在use(fn)方法中主要做了以下事情:

1. 錯誤校驗, fn必須是函式, 否則給出錯誤提示
2. fn不推薦使用生成器函式, v2版本Koa2會進行轉化, 但是v3就會不支援生成器函式了, 這裡主要是對koa1的向下相容。
3. 儲存註冊的中介軟體
3. return this. 支援鏈式呼叫
複製程式碼

這個時候你可以看懂this大概有這些屬性:

Application {
    _events:Object {}
    _eventsCount:0
    _maxListeners:undefined
    context:Object {}
    env:"development"
    middleware:Array(1) []
    proxy:false
    request:Object {}
    response:Object {}
    subdomainOffset:2
    Symbol(util.inspect.custom):inspect() { … }
    __proto__:EventEmitter
}
複製程式碼

然後進入listen(), 這裡有一段this.callback(), 我們需要去這個方法下打斷點看執行了什麼。

// 其實就是http.createServer(app.callback()).listen(...)的語法糖
listen(...args) {
    debug('listen');
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }
複製程式碼

callback()

// callback()做了以下幾件事:
1. 通過compose合併中介軟體
2. 為應用註冊error事件的監聽器
3. 返回一個請求處理函式handleRequest
複製程式碼

接下來我們看看this.createContext()和this.handleRequest(),分別打斷點看程式碼。

note: 提一個小問題, node應該經常會發生埠占用問題。
複製程式碼

每次請求都會建立一個上下文物件。

Koa2第一篇: 圖解Hello World學習原始碼

handleRequest(ctx, fnMiddleware) {
    const res = ctx.res;
    res.statusCode = 404;
    // 錯誤處理
    const onerror = err => ctx.onerror(err);
    const handleResponse = () => respond(ctx);
    // 通過第三方庫on-finished監聽http response,當請求結束時執行回撥,這裡傳入的回撥是context.onerror(err),即當錯誤發生時才執行。
    onFinished(res, onerror);
    // 即將所有中介軟體執行(傳入請求上下文物件ctx),之後執行響應處理函式(respond(ctx)),當丟擲異常時同樣使用onerror(err)處理。
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
  }
複製程式碼

對respond打斷點

/**
 * Response helper.
 * 在所有中介軟體執行完之後執行
 */

function respond(ctx) {
  // allow bypassing koa
  // 通過設定ctx.respond = false來跳過這個函式,但不推薦這樣子
  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();
  }
  // 當請求方法為HEAD時,判斷響應頭是否傳送以及響應主體是否為JSON格式,若滿足則設定響應Content-Length:
  if ('HEAD' == ctx.method) {
    if (!res.headersSent && isJSON(body)) {
      ctx.length = Buffer.byteLength(JSON.stringify(body));
    }
    return res.end();
  }

  // status body
  // 當返回的狀態碼錶示有響應主體,但響應主體為空時,將響應主體設定為響應資訊或狀態碼。並當響應頭未傳送時設定Content-Type與Content-Length:
  if (null == body) {
    if (ctx.req.httpVersionMajor >= 2) {
      body = String(code);
    } else {
      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);
}

複製程式碼

錯誤處理

onerror(err) {
    // 當err不為Error型別時丟擲異常。
    if (!(err instanceof Error)) throw new TypeError(util.format('non-error thrown: %j', err));
    // 當 err.status 是 404 或 err.expose 是 true 時預設錯誤處理程式也不會輸出錯誤
    if (404 == err.status || err.expose) return;
    // 預設情況下,將所有錯誤輸出到 stderr,除非 app.silent 為 true
    if (this.silent) return;

    const msg = err.stack || err.toString();
    console.error();
    console.error(msg.replace(/^/gm, '  '));
    console.error();
  }
複製程式碼

相關文章