node koa原始碼解釋

qq_43604182發表於2020-12-20

再次思考:從瀏覽器輸入 URL 到頁面展示過程的過程中發生了什麼?

通過前面的基礎學習,我們瞭解了基於 Web 的應用基本流程:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-LWuReu4F-1608449778043)(./assets/e9ac4df2f8db06466fee94cad4401522.svg)]

通過上圖不難發現,無論具體應用功能如何變化, 服務端 處理任務核心三個步驟:③、④、⑤ 中,③ 和 ⑤ 的模式基本是固定的(因為HTTP協議規範了),而 ④ 是最大的變數。

如果我們每次開發一個新的應用都要把 ③ 和 ⑤ 的邏輯重新實現一遍就會特別的麻煩。所以,我們可以封裝一個框架(庫)把 ③ 和 ⑤ 的邏輯進行統一處理,然後通過某種方式,把 ④ 的處理暴露給框架使用者。

Koa

資源:

官網:https://koajs.com/

中文:https://koa.bootcss.com/

  • 基於 NodeJS 的 web 框架,致力於 web 應用和 API 開發。
  • 由 Express 原班人馬打造。
  • 支援 async。
  • 更小、更靈活、更優雅。

安裝

當前最新 Koa 依賴 node v7.6.0+、ES2015+ 以及 async 的支援。

具體請關注官網說明(依賴會隨著版本的變化而變化)。

參考:https://koajs.com/#introduction

# 安裝 koa
npm i koa

# 或者
yarn add koa

核心

KoaNodeJS 原生 IncomingMessageServerResponse 物件和解析響應通用流程進行了包裝,並提供了幾個核心類(物件)用於其它各種使用者業務呼叫。

  • Application 物件
  • Context 物件
  • Request 物件
  • Response 物件

Application 物件

該物件是 Koa 的核心物件,通過該物件來初始化並建立 WebServer

/**
 * File: /node_modules/koa/lib/application.js
***/

constructor(options) {
  super();
  options = options || {};
  this.proxy = options.proxy || false;
  this.subdomainOffset = options.subdomainOffset || 2;
  this.proxyIpHeader = options.proxyIpHeader || 'X-Forwarded-For';
  this.maxIpsCount = options.maxIpsCount || 0;
  this.env = options.env || process.env.NODE_ENV || 'development';
  if (options.keys) this.keys = options.keys;
  this.middleware = [];
  this.context = Object.create(context);
  this.request = Object.create(request);
  this.response = Object.create(response);
  if (util.inspect.custom) {
    this[util.inspect.custom] = this.inspect;
  }
}

建構函式對 Application 建立進行了一些初始化工作,暫時不需要關注這裡的太多細節,後續關注。

應用程式碼

/**
 * File: /app.js
***/

const Koa = require('koa');

const app = new Koa();

listen 方法

WebServer 並不是在 Application 物件建立的時候就被建立的,而是在呼叫了 Application 下的 listen 方法的時候在建立。

/**
 * File: /node_modules/koa/lib/application.js
***/

listen(...args) {
  debug('listen');
  const server = http.createServer(this.callback());
  return server.listen(...args);
}

通過原始碼可以看到,其本質還是通過 NodeJS 內建的 http 模組的 createServer 方法建立的 Server 物件。並且把 this.callback() 執行後的結果(函式)作為後續請求的回撥函式。

/**
 * File: /node_modules/koa/lib/application.js
***/

callback() {
  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;
}

通過上述程式碼的分析,實際上請求執行的回撥函式式 callback 返回的 handleRequest 函式,且該函式接收的 reqres 引數就是 NodeJSHTTP 模組內建的兩個物件 IncomingMessageServerResponse 物件。其中:

const ctx = this.createContext(req, res);

這裡, Koa 會呼叫 Application 物件下的 createContext 方法對 reqres 進行包裝,生成 Koa 另外一個核心物件: Context 物件 - 後續分析。

return this.handleRequest(ctx, fn);

接著呼叫 Application 物件下的 handleRequest 方法進行請求處理,並傳入:

  • ctx: 前面提到的 Context 物件。
  • fn: 這個實際上是 const fn = compose(this.middleware); 這段程式碼得到的是一個執行函式,這裡又稱為: 中介軟體函式

應用程式碼

/**
 * File: /app.js
***/

