上篇提到,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
}
})
复制代码