Koa 原始碼解析

aniiantt發表於2018-10-18

Koa 本體專案比較簡單,可以看作 node http 模組的封裝。

分為 application.js、context.js、request.js、response.js 四個檔案。

專案入口是 application.js

application.js

該檔案定義和匯出了 application 類。

首先我們來看看 listen 方法,該函式可以建立一個 Http 服務。可以理解為 http.createServer 的語法糖。 原始碼:

listen(...args) {
  const server = http.createServer(this.callback());
  return server.listen(...args);
}
複製程式碼

app.callback 是 createServer 的回撥函式。

callback() {
  // 中介軟體合成一個函式,處理中介軟體的資料流動
  const fn = compose(this.middleware);

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


  const handleRequest = (req, res) => {
    // 建立 ctx 物件
    const ctx = this.createContext(req, res);
    return this.handleRequest(ctx, fn);
  };

  return handleRequest;
}
複製程式碼

middleware 是一箇中介軟體函式構成的陣列,中介軟體通過 app.use 被壓入這個陣列中,

use(fn) {
  this.middleware.push(fn);
  return this;
}
複製程式碼

handleRequest:

handleRequest(ctx, fnMiddleware) {
  const res = ctx.res;
  res.statusCode = 404; // 預設返回 404
  const onerror = err => ctx.onerror(err); // 出錯時呼叫 ctx.onerror 函式
  const handleResponse = () => respond(ctx); // 處理完中介軟體後,呼叫 respond 函式
  onFinished(res, onerror);
  return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
複製程式碼

respond:

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

  // 處理 HEAD 請求
  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);
  }

  // 處理 body 是 buffer 或者 string 或者 stream 的情況
  // 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);

  // 傳送 json
  // body: json
  body = JSON.stringify(body);
  if (!res.headersSent) {
    ctx.length = Buffer.byteLength(body);
  }
  res.end(body);
}
複製程式碼

createContext:

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.state = {};
  return context;
}
複製程式碼

compose 是 koa 的核心,在這裡 compose 則實現了 koa 的中介軟體模型。洋蔥模型...詳情見 google。

function compose (middleware) {
  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, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}
複製程式碼

通過測試用例來理解這個函式的作用

const ctx = {}
const fn0 = async (ctx, next) => {
  console.log(1);
  await next();
  console.log(6);
}
const fn1 = async (ctx, next) => {
  console.log(2);
  await next();
  console.log(5);
}
const fn2 = async (ctx, next) => {
  console.log(3);
  await next();
  console.log(4);
}
const stack = [fn0, fn1, fn2];

return compose(stack)(ctx);
複製程式碼

函式執行時將經歷的步驟是:

  1. 首先執行 fn0(ctx, dispatch.bind(null, 1))
  2. 執行到 fn0 中 await next() 的時候,即等待 dispatch(1) = fn1(ctx, dispatch.bind(null, 2)) 執行完成
  3. 執行到 fn1 中 await next() 的時候,即等待 dispatch(2) = fn2(ctx, dispatch.bind(null, 3)) 執行完成
  4. 執行到 fn2 中 await next() 的時候,即等待 dispatch(3) 執行完成。由於不存在 fn3,所以 dispatch(3) = Promise.resolve()。此時因為立即 resolve 了,所以 fn2 會繼續執行。
  5. 當 fn2 執行完成,dispatch(2) 被 resolve 了,所以 fn1 會繼續執行至完成,以此類推直到 fn0 執行完成。

如果 fn1 中,有兩個 next

const fn1 = async (ctx, next) => {
  console.log(2);
  await next();
  await next();
  console.log(5);
}
複製程式碼

執行第二個 next() 的時候,相當於執行 dispatch(2),此時 i = 2,而 index 相當於一個全域性的 i 的值,此時等於 3。判斷 i < index 丟擲錯誤。

如果傳入 next 函式:

compose(stack)(ctx, async (ctx) => console.log(called))
複製程式碼

在第 4 步,執行 dispatch(3) 的時候 if (i === middleware.length) fn = next 由於 i 為 middleware.length, next 不為 undefined。 所以將會返回 next(context, dispatch.bind(null, 4))。如果 next(context, dispatch.bind(null, 4)) 返回一個 promise,則會等待 next 返回結果,fn1 才會繼續執行。

context.js

context 有一個 proto 物件,它裡面裡面有一些輔助的函式,並且 proto 代理了它的 response 和 request 屬性中的一些屬性和方法,以便快捷訪問。在呼叫 app.createContext 函式的時候,會通過 Object.create(proto) 建立一個例項。然後給這個 request response state 之類的屬性。

request.js 和 response.js

request 物件和 response 物件。改善了原生的 req 和 res 物件。 增加了易用性。

相關文章