Koa2 原始碼學習(下)

Coder丶發表於2019-01-23

上文我們讀了koa原始碼中的application模組,瞭解其核心實現原理,其中在

// application.js

module.exports = class Application extends Emitter{
  ...
  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== 模組,同樣利用刪減法。

context.js

const proto = module.exports = {
  const createError = require(`http-errors`);
  const httpAssert = require(`http-assert`);
  const delegate = require(`delegates`);
  const statuses = require(`statuses`);
  ...
}
delegate(proto, `response`)
  .method(`attachment`)
  .method(`redirect`)
  .method(`remove`)
  ...

delegate(proto, `request`)
  .method(`acceptsLanguages`)
  .method(`acceptsEncodings`)
  .method(`acceptsCharsets`)
  ...
複製程式碼

delegate 把 response 和 request 下面的方法和屬性都掛載到proto上,然後把它暴露給application,這裡的proto就是context。

// delegator

function Delegator(proto, target) {
  if (!(this instanceof Delegator)) return new Delegator(proto, target);
  this.proto = proto;
  this.target = target;
  this.methods = [];
  this.getters = [];
  this.setters = [];
  this.fluents = [];
}

Delegator.prototype.method = function(name){
  var proto = this.proto;
  var target = this.target;
  this.methods.push(name);

  proto[name] = function(){
    return this[target][name].apply(this[target], arguments);
  };

  return this;
};
複製程式碼

Delegator 函式傳入proto和target並分別快取,然後呼叫method方法,把所有的方法名push到methods陣列裡,同時對proto下每一個傳入的方法名配置成一個函式,函式內部是具體目標物件的方法。詳細原始碼請看node-delegates

// application.js

module.exports = class Application extends Emitter{
  ...
  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;
  }
}
複製程式碼

Object.create 傳入了context暴露出的proto,proto作為指定的原型物件在它的原有基礎上生成新的物件(context),同時request和response也利用Object.create建立一個新的物件並把它掛載到context上。這樣,在context不僅能訪問到request response內部的方法屬性外還能訪問它們自身。

然後context,req,res互相掛載,這樣就能很便利的去訪問他們內部的方法和屬性。

Object.create 解釋看這裡Object.create

總結: content.js 主要就是提供了對request和response物件的方法與屬性便捷訪問能力。

request.js

// request.js

module.exports = {

  get header() {...},

  set header(val) {...},

  get headers() {...},

  set headers(val) {...},

  get url() {...},

  set url(val) {...},

  get origin() {...},

  get href() {...}

  ...
};
複製程式碼

從程式碼我們可以看到,request.js 封裝了請求相關的屬性以及方法,再把物件暴露給application,通過 application.js 中的createContext方法,代理對應的 request 物件。

具體原始碼看這裡 request.js

response.js

和request.js一樣,封裝了響應相關的屬性以及方法,這裡就不貼程式碼了。

具體原始碼看這裡 response.js

中介軟體

接下來我們分析中介軟體,首先我們要先理解什麼是中介軟體,先來看段程式碼:

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

app.use(async (ctx, next) => {
  ctx.type = `text/html; charset=utf-8`
  ctx.body = `hello world`
})

app.listen(8081)

複製程式碼

在 koa 中,要應用一箇中介軟體,我們使用 app.use(),我們要理解一個概念,就是在koa中,一切皆是中介軟體。再來一段程式碼:

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

const mid1 = async(ctx, next) => {
  ctx.body = `Hello `
  await next()
  ctx.body = ctx.body + `OK`
}

const mid2 = async(ctx, next) => {
  ctx.type = `text/html; charset=utf-8`
  await next()
}

const mid3 = async(ctx, next) => {
  ctx.body = ctx.body + `World `
  await next()
}

app.use(mid1)
app.use(mid2)
app.use(mid3)

app.listen(8085)

複製程式碼

列印出==Hello World OK==,從執行結果來看,首先執行mid1中的程式碼,在遇到await next()之後會把控制權交給下一個中介軟體處理,直到所有的中介軟體都執行完畢,然後再回來繼續執行剩下的業務程式碼。到這裡我們就對koa的中介軟體執行特點有所瞭解了。

// application

use(fn) {
  ...
  this.middleware.push(fn);
  return this;
}
複製程式碼

在前面的程式碼中,我們看到中介軟體在使用過程中會不斷加到堆疊中,執行順序也會按照先進先出的原則執行。但是koa中介軟體為什麼可以依次執行?並在執行過程中可以暫停下來走後面的流程然後再回來繼續執行?這裡我們就要用到koa-compose了。

compose這裡用到了純函式,關於純函式可以去看下函數語言程式設計相關概念,首先純函式無副作用,既不依賴,也不會改變全域性狀態。這樣函式之間可以達到自由組合的效果。

我們先用一段js程式碼來模擬下這個執行原理

function tail(i) {
  if(i > 3) return i
  console.log(`修改前`, i);

  return arguments.callee(i + 1)
}
tail(0)
// 修改前 0
// 修改前 1
// 修改前 2
// 修改前 3
複製程式碼

通過這種方式在每次呼叫的時候把這個函式的執行返回,它執行後的結果就是下一次呼叫的入參,這個返回的函式負責執行下一個流程,一直執行到邊界條件為止。

然後再看compose核心程式碼

// koa-compose

module.exports = compose

function compose (middleware) { // 接收中介軟體函式陣列
  if (!Array.isArray(middleware)) throw new TypeError(`Middleware stack must be an array!`) // 判斷入參middleware是否為陣列
  for (const fn of middleware) { // 判斷陣列內每一項是否是function
    if (typeof fn !== `function`) throw new TypeError(`Middleware must be composed of functions!`)
  }

  return function (context, next) { // 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`)) // 判斷next是否多次呼叫
      index = i
      let fn = middleware[i] // 下表為0,預設第一個中介軟體
      if (i === middleware.length) fn = next // 說明已呼叫到最後一箇中介軟體,這裡next為undified
      if (!fn) return Promise.resolve() // next取反為true,直接返回一個程式碼執行完畢的resolve
      try {
        return Promise.resolve(fn(context, function next () {
          return dispatch(i + 1) //遞迴呼叫,next將結果傳遞給下一個中介軟體
        }))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

複製程式碼

可以看到compose是一個閉包函式,返回匿名函式再執行的最終結果返回的是一個promise物件。

compose內部儲存了所有的中介軟體,通過遞迴的方式不斷的執行中介軟體。

再回到application來看

// application.js
 
callback() {
  const fn = compose(this.middleware);
  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);
  return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
複製程式碼

fnMiddleware 是通過 handleResponse 傳入下來的,然後在callback回撥執行的時候生成上下文物件ctx,然後把ctx傳給了handleRequest,另一個引數fn就是compose處理之後返回的匿名函式,對應就是compose裡return Promise.resolve(fn(context, function next (){} 這裡的context和next。

fnMiddleware第一次執行的時只傳入了ctx,next為undified,對應的就是compose裡直接return dispatch(0),這時候還沒有執行第一個中介軟體,在它內部才傳入了next。

compose的作用其實就是把每個不相干的中介軟體串在一起,然後來組合函式,把這些函式串聯起來依次執行,上一個函式的輸出結果就是下一個函式的入參。

總結

Compose 是一種基於 Promise 的流程控制方式,可以通過這種方式對非同步流程同步化,解決之前的巢狀回撥和 Promise 鏈式耦合。

–至此koa2的原始碼學習就全部完成了–

相關文章