一步一步實現koa

原文連接javascript

簡介

koa 是由 Express 原班人馬打造的,相比 Express 的大而全,koa 致力於成爲一個更小、更富有表現力、更健壯的 Web 框架,適合做爲 web 服務框架的基石。java

koa1 經過組合不一樣的 generator,能夠避免嵌套地獄,並極大地提高錯誤處理的效率。koa2 使用了最新的 async await generator 語法糖,使得開發更高效。node

koa 不在內核方法中綁定任何中間件,但確很輕易集成中間件,只須要 use 方法傳入一箇中間件函數,就能方便獲取請求響應等上下文信息和下一個中間件,使得中間件的使用井井有理。git

概覽

koa 源碼在 lib 文件下四個文件中,接下來一一介紹每一個模塊文件的內容。github

lib/
├── application.js
├── context.js
├── request.js
└── response.js
複製代碼
  • application.js 導出一個類函數,用來生成koa實例。該類派生 node events,方便錯誤處理。
  1. use() 添加訂閱中間件,內部使用一個數組維護中間件;
  2. listen() node http 起一個服務;
  3. callback() 返回一個 http 服務回調函數 cb。
    1. compose 處理中間件數組,返回一個函數 fnMiddleware。內部 promise 化中間件,遞歸調用使得中間件拿到上下文 ctx 和下一個中間件 next 並順序執行;
    2. createContext 在 cb 中接收 http 請求的回調參數 req、res,使得 application實例、context、request、response 可以相互訪問 req、res,每次返回一個新的 context;
    3. handleRequest 最終執行 fnMiddleware,中間件無錯誤後調用私有函數 respond 返回響應。
  • context.js 導出一個對象,主要功能有:錯誤處理、cookie 處理、代理 request.js、response.js 上的屬性和方法(例如:訪問ctx.url,實際上是訪問了 request.url,又其實訪問了node http req.url)。
  • request.js 導出一個對象,封裝處理了 node 原生 http 的請求 req ,方便獲取設置 req,避免直接與 req 打交道。
  • response.js 導出一個對象,封裝處理了 node 原生 http 的響應 res ,方便獲取設置 res,避免直接與 res 打交道。

使用例子

  • 起一個簡單的服務
const Koa = require('koa');
const app = new Koa();

app.listen(3000);
複製代碼

實際上是如下的語法糖web

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

http.createServer(app.callback()).listen(3000);
複製代碼
  • 使用中間件處理 node http 請求、響應
const Koa = require('koa');
const app = new Koa();

// logger 中間件
app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  console.log(`${ctx.method} ${ctx.url} - ${ms}`);
});

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

app.listen(3000);
複製代碼

logger 中間件 await next() 時會暫停下面代碼的執行,直到 response 中間件執行完畢。npm

注意到 response 沒有執行 next,此時已沒有下一個中間件,但即便執行也不會報錯,由於內部處理爲一個 Promise.resolve 的 promise。數組

注意在一箇中間件中屢次(2次及以上)執行 next() 會報錯。promise

若是 logger 中間件不執行 next,那麼 response 中間件不會被執行。也即 ctx.body 不會執行,application 中的 handleRequest 默認設置node http res.statusCode = 404,npm statuses 中維護了經常使用的 code 碼文本提示音,例如 404: Not Found瀏覽器

ctx.body 實際上是調用了 koa response 對象的 body set 方法,賦值給 _body 屬性而且根據值設置 http 狀態碼。最後是在中間件 resolve 後調用 application 中的私有 respond 函數,執行了 node http res.end()。

動手實現一個精簡的 koa

骨架

  • application.js 須要起服務,因此須要引入node http模塊;須要發佈訂閱一些消息,因此須要繼承node events模塊。剩餘引入其它三個文件的模塊。
const http = require('http');
const Emitter = require('events');
const context = require('./context');
const request = require('./request');
const response = require('./response');

class Koa extends Emitter {
  constructor() {
    super();
  }
  
