博客 / 詳情

返回

koa學習筆記

我們從以下幾個方面來學習Koa

  • 創建應用程序函數
  • 擴展res和req
  • 中間件實現原理

創建應用程序函數

Koa 是依賴 node 原生的 http 模塊來實現 http server 的能力,原生 http 模塊可以通過幾行代碼就啓動一個監聽在 8080 端口的http服務,createServer 的第一個參數是一個回調函數,這個回調函數有兩個參數,一個是請求對象,一個是響應對象,可以根據請求對象的內容來決定響應數據的內容;

const http = require("http");

const server = http.createServer((req, res) => {
  // 每一次請求處理的方法
  console.log(req.url);
  res.writeHead(200, { "Content-Type": "text/plain" });
  res.end("Hello NodeJS");
});

server.listen(8080);

在 Koa 中,createServer 回調函數中的 req 和 res 會被保存到 ctx 對象上,伴隨整個處理請求的生命週期,Koa 源碼中的 request.js 和 response.js 就是對這兩個對象添加了大量便捷獲取數據和設置數據的方法,如獲取請求的方法、請求的路徑、設置返回數據體、設置返回狀態碼等操作。

而Koa在封裝創建應用程序的方法中主要執行了以下流程:

  • 組織中間件(監聽請求之前)
  • 生成context上下文對象
  • 執行中間件
  • 執行默認響應方法或者異常處理方法
// application.js

// 這個方法是封裝了http模塊提供的http.createServer和listen方法
listen(...args) {
  debug('listen');
  const server = http.createServer(this.callback());
  return server.listen(...args);
}

callback() {
  //組織中間件,在監聽請求之前完成的
  const fn = compose(this.middleware);
  if (!this.listenerCount('error')) this.on('error', this.onerror);
  const handleRequest = (req, res) => {
    //創建context上下文對象
    const ctx = this.createContext(req, res);
    return this.handleRequest(ctx, fn);
  };
  return handleRequest;
}

handleRequest(ctx, fnMiddleware) {
  const res = ctx.res;
  // 默認狀態碼為404
  res.statusCode = 404;
  const onerror = err => ctx.onerror(err);
  const handleResponse = () => respond(ctx);
  onFinished(res, onerror);
  // 執行中間件
  return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}

// 創建context上下文對象
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;
}

擴展res和req

NodeJS中原生的res和req是http.IncomingMessage和http.ServerResponse的實例,Koa中則是自定義request和response對象,保持對原生的res和req引用,然後通過getter和setter方法實現擴展。

// application.js

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; // 保存原生 req 對象
    context.res = request.res = response.res = res; // 保存原生 res 對象
    request.ctx = response.ctx = context;
    request.response = response; // response 拓展
    response.request = request; // request 拓展
    context.originalUrl = request.originalUrl = req.url;
    context.state = {};
    // 最終返回完整的context上下文對象
    return context;
}

在Koa中要區別這兩組對象:

  • request、response: Koa擴展的對象
  • res、req: NodeJS原生對象
// request.js
get header() {
  return this.req.headers;
},

set header(val) {
  this.req.headers = val;
}

此時已經可以採用這樣的方式訪問header屬性:

ctx.request.header

delegates 屬性代理

koa對response和request使用了屬性代理,使我們可以直接在context中使用request和response的方法,其中method方法是委託方法,getter方法用來委託getter,access方法委託getter+setter

// context.js
/**
 * Response delegation.
 */

delegate(proto, 'response')
  .method('attachment')
  .method('redirect')
  .method('remove')
  .method('vary')
  .method('has')
  .method('set')
  .method('append')
  .method('flushHeaders')
  .access('status')
  .access('message')
  .access('body')
  .access('length')
  .access('type')
  .access('lastModified')
  .access('etag')
  .getter('headerSent')
  .getter('writable');

/**
 * Request delegation.
 */