const Koa = require('koa');

const app = new Koa();

app.listen(8888);

中介軟體函式

所謂的中介軟體函式,其實就是一開始我們提到的 ④,首先, Application 物件中會提供一個屬性 this.middleware = []; ,它是一個陣列,用來儲存 ④ 需要處理的各種業務函式。這些業務函式會通過 Application 下的 use 方法進行註冊(類似事件註冊)。

為什麼叫中介軟體

因為它是在 請求 之後, 響應 之前呼叫的函式,所以就叫它 中介軟體函式

響應流程處理

通過上述流程分析,可以看到,每一個請求都會執行到 Application 物件下的 handleRequest 方法。

/**
 * File: /node_modules/koa/lib/application.js
***/

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 就是一系列中介軟體函式執行後結果(一個 Promise 物件),當所有中介軟體函式執行完成以後,會通過 then 呼叫 handleResponse ,也就是呼叫了 respond 這個方法。

響應處理

/**
 * File: /node_modules/koa/lib/application.js
***/

function respond(ctx) {
  // allow bypassing koa
  if (false === ctx.respond) return;

  if (!ctx.writable) return;

  const res = ctx.res;
  let body = ctx.body;
  const code = ctx.status;

  // ignore body
  if (statuses.empty[code]) {
    // strip headers
    ctx.body = null;
    return res.end();
  }

  if ('HEAD' === ctx.method) {
    if (!res.headersSent && !ctx.response.has('Content-Length')) {
      const { length } = ctx.response;
      if (Number.isInteger(length)) ctx.length = length;
    }
    return res.end();
  }

  // status body
  if (null == body) {
    if (ctx.req.httpVersionMajor >= 2) {
      body = String(code);
    } else {
      body = ctx.message || String(code);
    }
    if (!res.headersSent) {
      ctx.type = 'text';
      ctx.length = Buffer.byteLength(body);
    }
    return res.end(body);
  }

  // responses
  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);
}

上面這個函式就是 Koa 在處理完各種中介軟體函式以後,最後進行響應的邏輯。

Koa 的流程

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-PnyjrWVB-1608449778045)(./assets/lake_card_mindmap.png)]

中介軟體

首先, Application 物件通過一個陣列來儲存中介軟體:

/**
 * File: lib/application.js
***/

constructor() {
    // ...
  this.middleware = [];
  // ...
}

註冊中介軟體函式

其次,Application 物件提供了一個 use 方法來註冊中介軟體函式:

/**
 * File: lib/application.js
***/
  
use(fn) {
  if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
  if (isGeneratorFunction(fn)) {
    deprecate('Support for generators will be removed in v3. ' +
              'See the documentation for examples of how to convert old middleware ' +
              'https://github.com/koajs/koa/blob/master/docs/migration.md');
    fn = convert(fn);
  }
  debug('use %s', fn._name || fn.name || '-');
  this.middleware.push(fn);
  return this;
}

應用程式碼

/**
 * File: /app.js
***/

const Koa = require('koa');

const app = new Koa();

app.use( (ctx) => {
  ctx.body = 'Hello!';
} );

app.listen(8888);

中介軟體的執行

中介軟體的執行實際來源另外一個獨立模組: koa-compose 提供的 compose 函式。

/**
 * File: lib/application.js
***/

callback() {
  const fn = compose(this.middleware);

  // ...
}
/**
 * Module: koa-compose
 * File: index.js
***/

function compose (middleware) {
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  for (const fn of middleware) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }

  /**
   * @param {Object} context
   * @return {Promise}
   * @api public
   */

  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 函式

組合 - 把多個函式組合成一個函式執行。

上面這個 compose 函式核心就在:

Promise.resolve(fn(context, dispatch.bind(null, i + 1)));

它會在執行當前中介軟體函式的時候,把下一個中介軟體函式作為當前中介軟體函式的第二個引數傳入(next)。這樣就可以實現對多箇中介軟體函式執行流程進行把控。

應用程式碼

/**
 * File: /app.js
***/

const Koa = require('koa');

const app = new Koa();

app.use(async ctx => {
   console.log('a');
});

app.use(async ctx => {
    console.log('b');
 });

 app.use(async ctx => {
    console.log('c');
 });

app.use(async ctx => {
    console.log('d');
});

app.listen(8888);

輸出

