1. 程式人生 > >koa原始碼學習筆記

koa原始碼學習筆記

我們的koa程式可以簡化為以下三步

const app = new koa()
app.use(middleware)
app.listen(port)

搞清楚這三步分別做了什麼?讓我們翻開koa原始碼一看究竟.開啟node_modules/koa/package.json,可以看到程式的入口

"main": "lib/application.js"

application.js主要做了三件事

  1. 建立app物件,該物件繼承了Emitter,並且繼承了context,req,res物件
module.exports = class Application extends Emitter {
  constructor() {
    super();
    this.middleware = []; //中介軟體陣列
    this.context = Object.create(context); //context掛載到app
    this.request = Object.create(request); //request掛載到app
    this.response = Object.create(response); //response掛載到app
  }
 }

分別來看看這lib下的context.js, request.js, response.js三個檔案

//  context.js 就暴露了一個物件
const proto = module.exports = {

};

// 使用delegate將request和response物件掛載到proto上
delegate(proto, 'response')  // 這裡
  .method('attachment')
  .method('flushHeaders')
  ... 省略若干 .....
  .access('status')
  .getter('writable');
  
  delegate(proto, 'request') // 這裡
  .method('acceptsLanguages')
  .method('acceptsEncodings')
    ... 省略若干 .....
  .access('querystring')
  .getter('ip');

delegate來自於node_modules/delegates/index.js,也很簡單,就定義了5個方法method, access, getter, setter, fluent

function Delegator(proto, target) {
  this.proto = proto;
  this.target = target;
}
// method, 通過apply將this繫結在target,也就是上面的request和response物件上
Delegator.prototype.method = function(name){
  var proto = this.proto;
  var target = this.target;
  proto[name] = function(){
    return this[target][name].apply(this[target], arguments);
  };
  return this;
};

request.js和response.js:裡面都是一系列的getter setter方法, 繼承自req和res物件

//request.js
module.exports = {
  get header() {
    return this.req.headers;
  },
  set header(val) {
    this.req.headers = val;
  },
  get url() {
    return this.req.url;
  },
  set url(val) {
    this.req.url = val;
  },
  ... 省略若干 .....
}

因此有

ctx.body = "Hi Allen" //實際上呼叫的是response.js裡面的set body
  1. use的時候將回調函式push到middlewares中介軟體陣列中
  use(fn) {
    this.middleware.push(fn);
    return this;
  }
  1. listen的時候,建立http server,監聽埠,當埠發生變化時候執行相應的回撥,
  listen(...args) {
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }

我們來看下this.callback,該函式執行的時候返回handleRequest,handleRequest首先建立了呼叫了createContext,將req和res上的方法屬性掛在了ctx上, 其次呼叫handleRequest將ctx物件傳給中介軟體函式,

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

這裡的this.context來自於context.js檔案,context.js用引入delegate.js,因此,原型鏈查詢的方向是context->context.js暴露出來的物件->delegate.js暴露出來的物件, 可以看到context可以直接訪問request和response上的屬性,也可以通過context.request來訪問, (至於為什麼要有request.response, response.request這一步有些疑惑,還望高人指出)

  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.state = {};
    return context;
  }

handleResponse在中介軟體執行結束之後再執行

  handleRequest(ctx, fnMiddleware) {
    const res = ctx.res;
    const handleResponse = () => respond(ctx);
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
  }

去除掉引數校驗之後,response返回客戶端的需要的資料

function respond(ctx) {
  const res = ctx.res;
  let body = ctx.body;
  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);
}

中介軟體的執行順序問題: 按use的順序執行,碰到next()就傳遞控制權,next執行完之後再返回執行

const koa = require('koa')
const logger = require('koa-logger')
const app = new koa();

const indent = (n)=> {
    return new Array(n).join('&nbsp')
}


const mid1 = () => {
    return async (ctx, next) => {
        ctx.body = `<h3>請求 => 第一層中介軟體</h3>`
        await next()
        ctx.body += `<h3>響應 => 第一層中介軟體</h3>`
        
    }
}

const mid2 = () => {
    return async (ctx, next) => {
        ctx.body += `<h3>${indent(4) }請求 => 第二層中介軟體</h3>`
        await next()
        ctx.body += `<h3>${ indent(4)} 響應 => 第二層中介軟體</h3>`
        
    }
}

const mid3 = () => {
    return async (ctx, next) => {
        ctx.body += `<h3>${ indent(8)}請求 => 第三層中介軟體</h3>`
        await next()
        ctx.body += `<h3>${ indent(8)} 響應 => 第三層中介軟體</h3>`
        
    }
}

app.use(logger())
app.use(mid1())
app.use(mid2())
app.use(mid3())
app.use((ctx,next) =>  {
    ctx.body += `<p style='color: red'>${indent(12)}koa 核心業務處理'</p>`
})

app.listen(3001)

返回的結果是這樣的,類似一種"U型結構"

請求 => 第一層中介軟體
   請求 => 第二層中介軟體
       請求 => 第三層中介軟體
       
           koa 核心業務處理'
           
        響應 => 第三層中介軟體
    響應 => 第二層中介軟體
響應 => 第一層中介軟體

koa中的中介軟體之所以有這樣的能力,在於koa-compose的包裝.koa-compose使用的遞迴的形式進行呼叫,並且使用尾遞迴進行了優化.翻開koa-compose/index.js的原始碼

function compose (middleware) {
  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)
      }
    }
  }
}

掌握koa等框架並不難,難的是http協議,資源,請求流程的優化設定,這些屬於網路通訊的硬知識,需要花時間去學習掌握