delegate(proto, 'request')
  .method('acceptsLanguages')
  .method('acceptsEncodings')
  .method('acceptsCharsets')
  .method('accepts')
  .method('get')
  .method('is')
  .access('querystring')
  .access('idempotent')
  .access('socket')
  .access('search')
  .access('method')
  .access('query')
  .access('path')
  .access('url')
  .access('accept')
  .getter('origin')
  .getter('href')
  .getter('subdomains')
  .getter('protocol')
  .getter('host')
  .getter('hostname')
  .getter('URL')
  .getter('header')
  .getter('headers')
  .getter('secure')
  .getter('stale')
  .getter('fresh')
  .getter('ips')
  .getter('ip');

delegates 實現

對於 setter 和 getter方法,是通過調用對象上的 __defineSetter__ 和 __defineGetter__ 來實現的

// delegates/index.js

// getter
Delegator.prototype.getter = function(name){
  var proto = this.proto;
  var target = this.target;
  this.getters.push(name);

  proto.__defineGetter__(name, function(){
    return this[target][name];
  });

  return this;
};

// setter
Delegator.prototype.setter = function(name){
  var proto = this.proto;
  var target = this.target;
  this.setters.push(name);

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

  return this;
};

// access
Delegator.prototype.access = function(name){
  return this.getter(name).setter(name);
};

// method
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;
};

中間件實現原理

在Koa中,通過app.use() 來註冊中間件,Koa支持三種不同類型的中間件:普通函數,async 函數,Generator函數,如果是Generator函數,那就用 convert 把函數包起來,然後在push到 this.middleware

  use(fn) {
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
    if (isGeneratorFunction(fn)) {
        fn = convert(fn);
    }
    debug('use %s', fn._name || fn.name || '-');
    this.middleware.push(fn);
    return this;
  }

convert作用

convert是用於將koa中以前基於generator寫法的中間件轉為基於promise寫法。convert()的源碼實現邏輯如下:

  1. convert 方法首先判斷傳入的中間件是否是一個函數,如果不是就拋出異常;
  2. 接着判斷是否是一個 generator 函數,如果不是就直接返回,不做處理;
  3. 利用co將 generator 函數形式的中間件轉成 promise 形式的中間件。

    function convert (mw) {
      if (typeof mw !== 'function') {
     throw new TypeError('middleware must be a function')
      }
      // assume it's Promise-based middleware
      if (
     mw.constructor.name !== 'GeneratorFunction' &&
     mw.constructor.name !== 'AsyncGeneratorFunction'
      ) {
     return mw
      }
      const converted = function (ctx, next) {
     return co.call(
       ctx,
       mw.call(
         ctx,
         (function * (next) { return yield next() })(next)
       ))
      }
      converted._name = mw._name || mw.name
      return converted
    }

    中間件的執行

    Koa中間件的執行流程主要通過koa-compose中的compose函數完成,基於洋葱圈模型:
    image.png
    koa-compose 的代碼很短,一共才不到50行,主要執行順序如下:

    // koa-compose 
    function compose (middleware) { //傳入middleware數組
      // 不是數組拋出異常
      if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
      // 判斷每個middleware中的每一項是否為函數
      // 不是函數拋出異常
      for (const fn of middleware) {
     if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
      }
      // 返回一個函數
      return function (context, next) {
     //index計數
     let index = -1
     return dispatch(0) //調用dispatch,從第一個中間件開始
     function dispatch (i) {
       // i小於index,證明在中間件內調用了不止一次的next(),拋出錯誤
       if (i <= index) return Promise.reject(new Error('next() called multiple times'))
       index = i // 更新index的值
       let fn = middleware[i] //middleware中的函數,從第i個開始
       if (i === middleware.length) fn = next //如果i走到最後一個的後面,就讓fn為next,此時fn為undefined
       if (!fn) return Promise.resolve()// 那麼這時候就直接resolve
       try {
        // 把下一個中間件作為當前中間件的next傳入
         return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
       } catch (err) {
         return Promise.reject(err)
       }
     }
      }
    }
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.