a

我們會發現,當我們訪問這個 WebServer 的時候,後端伺服器上列印的只有 a 。這是因為當第一個中介軟體函式執行以後,後續的中介軟體是需要通過當前執行中介軟體函式的第二個引數去顯式的呼叫才能執行的。

應用程式碼

/**
 * File: /app.js
***/

const Koa = require('koa');

const app = new Koa();

app.use(async (ctx, next) => {
   console.log('a');
   next();
});

app.use(async (ctx, next) => {
    console.log('b');
    next();
 });

app.use(async (ctx, next) => {
    console.log('c');
    next();
 });

app.use(async (ctx, next) => {
    console.log('d');
    next();
});

app.listen(8888);

輸出

a
b
c
d

通過以上的程式碼改造,我們會發現就實現了 abcd 的輸出了。

因為函式呼叫的棧(LIFO - Last In First Out - 後進先出)特性,所以:

應用程式碼

/**
 * File: /app.js
***/

const Koa = require('koa');

const app = new Koa();

app.use(async (ctx, next) => {
   console.log('a - start');
   next();
   console.log('a - end');
});

app.use(async (ctx, next) => {
    console.log('b - start');
    next();
    console.log('b - end');
 });

app.use(async (ctx, next) => {
    console.log('c - start');
    next();
    console.log('c - end');
 });

app.use(async (ctx, next) => {
        console.log('d - start');
    next();
    console.log('d - end');
});

app.listen(8888);

輸出

a - start
b - start
c - start
d - start
d - end
c - end
b - end
a - end

我們給這種特性現象起了一個很形象的名字:

洋蔥模型

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-WOEzcASV-1608449778047)(./assets/image.png)]

好處

框架這麼設計的好處在哪呢? - 擴充套件

應用程式碼

/**
 * File: /app.js
***/

const Koa = require('koa');

const app = new Koa();

app.use(async (ctx, next) => {
   // 這是我們開始的某個業務邏輯
  ctx.body = 'hello';
});

app.listen(8888);

現在,我們希望在不改變原有中介軟體邏輯的基礎上進行一些擴充套件,比如在現有 body 內容後面新增 ',kkb!' 這個字串,我們就可以利用中介軟體特性來進行擴充套件了:

應用程式碼

/**
 * File: /app.js
***/

const Koa = require('koa');

const app = new Koa();

// 在不改變原有中介軟體邏輯程式碼基礎上進行擴充套件
app.use(async (ctx, next) => {
  // 注意這裡,我們是要在原有中介軟體邏輯之後新增新的邏輯,所以先 呼叫 next。
  next();
  ctx.body += ', kkb!';
});

app.use(async (ctx, next) => {
   // 這是我們開始的某個業務邏輯
  ctx.body = 'hello';
});

app.listen(8888);

next 呼叫取決中介軟體的具體需求。放置在你想要呼叫的任何階段。

非同步的中介軟體

有的時候,我們的中介軟體邏輯中會包含一些非同步任務:

應用程式碼

/**
 * File: /app.js
***/

const Koa = require('koa');

const app = new Koa();

// 在不改變原有中介軟體邏輯程式碼基礎上進行擴充套件
app.use(async (ctx, next) => {
  // 注意這裡,我們是要在原有中介軟體邏輯之後新增新的邏輯,所以先 呼叫 next
  next();
  ctx.body += ', kkb!';
});

app.use(async (ctx, next) => {
    setTimeout(() => {
        ctx.body = 'hello';
    }, 1000);
});

app.listen(8888);

我們會發現,還不等定時器執行, Koa 就已經返回(處理響應了)。我們需要把任務包裝成 Promise 的:

應用程式碼

/**
 * File: /app.js
***/

const Koa = require('koa');

const app = new Koa();

// 在不改變原有中介軟體邏輯程式碼基礎上進行擴充套件
app.use(async (ctx, next) => {
  // 注意這裡需要使用 await 來處理非同步的任務
  await next();
  ctx.body += ', kkb!';
});

app.use(async (ctx, next) => {
    // 返回一個 Promise
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            ctx.body = 'hello';
            resolve();
        }, 1000);
    });
});

app.listen(8888);

輸出(客戶端-如:瀏覽器)

hello, kkb!

注意:這裡需要注意中介軟體的註冊順序!

應用程式碼

