koa2核心源碼淺析

koa是一個輕量級的web應用框架。其實現很是精簡和優雅,核心代碼僅有區區一百多行,很是值得咱們去細細品味和學習。javascript

在開始分析源碼以前先上demo~java

DEMO 1

const Koa = require('../lib/application');
const app = new Koa();

app.use(async (ctx, next) => {
  console.log('m1-1');
  await next();
  console.log('m1-2');
});

app.use(async (ctx, next) => {
  console.log('m2-1');
  await next();
  console.log('m2-2');
});

app.use(async (ctx, next) => {
  console.log('m3-1');
  ctx.body = 'there is a koa web app';
  await next();
  console.log('m3-2');
});

app.listen(8001);
複製代碼

上面代碼最終會在控制檯依次輸出node

m1-1
m2-1
m3-1
m3-2
m2-2
m1-2
複製代碼

當在中間件中調用next()時,會中止當前中間件的執行,轉而進行下一個中間件。當下一箇中間件執行完後,纔會繼續執行next()後面的邏輯。web

DEMO 2

咱們改一下第一個中間件的代碼,以下所示:json

app.use(async (ctx, next) => {
  console.log('m1-1');
  // await next();
  console.log('m1-2');
});
複製代碼

當把第一個中間件的await next()註釋後,再次執行,在控制檯的輸出以下:數組

m1-1
m2-1
複製代碼

顯然,若是不執行next()方法,代碼將只會執行到當前的中間件,不事後面還有多少箇中間件,都不會執行。bash

這個next爲什麼會具備這樣的魔力呢,下面讓咱們開始愉快地分析koa的源碼,一探究竟~markdown

代碼結構

分析源碼以前咱們先來看一下koa的目錄結構,koa的實現文件只有4個,這4個文件都在lib目錄中。cookie

  • application.js — 定義了一個類,這個類定義了koa實例的方法和屬性
  • context.js — 定義了一個proto對象,並對proto中的屬性進行代理。中間件中使用的ctx對象,其實就是繼承自proto
  • request.js — 定義了一個對象,該對象基於原生的req拓展了一些屬性和方法
  • response.js - 定義了一個對象,該對象基於原生的res拓展了一些屬性和方法

經過package.json文件得知,koa的入口文件是lib/application.js,咱們先來看一下這個文件作了什麼。app

定義koa類

打開application.js查看源碼能夠發現,這個文件主要就是定義了一個類,同時定義了一些方法。

module.exports = class Application extends Emitter {

  constructor() {
    super();
    this.middleware = []; // 中間件數組
  }
  
  listen (...args) {
    // 啓用一個http server並監聽指定端口
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }
  
  use (fn) {
    // 把中間添加到中間件數組
    this.middleware.push(fn);
    return this;
  }
  
}
複製代碼

咱們建立完一個koa對象以後,一般只會使用兩個方法,一個是listen,一個是use。listen負責啓動一個http server並監聽指定端口,use用來添加咱們的中間件。

當調用listen方法時,會建立一個http server,這個http server須要一個回調函數,當有請求過來時執行。上面代碼中的this.callback()就是用來返回這樣的一個函數:這個函數會讀取應用全部的中間件,使它們按照傳入的順序依次執行,最後響應請求並返回結果。

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的執行流程

callback函數會在應用啓動時執行一次,而且返回一個函數handleRequest。每當有請求過來時,handleRequest都會被調用。咱們將callback拆分爲三個流程去分析:

  1. 把應用的全部中間件合併成一個函數fn,在fn函數內部會依次執行this.middleware中的中間件(是否所有執行,取決因而否有調用next函數執行下一個中間件)
  2. 經過createContext生成一個可供中間件使用的ctx上下文對象
  3. 把ctx傳給fn,並執行,最後對結果做出響應

koa中間件執行原理

const fn = compose(this.middleware);
複製代碼

源碼中使用了一個compose函數,基於全部可執行的中間件生成了一個可執行函數。當該函數執行時,每個中間件將會被依次應用。compose函數的定義以下:

function compose (middleware) {
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  for (const fn of middleware) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }

  return function (context, next) {
    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 //我的認爲對在koa中這裏的fn = next並無意義
      if (!fn) return Promise.resolve() // 執行到最後resolve出來
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}
複製代碼

它會先執行第一個中間件,執行過程當中若是遇到next()調用,就會把控制權交到下一個中間件並執行,等該中間件執行完後,再繼續執行next()以後的代碼。這裏的dispatch.bind(null, i + 1)就是next函數。到這裏就能解答,爲何必需要調用next方法,才能讓當前中間件後面的中間件執行。(有點拗口…)匿名函數的返回結果是一個Promise,由於要等到中間件處理完以後,才能進行響應。

context模塊分析

中間件執行函數生成好以後,接下來須要建立一個ctx。這個ctx能夠在中間件裏面使用。ctx提供了訪問reqres的接口。 建立上下文對象調用了一個createContext函數,這個函數的定義以下:

/** * 建立一個context對象,也就是在中間件裏使用的ctx,並給ctx添加request, respone屬性 */
  createContext(req, res) {
    const context = Object.create(this.context); // 繼承自context.js中export出來proto
    const request = context.request = Object.create(this.request); // 把自定義的request做爲ctx的屬性
    const response = context.response = Object.create(this.response);// 把自定義的response做爲ctx的屬性
    context.app = request.app = response.app = this;
    // 爲了在ctx, request, response中,都能使用httpServer回調函數中的req和res
    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;
  }
複製代碼

ctx對象其實是繼承自context模塊中定義的proto對象,同時添加了requestresponse兩個屬性。requestresponse也是對象,分別繼承自request.jsresponse.js定義的對象。這兩個模塊的功能是基於原生的reqres封裝了一些gettersetter,原理比較簡單,下面就再也不分析了。

咱們重點來看看context模塊。

const proto = module.exports = {

  inspect() {
    if (this === proto) return this;
    return this.toJSON();
  },
  
  toJSON() {
    return {
      request: this.request.toJSON(),
      response: this.response.toJSON(),
      app: this.app.toJSON(),
      originalUrl: this.originalUrl,
      req: '<original node req>',
      res: '<original node res>',
      socket: '<original node socket>'
    };
  },

  assert: httpAssert,

  throw(...args) {
    throw createError(...args);
  },
  
  onerror(err) {
    if (null == err) return;
    if (!(err instanceof Error)) err = new Error(util.format('non-error thrown: %j', err));
    let headerSent = false;
    if (this.headerSent || !this.writable) {
      headerSent = err.headerSent = true;
    }
    // delegate
    this.app.emit('error', err, this);
    if (headerSent) {
      return;
    }
    const { res } = this;
    // first unset all headers
    /* istanbul ignore else */
    if (typeof res.getHeaderNames === 'function') {
      res.getHeaderNames().forEach(name => res.removeHeader(name));
    } else {
      res._headers = {}; // Node < 7.7
    }

    // then set those specified
    this.set(err.headers);

    // force text/plain
    this.type = 'text';

    // ENOENT support
    if ('ENOENT' == err.code) err.status = 404;

    // default to 500
    if ('number' != typeof err.status || !statuses[err.status]) err.status = 500;

    // respond
    const code = statuses[err.status];
    const msg = err.expose ? err.message : code;
    this.status = err.status;
    this.length = Buffer.byteLength(msg);
    this.res.end(msg);
  },

  get cookies() {
    if (!this[COOKIES]) {
      this[COOKIES] = new Cookies(this.req, this.res, {
        keys: this.app.keys,
        secure: this.request.secure
      });
    }
    return this[COOKIES];
  },

  set cookies(_cookies) {
    this[COOKIES] = _cookies;
  }
};
複製代碼

context模塊定義了一個proto對象,該對象定義了一些方法(eg: throw)和屬性(eg: cookies)。咱們上面經過createContext函數建立的ctx對象,就是繼承自proto。所以,咱們能夠在中間件中直接經過ctx訪問proto中定義的方法和屬性。

值得一提的點是,做者經過代理的方式,讓開發者能夠直接經過ctx[propertyName]去訪問ctx.requestctx.response上的屬性和方法。

實現代理的關鍵邏輯

/** * 代理response一些屬性和方法 * eg: proto.response.body => proto.body */
delegate(proto, 'response')
  .method('attachment')
  .method('redirect')
  .access('body')
  .access('length')
  // other properties or methods
  
/** * 代理request的一些屬性和方法 */
delegate(proto, 'request')
  .method('acceptsLanguages')
  .method('acceptsEncodings')
  .method('acceptsCharsets')
  .method('accepts')
  .method('get')
  // other properties or methods
複製代碼

實現代理的邏輯也很是簡單,主要就是使用了__defineGetter____defineSetter__這兩個對象方法,當setget對象的某個屬性時,調用指定的函數對屬性值進行處理或返回。

最終的請求與響應

ctx(上下文對象)和fn(執行中間件的合成函數)都準備好以後,就能真正的處理請求並響應了。該步驟調用了一個handleRequest函數。

handleRequest(ctx, fnMiddleware) {
    const res = ctx.res;
    res.statusCode = 404; // 狀態碼默認404
    const onerror = err => ctx.onerror(err);
    const handleResponse = () => respond(ctx);
    onFinished(res, onerror);
    // 執行完中間件函數後,執行handleResponse處理結果
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
  }
複製代碼

handleRequest函數會把ctx傳入fnMiddleware並執行,而後經過respond方法進行響應。這裏默認把狀態碼設爲了404,若是在執行中間件的過程當中有返回,例如對ctx.body進行負責,koa會自動把狀態碼設成200,這一部分的邏輯是在response對象的body屬性的setter處理的,有興趣的朋友能夠看一下response.js

respond函數會對ctx對象上的body或者其餘屬性進行分析,而後經過原生的res.end()方法將不一樣的結果輸出。

最後

到這裏,koa2的核心代碼大概就分析完啦。以上是我我的總結,若有錯誤,請見諒。歡迎一塊兒交流學習!

相關文章
相關標籤/搜索