Koa、Egg、Express做爲Nodejs三大開發框架,相信對Node有實戰經驗的朋友都很是熟悉,相對Express,Koa和Egg在企業級開發中應用更爲普遍,Egg也是基於Koa的封裝,因此咱們熟悉Koa以後,對Egg手到擒來,下面咱們經過分析中間件koa-bodyparser來深刻了解Koa框架中的http請求部分。javascript
結合http、koa、koa middleware畫的一個簡單框架圖 html
經過koa-router搭建一個簡單的node服務,服務提供了一個get接口:/product/1java
/* app.js */
const Koa = require('koa');
const Router = require('koa-router')
const app = new Koa()
const router = new Router()
router.get('/product/1', async (ctx, next) => {
ctx.response.body = {
data: {},
success: true
}
})
app.use(router.routes())
app.listen(3000, () => {
console.log('app is starting: http://127.0.0.1:3000')
})
複製代碼
啓動服務node
node app.js
複製代碼
經過postman請求接口/product/1 git
經過http響應體能夠看出請求返回符合咱們預期,可是咱們的服務提供的api不全是get請求,瞭解http的同窗可能知道,經常使用的http method經過修改app.js添加一個method爲post的路由github
router.post('/product/list', async (ctx, next) => {
ctx.response.body = {
data: {
list: [],
page: {},
success: true
}
}
})
複製代碼
經過postman請求接口/product/list數據庫
請求成功,返回預期數據。可是post和get是有區別的,具體區別不細講,一般的分頁請求咱們用post方式,調用一個分頁接口會傳pageNum、pageSize等查詢參數,服務器端拿到這些查詢參數拼接成SQL查數據庫並返回,服務器怎麼拿到這些數據呢?熟悉koa的都知道在 ctx.request.body中能夠拿到。路由改造json
router.post('/product/list', async (ctx, next) => {
const pageNum = ctx.request.body.pageNum
const pageSize = ctx.request.body.pageSize
ctx.response.body = {
data: {
list: [],
page: { pageNum, pageSize },
success: true
}
}
})
複製代碼
重啓服務,經過postman再次請求接口/product/listapi
此時接口響應狀態碼是500,報服務器異常,咱們看看Node服務 Node服務拋了個異常,意思是ctx.request.body爲空,拿不到post過來的參數,爲何拿不到?其實ctx.request的body屬性是在koa中間件koa-bodyparser中添加的,下面咱們來看看koa-bodyparser的實現原理。上面的例子咱們沒有引入koa-bodyparser,因此ctx.request上的body屬性沒有拿到,咱們在程序中引入koa-bodyparser看看。bash
const app = new Koa()
const router = new Router()
const Router = require('koa-router')
const bodyParser = require('koa-bodyparser')
app.use(bodyParser())
router.post('/product/list', async (ctx, next) => {
const pageNum = ctx.request.body.pageNum
const pageSize = ctx.request.body.pageSize
ctx.response.body = {
data: {
list: [],
page: { pageNum, pageSize },
success: true
}
}
})
app.use(router.routes())
app.listen(3000)
複製代碼
經過postman請求接口/product/list
能夠看到接口請求成功,服務器內部成功獲取到post過去的參數pageNum和pageSize咱們去github上clone一份koa-bodyparser的源代碼,我以3.1.0版本爲例,代碼比較簡單,咱們找到入口index.js
從代碼結構能夠看出,koa-bodyparser向外暴露一個方法,方法中會在app的request屬性和respons,context屬性上添加一系列方法:
中間件入參app是什麼?其實就是koa的上下文context,咱們看看koa源碼中對context的定義
/* koa源碼 */
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;
}
複製代碼
koa的上下文是經過createContext方法建立的,建立的目的就是能夠在任何地方經過this或者createContext返回的對象中拿到請求(request/req)和響應(response/res)上的各類屬性,好比body屬性,能夠經過ctx.request.body,ctx.req.body,ctx.request.req.body拿到,瞭解這個以後,咱們看看koa-bodyparser中的body方法實現原理。
看看koa-bodyparser中body方法源碼
request.body = function (limit) {
switch (this.is('urlencoded', 'json')) {
case 'json': return this.json(limit)
case 'urlencoded': return this.urlencoded(limit)
}
}
複製代碼
代碼中有個switch分支判斷,判斷條件this.is('urlencoded', 'json')的返回值,is方法並無在當前的中間件中實現,其實這個is方法是在koa中實現的,具體的做用就是判斷當前的http請求頭content-type值,熟悉http的同窗對這個請求頭不陌生,在封裝http client的時候會常常用到,經常使用的content-type有:
其實content-type有十幾種,經常使用的就是上面幾種,每種類型的區別,不熟悉的能夠本身百度,看下is方法的示例。
/* When Content-Type is application/json */
this.is('json', 'urlencoded'); // => 'json'
複製代碼
由於3.1.0版本的koa-bodyparser,只支持兩種類型的content-type,即application/json和application/x-www-form-urlencoded,二者有什麼區別?
以後koa-bodyparser的高版本支持了更多類型form、text等。經過body方法源碼,咱們可看出,若是content-type爲application/json,switch就會走json分支,調用this.json並返回,咱們看看具體實現:
request.json = function (limit) {
if (!this.length) return Promise.resolve()
return this.text(limit).then((text) => this._parse_json(text))
}
request.text = function (limit) {
this.response.writeContinue()
return get(this.req, {
limit: limit || '100kb',
length: this.length,
encoding: 'utf8'
})
}
複製代碼
this.json調用了this.text方法,thi.text又調用了get方法,並傳入了this.req和options,get方法是第三方模塊提供的:
const get = require('raw-body')
複製代碼
接下來能夠看看raw-body模塊的實現
raw-body其實就是監聽this.req上的data事件,this.req是koa在建立服務時,調用了Node內置模塊http的createServer方法傳遞的
/* koa源碼 */
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) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
複製代碼
http模塊的createServer調用方式
http.createServer((req, res) => {
...
})
複製代碼
http模塊的源碼咱們在這裏不作分析。上面說到raw-body其實就是監聽this.req上的data事件,data事件的入參是post請求body的二進制流,咱們能夠把二進制流轉換爲options.encoding中的編碼格式,好比utf-8,buffer等,默認爲buffer
/* raw-body核心源碼 */
var buffer = decoder
? ''
: []
// attach listeners
stream.on('aborted', onAborted)
stream.on('close', cleanup)
stream.on('data', onData)
stream.on('end', onEnd)
stream.on('error', onEnd)
複製代碼
function onData (chunk) {
if (complete) return
received += chunk.length
if (limit !== null && received > limit) {
done(createError(413, 'request entity too large', {
limit: limit,
received: received,
type: 'entity.too.large'
}))
} else if (decoder) {
buffer += decoder.write(chunk)
} else {
buffer.push(chunk)
}
}
複製代碼
function onEnd (err) {
if (complete) return
if (err) return done(err)
if (length !== null && received !== length) {
done(createError(400, 'request size did not match content length', {
expected: length,
length: length,
received: received,
type: 'request.size.invalid'
}))
} else {
var string = decoder
? buffer + (decoder.end() || '')
: Buffer.concat(buffer)
done(null, string)
}
}
複製代碼
經過koa建立node服務,發起一個post請求,內置模塊http處理了不少上游的東西,下游的koa中間件把http body的二進制流根據content-type類型,調用對應的適配器轉換成json對象,而後放到ctx.request屬性上。