  listen() {}
  
  use() {}
  
  callback() {}
  
  handleRequest() {}
  
  createContext() {}
}

module.exports = Koa;
複製代碼
  • context.js
let proto = {};

module.exports = proto;
複製代碼
  • request.js
const request = {};

module.exports = request;
複製代碼
  • response.js
const response = {};

module.exports = response;
複製代碼

第一步,接收一箇中間功能

  • 構造函數,其它三個對象都能被 app 實例訪問。
constructor() {
  super();
  this.context = Object.create(context);
  this.request = Object.create(request);
  this.response = Object.create(response);
  this.fn = null;
}
複製代碼

簡單提一下爲何要使用 Object.create,例如避免改動 this.context.x 而影響 context.x (除非你 this.context.__proto__.x,顯然沒人會刻意這麼去作)。

if(!Object.create) {
  Object.create = function(proto) {
    function F(){}
    F.prototype = proto;
    return new F;
  }
}
複製代碼
  • listen,語法糖方便起 http 服務。
listen(...args) {
  const server = http.createServer(this.callback());
  return server.listen(...args);
}
複製代碼
  • use,訂閱中間件,暫時只能訂閱一個。
use(fn) {
  this.fn = fn;
  return this;
}
複製代碼
  • callback,處理中間件,而且返回一個接收 node http req,res 的回調函數。 每次接收一個 http 請求時,都會使用 koa createContext 根據當前請求環境新建上下文。
callback() {
  return (req, res) => {
    const ctx = this.createContext(req, res);
    return this.handleRequest(ctx);
  };
}
複製代碼
  • handleRequest,執行中間件,和響應 http 請求。
handleRequest(ctx) {
  this.fn(ctx);
  ctx.res.end(ctx.body);
}
複製代碼
  • createContext,每次處理一個 http 請求都會根據當前請求的 req、res 來更新相關內容。 一系列賦值操做,主要爲了新生成得 context、request、response 能夠相互訪問,且能訪問 koa app 實例和 http req、res。
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;
  return context;
}
複製代碼
  • request.js,簡單給 koa request 對象添加幾個處理 url 的方法
const parse = require('parseurl');

const request = {
  get url() {
    return this.req.url;
  },
  get path() {
    return parse(this.req).pathname;
  },
  get query() {
    return parse(this.req).query;
  }
};
複製代碼
  • response.js,這裏只添加一個設置響應 body 的方法
const response = {
  get body() {
    return this._body;
  },
  set body(val) {
    this.res.statusCode = 200;
    this._body = val;
  }
};
複製代碼
  • 主文件 index.js
const Koa = require('./application');
const app = new Koa();

app.use(ctx => {
  console.log(ctx.req.url);
  console.log(ctx.request.req.url);
  console.log(ctx.response.req.url);
  console.log(ctx.request.url);
  console.log(ctx.request.path);
  console.log(ctx.request.query);
  console.log(ctx.url);
  console.log(ctx.path);
  console.log(ctx.query);
  ctx.body = 'hello world';
});

app.listen(3000);
複製代碼
  • node index.js,瀏覽器輸入 localhost:3000/path?x=1&y=2,console 輸出
/path?x=1&y=2
/path?x=1&y=2
/path?x=1&y=2
/path?x=1&y=2
/path
x=1&y=2
undefined
undefined
undefined
複製代碼

能夠看出,可使用 koa context、request、response 來訪問 node req 的屬性,也能夠直接訪問 request 對象上定義的方法。

建議是避免操做 node http 的 req 或 res。

衆所周知,koa 是支持 context 實例代理訪問 koa request、response 上的方法的。

第二步,實現 context 代理

  • context.js,代理訪問 koa request、response 上的方法

koa 使用了 __defineSetter____defineGetter__ 來實現,提示這兩個方法已被標準廢棄,這裏使用 Object.defineProperty 來實現。

