koa原理淺析

何赫赫發表於2019-02-16

koa原理淺析

選取的版本為koa2

原文連結

koa的原始碼由四個檔案組成

application.js    koa的骨架
context.js        ctx的原型
request.js        request的原型
response.js       response的原型

基本用法

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

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

app.listen(3000);

初始伺服器

利用http模組建立伺服器

const app = http.createServer((req, res) => {
    ...
})  
app.listen(3000)

事實上koa把這些包在了其listen方法中

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

顯然this.callback()返回的是一個形如下面的函式

(req, res) => {}

上下文ctx

callback方法如下

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

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

    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res);
      return this.handleRequest(ctx, fn);
    };

    return handleRequest;
  }

ctx在koa中事實上是一個包裝了request和response的物件,從createContext中可以看到起繼承自context

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

可以看到ctx.request繼承自request,ctx.response繼承自response,檢視response和request可以看到裡面大都是set和get方法(獲取query,設定header)等等。並且ctx代理了ctx.request和ctx.response的方法,在原始碼中可以看到


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`);

所以我們可以直接這麼寫

ctx.url

等價於

ctx.request.url

中介軟體

我們再看一下callback函式,觀察發現compose模組十分的神奇,我暫且把它稱為是一個迭代器,它實現了中介軟體的順序執行

const fn = compose(this.middleware);

列印fn如下

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

最初接觸koa的時候我疑惑為什麼我寫了

ctx.body = `hello world`

並沒有ctx.response.end()之類的方法,事實上koa已經幫我們做了處理,在handleRequest方法中

const handleResponse = () => respond(ctx);

// fnMiddleware即為上面compose之後的fn
fnMiddleware(ctx).then(handleResponse).catch(onerror)

fnMiddleware返回的是一個promise,在中介軟體邏輯完成後在respond函式中最終去處理ctx.body

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

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

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

錯誤處理

  • (非首部)中介軟體層處理(我瞎起的)

對於每個中介軟體可能發生的錯誤,可以直接在該中介軟體捕獲

app.use((ctx, next) => {

    try {
        ...        
    } catch(err) {
        ...
    }

})
  • (首部)中介軟體層處理

事實上,我們只要在第一個中介軟體新增try… catch… ,整個中介軟體組的錯誤都是可以捕獲的到的。

  • (應用級別)頂層處理
app.on(`error`, (err) = {})

在上面中介軟體執行時看到,koa會自動幫我們捕獲錯誤並處理,如下

      try {
        return Promise.resolve(fn(context, function next () {
          return dispatch(i + 1)
        }))
      } catch (err) {
        //  捕獲錯誤
        return Promise.reject(err)
      }


//  在ctx.onerror中處理
const onerror = err => ctx.onerror(err);
fnMiddleware(ctx).then(handleResponse).catch(onerror)

我們看ctx.onerror發現它事實上是出發app監聽的error事件

  onerror(err) {


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

假如我們沒有定義error回撥怎麼辦呢,koa也為我們定義了預設的錯誤處理函式

callback方法做了判斷

  callback() {

    ...

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

    ...
  }

全文完

相關文章