上篇提到,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 提供的原生 request
和 response
傳給回撥 handleRequest
,它執行兩項工作:
- 建立一個上下文
ctx
,封裝了本次的請求和響應 - 將上下文
ctx
和函式fn
交由this.handleRequest()
處理
接下來我們看一下上下文 ctx 是怎麼建立和使用的。
建立上下文 ctx
直接將 Node 提供的原生 request
和 response
傳給了 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
處理的事情委託給了 request
和 response
,這兩個物件來自於 request.js
和 response.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.type
和 ctx.length
就會由 response
物件執行,ctx.path
和 ctx.method
就會由 request
物件執行。不要忘了, response
和 request
是 Koa 自己的請求和響應。怎麼把它們與 Node 請求&響應聯絡起來呢?
請求與響應
再囉嗦一遍,真正將請求與響應的操作落實到位的不是上下文 ctx
,而是來自 request.js
的 request
物件和來自 response.js
的response
物件。我們看一下這兩個物件的實現。
/* 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
委託給了 request
, request
對 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
}
})
複製程式碼