/**
 * File: /app.js
***/

const Koa = require('koa');

const app = new Koa();

app.use(async (ctx, next) => {
    // 返回一個 Promise
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            ctx.body = 'hello';
            resolve();
        }, 1000);
    });
});

// 在不改變原有中介軟體邏輯程式碼基礎上進行擴充套件
app.use(async (ctx, next) => {
  // 注意這裡需要使用 await 來處理非同步的任務
  await next();
  ctx.body += ', kkb!';
});

app.listen(8888);

輸出(客戶端-如:瀏覽器)

hello

Context 物件

Koa 的核心物件之一,它為 Koa 框架內部提供了重要的各種介面,同時也通過這個物件代理了 ApplicationRequestResponse 物件的訪問,簡而言之,後續框架的提供的各種方法都是通過該物件來完成的。

/**
 * File: lib/application.js
***/

constructor() {
    // ...
  this.context = Object.create(context);
  // ...
}

context 物件

這裡的 context 物件,來源於 lib/context.js ,提供一些基礎方法,同時對 RequestResponse 物件做了代理訪問:

/**
 * File: lib/context.js
***/
'use strict';

/**
 * Module dependencies.
 */

const util = require('util');
const createError = require('http-errors');
const httpAssert = require('http-assert');
const delegate = require('delegates');
const statuses = require('statuses');
const Cookies = require('cookies');

const COOKIES = Symbol('context#cookies');

/**
 * Context prototype.
 */

const proto = module.exports = {

  /**
   * util.inspect() implementation, which
   * just returns the JSON output.
   *
   * @return {Object}
   * @api public
   */

  inspect() {
    if (this === proto) return this;
    return this.toJSON();
  },

  /**
   * Return JSON representation.
   *
   * Here we explicitly invoke .toJSON() on each
   * object, as iteration will otherwise fail due
   * to the getters and cause utilities such as
   * clone() to fail.
   *
   * @return {Object}
   * @api public
   */

  toJSON() {
    return {
      request: this.request.toJSON(),
      response: this.response.toJSON(),
      app: this.app.toJSON(),
      originalUrl: this.originalUrl,
      req: '<original node req>',
      res: '<original node res>',
      socket: '<original node socket>'
    };
  },

  /**
   * Similar to .throw(), adds assertion.
   *
   *    this.assert(this.user, 401, 'Please login!');
   *
   * See: https://github.com/jshttp/http-assert
   *
   * @param {Mixed} test
   * @param {Number} status
   * @param {String} message
   * @api public
   */

  assert: httpAssert,

  /**
   * Throw an error with `status` (default 500) and
   * `msg`. Note that these are user-level
   * errors, and the message may be exposed to the client.
   *
   *    this.throw(403)
   *    this.throw(400, 'name required')
   *    this.throw('something exploded')
   *    this.throw(new Error('invalid'))
   *    this.throw(400, new Error('invalid'))
   *
   * See: https://github.com/jshttp/http-errors
   *
   * Note: `status` should only be passed as the first parameter.
   *
   * @param {String|Number|Error} err, msg or status
   * @param {String|Number|Error} [err, msg or status]
   * @param {Object} [props]
   * @api public
   */

  throw(...args) {
    throw createError(...args);
  },

  /**
   * Default error handling.
   *
   * @param {Error} err
   * @api private
   */

  onerror(err) {
    // don't do anything if there is no error.
    // this allows you to pass `this.onerror`
    // to node-style callbacks.
    if (null == err) return;

    if (!(err instanceof Error)) err = new Error(util.format('non-error thrown: %j', err));

    let headerSent = false;
    if (this.headerSent || !this.writable) {
      headerSent = err.headerSent = true;
    }

    // delegate
    this.app.emit('error', err, this);

    // nothing we can do here other
    // than delegate to the app-level
    // handler and log.
    if (headerSent) {
      return;
    }

    const { res } = this;

    // first unset all headers
    /* istanbul ignore else */
    if (typeof res.getHeaderNames === 'function') {
      res.getHeaderNames().forEach(name => res.removeHeader(name));
    } else {
      res._headers = {}; // Node < 7.7
    }

    // then set those specified
    this.set(err.headers);

    // force text/plain
    this.type = 'text';

    // ENOENT support
    if ('ENOENT' == err.code) err.status = 404;

    // default to 500
    if ('number' != typeof err.status || !statuses[err.status]) err.status = 500;

    // respond
    const code = statuses[err.status];
    const msg = err.expose ? err.message : code;
    this.status = err.status;
    this.length = Buffer.byteLength(msg);
    res.end(msg);
  },

  get cookies() {
    if (!this[COOKIES]) {
      this[COOKIES] = new Cookies(this.req, this.res, {
        keys: this.app.keys,
        secure: this.request.secure
      });
    }
    return this[COOKIES];
  },

  set cookies(_cookies) {
    this[COOKIES] = _cookies;
  }
};

