Koa原始碼閱讀(二)上下文ctx

Hoxz發表於2018-12-07

上篇提到,this.callback() 返回一個回撥函式,其實是以閉包的形式返回了一個區域性函式變數 handleRequest,供 Server 呼叫來處理 HTTP 請求。

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

  const handleRequest = (req, res) => {
    const ctx = this.createContext(req, res);
    return this.handleRequest(ctx, fn);
  };

  return handleRequest;
}
複製程式碼

請求到來時,Server 將 Node 提供的原生 requestresponse 傳給回撥 handleRequest,它執行兩項工作:

  • 建立一個上下文 ctx,封裝了本次的請求和響應
  • 將上下文 ctx 和函式 fn 交由 this.handleRequest() 處理

接下來我們看一下上下文 ctx 是怎麼建立和使用的。

建立上下文 ctx

直接將 Node 提供的原生 requestresponse 傳給了 this.createContext() 方法。

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;
}
複製程式碼

程式碼似乎重複性很大,我們梳理一下:

屬性 含義
context / .ctx 上下文
req / .req Node 請求
res / .res Node 響應
request / .request Koa 請求
response / .response Koa響應

主要就是上下文、Node 請求&響應、Koa 請求&響應之間的交叉引用,便於使用。

ctx 是怎麼封裝了請求與響應?Node 請求&響應與 Koa 請求&響應之間又是什麼關係呢?這就不得不提到 Koa 用到的委託模式了。

委託模式

委託模式(Delegation Pattern)是設計模式的一種,意思是外層暴露的物件將請求委託給內部的其他物件進行處理。

context.js 可以中看到,Koa 使用 delegates 這個 NPM 包,將本應由上下文 ctx 處理的事情委託給了 requestresponse,這兩個物件來自於 request.jsresponse.js

/* context.js */

const delegate = require('delegates');

const proto = module.exports = {
  /* 此處是 context 自己完成的一些方法和屬性 */
}

/* 委託給 response 處理 */
delegate(proto, 'response')
  .method('attachment')
	.method('redirect')
  .access('status')
	.access('body')
	.access('length')
  /* ... */

/* 委託給 request 處理 */
delegate(proto, 'request')
  .method('acceptsLanguages')
  .method('acceptsEncodings')
  .access('method')
  .access('query')
  .access('path')
  .access('url')
  .getter('host')
  .getter('hostname')
  .getter('URL')
  /* ... */
複製程式碼

這樣一來,我們對上下文 ctx 的操作,如 ctx.typectx.length 就會由 response 物件執行,ctx.pathctx.method 就會由 request 物件執行。不要忘了, responserequest 是 Koa 自己的請求和響應。怎麼把它們與 Node 請求&響應聯絡起來呢?

請求與響應

再囉嗦一遍,真正將請求與響應的操作落實到位的不是上下文 ctx ,而是來自 request.jsrequest 物件和來自 response.jsresponse 物件。我們看一下這兩個物件的實現。

/* request.js */

module.exports = {
  /* ... */
  
  /**
   * Get request URL.
   *
   * @return {String}
   * @api public
   */

  get url() {
    return this.req.url;
  },
  
  /* ... */
}
複製程式碼
/* response.js */

module.exports = {
  /* ... */
  
  /**
   * Check if a header has been written to the socket.
   *
   * @return {Boolean}
   * @api public
   */

  get headerSent() {
    return this.res.headersSent;
  },
  
  /* ... */
}
複製程式碼

原來是靠 Koa 請求/響應去操作 Node 請求/響應來實現的!整個流程串起來就是,上下文 ctx 委託給 Koa 請求/響應,Koa 請求/響應操作 Node 請求/響應,從而實現了完整的請求/響應處理流程。

這個關係弄懂了,Koa的上下文 ctx 是怎麼回事也就明白了。

開發中常遇到的獲取 POST 引數問題

前面提到,ctx.query 委託給了 requestrequest 對 Node 請求中的 query 做了封裝,所以我們可以直接用 ctx.query 獲取到 GET 引數。

而 POST 請求就沒有這種封裝,需要通過解析 Node 原生請求來獲取其引數。

app.use( async ( ctx ) => {
  if ( ctx.url === '/' && ctx.method === 'POST' ) {
    // 當 POST 請求的時候,解析 POST 表單裡的資料,並顯示出來
    let postData = await parsePostData( ctx )
    ctx.body = postData
  }
})

// 解析上下文裡 Node 原生請求的 POST 引數
function parsePostData( ctx ) {
  return new Promise((resolve, reject) => {
    try {
      let postdata = "";
      ctx.req.addListener('data', (data) => {
        postdata += data
      })
      ctx.req.addListener("end",function(){
        let parseData = parseQueryStr( postdata )
        resolve( parseData )
      })
    } catch ( err ) {
      reject(err)
    }
  })
}

// 將 POST 請求引數字串解析成 JSON
function parseQueryStr( queryStr ) {
  let queryData = {}
  let queryStrList = queryStr.split('&')
  console.log( queryStrList )
  for (  let [ index, queryStr ] of queryStrList.entries()  ) {
    let itemList = queryStr.split('=')
    queryData[ itemList[0] ] = decodeURIComponent(itemList[1])
  }
  return queryData
}

// 程式碼來源於:https://chenshenhai.github.io/koa2-note/note/request/post.html
複製程式碼

也可以直接使用 koa-bodyparser 這個 NPM 包作為中介軟體完成 POST 資料處理。

const bodyparser = require('koa-bodyparser')

app.use(bodyparser())

app.use( async (ctx) => {
  if (ctx.url === '/' && ctx.method === 'POST') {
    let data = ctx.request.body
    ctx.body = data
  }
})
複製程式碼

相關文章