注意 Object.defineProperty 只設置 get 方法 enumerableconfigurable 默認都是 false

function defineGetter(prop, name) {
  Object.defineProperty(proto, name, {
    get() {
      return this[prop][name];
    },
    enumerable: true,
    configurable: true,
  });
}

function defineSetter(prop, name) {
  Object.defineProperty(proto, name, {
    set(val) {
      this[prop][name] = val;
    },
    enumerable: true,
    configurable: true,
  });
}

defineGetter('request', 'url');
defineGetter('request', 'path');
defineGetter('request', 'query');

defineGetter('response', 'body');
defineSetter('response', 'body');
複製代碼
  • 重啓服務,console.log 輸出
/path?x=1&y=2
/path?x=1&y=2
/path?x=1&y=2
/path?x=1&y=2
/path
x=1&y=2
/path?x=1&y=2
/path
x=1&y=2
複製代碼

ctx.body = 'hello world' 也不是新添加屬性,而是訪問 response 上的 body set 方法。

第三步,接收多個同步中間件

constructor() {
- this.fn = null;
+ this.middleware = [];
}

use(fn) {
- this.fn = fn;
+ this.middleware.push(fn);
}
複製代碼
  • 新增 compose,實現洋蔥圈模型
function compose(middleware) {
  return function (context, next) {
    let index = -1;
    return dispatch(0);
    function dispatch(i) {
      if(i <= index) throw new Error('next() 在中間件中被調用2次以上');
      index = i;
      let fn = middleware[i];
      if(i === middleware.length) fn = next;
      if(!fn) return;
      return fn(context, dispatch.bind(null, i + 1));
    }
  }
}
複製代碼
callback() {
+ const fn = compose(this.middleware); 
  return (req, res) => {
    const ctx = this.createContext(req, res);
- return this.handleRequest(ctx);
+ return this.handleRequest(ctx, fn);
  };
}

- handleRequest(ctx) {
+ handleRequest(ctx, fnMiddleware) {
- this.fn(ctx);
+ fnMiddleware(ctx);
  ctx.res.statusCode = 200;
  ctx.res.end(ctx.body);
}
複製代碼
  • index.js,就能使用多箇中間件和 next 了
app.use((ctx, next) => {
  console.log(ctx.url);
  next();
});

app.use((ctx, next) => {
  ctx.body = 'hello world';
  next();
});
複製代碼

第四步,異步洋蔥圈模型

  • 改造 compose,支持異步
function compose(middleware) {
  return function (context, next) {
    let index = -1;
    return dispatch(0);
    function dispatch(i) {
- if(i <= index) throw new Error('next() 在中間件中被調用2次以上');
+ if(i <= index) return Promise.reject(new Error('next() 在中間件中被調用2次以上'));
      index = i;
      let fn = middleware[i];
      if(i === middleware.length) fn = next;
- if(!fn) return;
+ if(!fn) return Promise.resolve();
- return fn(context, dispatch.bind(null, i + 1));
+ try {
+ return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
+ } catch (err) {
+ return Promise.reject(err);
+ }
    }
  }
}
複製代碼
handleRequest(ctx, fnMiddleware) {
- fnMiddleware(ctx);
- ctx.res.statusCode = 200;
- ctx.res.end(ctx.body);
+ fnMiddleware(ctx).then(() => {
+ ctx.res.statusCode = 200;
+ ctx.res.end(ctx.body);
+ });
}
複製代碼
  • index.js 異步洋蔥圈
app.use(async (ctx, next) => {
  await new Promise(resolve => {
    setTimeout(() => {
      console.log(ctx.url);
      resolve();
    }, 500);
  });
  next();
});

app.use((ctx, next) => {
  ctx.body = 'hello world';
  next();
});
複製代碼

這樣一個簡單的 koa 的主要功能就實現了,行文爲了簡單,不少錯誤處理等細節都忽略了,這在正式的產品中是大忌,但願當心謹慎。

相關文章
相關標籤/搜索