Koa原始碼分析

_xiadd_發表於2018-01-20

上篇文章寫了如何閱讀Koa的原始碼, 粗略的過了一下Koa的原始碼, 但是作為一個沒有得出一個具體的結論, 中介軟體的執行原理也不清楚, 這裡我們再仔細的過一遍Koa的原始碼.

跟著例子過一遍

首先還是先過一遍例子

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

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

app.listen(3000);
複製程式碼

起一個web服務, 來一個Hello World, 作為http模組的再封裝, 我們還是慢慢來挖掘它是如何封裝的吧(無關的程式碼我都會刪掉).

首先是listen:

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

http模組我們都知道 無非是http.createServer(fn).listen(port), 其中fn帶著req, res. 根據上面的封裝我們可以肯定this.callback肯定是帶著請求以及進行響應了. 那麼再來看看this.callback吧.

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

    return handleRequest;
  }
複製程式碼

果然callback返回的函式是帶著req, res的, 那我繼續往下走看handleRequest究竟經歷了什麼, ctx大佬出現了, 我們在用koa的時候所有請求響應都是掛在ctx上的, 看起來ctx是通過createContext建立的, 那就繼續看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.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;
  }
複製程式碼

createContext比較簡單, 就是把各種有用的沒用的變數掛到context上, 程式碼也很簡單, 但是因為涉及到request和response我們需要簡單看一下request.js和response.js:

module.exports = {
  get header() {
    return this.req.headers;
  },
//..more items
}
複製程式碼

都是很簡單獲取變數沒啥好說的, 那麼回到前面callback部分, ctx建立好了然後呼叫並返回了this.handleReques, 沒啥好說的, 繼續看唄:

  handleRequest(ctx, fnMiddleware) {
    const res = ctx.res;
    res.statusCode = 404;
    const onerror = err => ctx.onerror(err);
    const handleResponse = () => respond(ctx);
    onFinished(res, onerror);
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
  }
複製程式碼

這一部分略微複雜一點, 由上面看出來fnMiddleware 是我們取出來的中介軟體, 然後我們把ctx傳到中介軟體裡執行, 跟我們的通常用法有點像了. 到這一步重點來了: 中介軟體

中介軟體

在探究中介軟體的原理之前, 不妨先來看看中介軟體是怎麼用的, 來個簡單的例子:

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

app.use(async function m1 (ctx, nex) {
   console.log('m1')
   await next()
   console.log('m2 end')
})

app.use(async function m2 (ctx, nex) {
  console.log('m2')
  await next()
  console.log('m2 end')
})

app.use(async function m3 (ctx, nex) {
  console.log('m3')
  ctx.body = 'Hello World'
})
複製程式碼

上面的結果很明確了, 但是我們不妨來視覺化一下:

m1: 輸出m1
await1: m1你先暫停一下讓m2先走
m1: ...
m2: 輸出m2
await2: m2你也停一下讓m3先走
m2: ...(委屈)
m3: 輸出m3, 上面的注意啦要遠路返回了
m2: 輸出m2 end m1注意了我要返回啦
m1: 輸出m1 end

respond: ctx.body是Hello world呢 就糊弄一下使用者返回吧
複製程式碼

看到沒, ctx.body不代表立即響應, 僅僅是一個我們後面會用到的變數, 也就是說我們的ctx過了一遍所有的中介軟體然後才會做出響應. 這裡不提await神奇的暫停效果, 我們就需要可以這麼用就行了. 那麼我們這個中介軟體是怎麼實現的呢, 來看compose.js:

function compose (middleware) {

  /**
   * @param {Object} context
   * @return {Promise}
   * @api public
   */

  return function (context, next) {
    let index = -1
    return dispatch(0)
    function dispatch (i) {
      index = i
      let fn = middleware[i]
      if (!fn) return Promise.resolve()
      return Promise.resolve(fn(context, function next () {
          return dispatch(i + 1)
        }))
    }
  }
}
複製程式碼

看過我前一篇的可以知道這裡其實就是一個遞迴. 但是跟connect的遞迴不一樣這裡是Promise, 我們都知道await 跟Promise搭配味道更佳嘛. 重點是這個next, 我們呼叫了await next之後, 程式非得等這個Promise執行完不可, 我們來簡化一下中介軟體的模型:

Promise.resolve(async m1 () {
  console.log(m1)
  await Promise.resolve(async m2 () {
    console.log(m2)
    await Promise.resolve(async m3 () {
      console.log(m3)
      ctx.body = 'xxx'
     })
     console.log(m2 end)
  })
  console.log(m1 end)
})
複製程式碼

是不是這樣一下就清楚了, 作為應用層的東西, 我們不需要去考慮async/await究竟是怎麼實現的, 只需要瞭解它實現了什麼樣的效果.

還是得佩服tj大神. 有問題可以互相交流哈.

image

相關文章