/**
 * Custom inspection implementation for newer Node.js versions.
 *
 * @return {Object}
 * @api public
 */

/* istanbul ignore else */
if (util.inspect.custom) {
  module.exports[util.inspect.custom] = module.exports.inspect;
}

/**
 * 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');

context 物件的初始化

通過上面程式碼,我們可以看到,在 Application 物件初始化的時候,會建立一個 Context 物件,並掛載到 Applicationcontext 屬性下。同時在中介軟體執行的時候,還會對這個 context 進行包裝,並把包裝後的 context 物件作為中介軟體函式的第一個引數進行傳入,所以我們就可以通過中介軟體函式的第一個引數來呼叫這個 context 物件了。

/**
 * File: lib/application.js
***/

callback() {
  // ...
  const ctx = this.createContext(req, res);
  return this.handleRequest(ctx, fn);
}
/**
 * File: lib/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;
  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;
}

ctx.state 屬性

有的時候,我們需要在多箇中介軟體中傳遞共享資料,雖然我們可以通過 context 物件進行掛載,但是這樣會對 context 物件造成汙染, context 物件為我們提供了一個專門用來共享這類使用者資料的名稱空間( context.state = {} )。

應用程式碼

/**
 * File: /app.js
***/

const Koa = require('koa');

const app = new Koa();

app.use(async (ctx, next) => {
  ctx.state.user = {id: 1, name: 'zMouse'};
});

app.use(async (ctx, next) => {
  ctx.body = `Hello, ${ctx.state.user.name}`;
});

app.listen(8888);

ctx.throw([status], [msg], [properties])

用來手動丟擲一個包含 狀態碼、狀態碼文字 以及 其它資訊的錯誤。狀態預設為:500。

應用程式碼

/**
 * File: /app.js
***/

const Koa = require('koa');

const app = new Koa();

app.use(async (ctx, next) => {
  ctx.throw(401, 'access_denied', { user: user });
});

app.use(async (ctx, next) => {
  ctx.body = 'kkb!';
});

app.listen(8888);

app.on(‘error’, callback)

配合著 Application 物件的 on 方法(繼承至 NodeJS 的 Emitter )來捕獲 throw 錯誤。

/**
 * File: lib/application.js
***/

module.exports = class Application extends Emitter {
  // ...
}

應用程式碼

/**
 * File: /app.js
***/

const Koa = require('koa');

const app = new Koa();

app.on('error', async (err) => {
  console.log('error');
  // app 會自動根據錯誤向前端進行響應。
});

app.use(async (ctx, next) => {
  ctx.throw(401, 'access_denied', { user: user });
});

app.use(async (ctx, next) => {
  ctx.body = 'kkb!';
});

app.listen(8888);

Request 物件

Koa` 通過 `Getter` 、 `Setter` 對 `Request` 進行了封裝,具體程式碼: `node_modules/koa/lib/request.js

Response 物件

Koa` 通過 `Getter` 、 `Setter` 對 `Response` 進行了封裝,具體程式碼: `node_modules/koa/lib/response.js

Context 代理

為了方便對 RequestResponse 物件進行操作, Koa 通過 delegatesContext 進行了代理訪問處理,使得可以通過 Context 即可操作對應的 RequestResponse

/**
 * File: lib/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');

引數:proto

delegate 的第一個引數 proto 就是 Context 物件。

引數:‘response’ 和 ‘response’

delegate 的第二個引數 'response''request' 就是需要被代理訪問的 Request 物件和 Response 物件。

method 方法

代理物件的對應方法。

access 方法

代理物件屬性的 gettersetter

getter 方法

代理物件屬性的 getter

setter 方法

代理物件屬性的 setter

相關文章