閱讀koa源碼, Koa中間件是如何工做的(DAY 1)

圖片描述

Koa中的中間件不一樣於Express,Koa使用了洋蔥模型。神奇的Koa框架僅僅只包含了4個文件。今天咱們只看一下主文件—application.js,它包含了中間件如何工做的核心邏輯。node

圖片描述

準備

git clone git@github.com:koajs/koa.git
npm install

而後咱們在項目根目錄添加一個index.js文件,做測試用途。git

// index.js
// Include the entry file of koa
const Koa = require('./lib/application.js');
const app = new Koa();
const debug = require('debug')('koa');
app.use(async (ctx, next) => {
  console.log(1);
  await next();
  console.log(6);
  const rt = ctx.response.get('X-Response-Time');
  console.log(`${ctx.method} ${ctx.url} - ${rt}`);
});
// time logger here
app.use(async (ctx, next) => {
  console.log(2);
  const start = Date.now();
  await next();
  console.log(5);
  const ms = Date.now() - start;
  ctx.set('X-Response-Time', `${ms}ms`);
});
app.use(async (ctx, next) => {
  console.log(3);
  ctx.body = 'Hello World';
  await next();
  console.log(4);
});


app.listen(3000);

運行服務器:github

node index.js

訪問http://localhost:3000,你將看到1, 2, 3, 4, 5, 6的輸出,這叫作洋蔥模型(中間件)npm

洋蔥模型如何工做

讓咱們來看看Koa的核心代碼,瞭解一下中間件的工做原理。在index.js文件中,咱們能夠這樣使用中間件:api

const app = new Koa();
app.use(// middleware);
app.use(// middleware);
app.listen(3000);

而後再來看看application.js,下面的代碼是和中間件有關的,我在代碼中加了一下備註。promise

const compose = require('koa-compose');

module.exports = class Application extends Emitter {
  
  constructor() {
    super();
    this.proxy = false;
    // Step 0: init a middleware list
    this.middleware = [];
  }

  use(fn) {
    // Step 1: adding the middleware to the list
    this.middleware.push(fn);
    return this;
  }

  listen(...args) {
    debug('listen');
    // Step 2: using this.callback() to compose all middleware
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }

  callback() {
    // Step 3: This is the most important part - compose, it group all 
    // middleware to one big function and return a promise, we will talk more
    // about this function
    const fn = compose(this.middleware);
    if (!this.listenerCount('error')) this.on('error', this.onerror);
    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res);
      return this.handleRequest(ctx, fn);
    };

    return handleRequest;
  }

  handleRequest(ctx, fnMiddleware) {
    const res = ctx.res;
    res.statusCode = 404;
    const onerror = err => ctx.onerror(err);
    const handleResponse = () => respond(ctx);
    onFinished(res, onerror);
    // Step 4: Resolve the promise
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
  }
}

關於Compose函數

關於compose函數的更多信息,咱們來看看koa-compose服務器

module.exports = compose
function compose (middleware) {
  // skipped type checking code here
  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)
      }
    }
  }
}

全部的中間件都傳遞給compose函數,它將返回dispatch(0),dispatch函數將當即執行並返回一個promise。在咱們理解dispatch函數的內容前,咱們必須先了解promise的語法。app

關於Promise

一般咱們是這樣使用promise的:框架

const promise = new Promise(function(resolve, reject) {
  if (success){
    resolve(value);
  } else {
    reject(error);
  }
});

在Koa中,promise是這樣使用的:koa

let testPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('test success');
  }, 1000);
});
Promise.resolve(testPromise).then(function (value) {
  console.log(value); // "test success"
});

因此,咱們知道,在compose函數中,它返回一個promise

回到Koa - compose中間件

module.exports = compose
function compose (middleware) {
  // skipped type checking code here
  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)
      }
    }
  }
}

dispatch是一個遞歸函數,它將遍歷全部的中間件。在咱們的index.js文件中,咱們有三個中間件,這三個中間件將在await next()前執行代碼

app.use(async (ctx, next) => {
  console.log(2);
  const start = Date.now();
  await next(); // <- stop here and wait for the next middleware complete
  console.log(5);
  const ms = Date.now() - start;
  ctx.set('X-Response-Time', `${ms}ms`);
});

咱們能夠看看三個中間件的執行順序:

  • 當執行dispatch(0), Promise.resolve(fn(context, dispatch.bind(null,0+1))) 被執行
  • 第一個中間件運行至await next()
  • 第二個中間件是,next() = dispatch.bind(null, 0+1)
  • 第二個中間件運行至await next()
  • 第三個中間件是, next() = dispatch.bind(null, 1+1)
  • 第三個中間件運行至await next()
  • next() = dispatch.bind(null, 2+1), 沒有第四個中間件,當即返回 if(!fn) return Promise.resolve(), 在第三個中間件中的await next() 被 resolved, 並執行第三個中間件剩下的代碼
  • 在第二個中間件中的await next() 被resolve,並執行第二個中間件剩下的代碼
  • 在第一個中間件中的await next() 被resolve,並執行第一個中間件剩下的代碼

爲何使用洋蔥模型?

若是在中間件中有async/await,編碼會變得更加的簡單。當咱們想寫一個針對api請求的時間記錄器,將會是一件很是簡單的事:

app.use(async (ctx, next) => {
  const start = Date.now();
  await next(); // your API logic
  const ms = Date.now() - start;
  console.log('API response time:' + ms);
});
相關文章
相關標籤/搜索