經過bodyparser源碼深刻了解koa運行機制

Koa、Egg、Express做爲Nodejs三大開發框架,相信對Node有實戰經驗的朋友都很是熟悉,相對Express,Koa和Egg在企業級開發中應用更爲普遍,Egg也是基於Koa的封裝,因此咱們熟悉Koa以後,對Egg手到擒來,下面咱們經過分析中間件koa-bodyparser來深刻了解Koa框架中的http請求部分。javascript

基於Koa服務框架

結合http、koa、koa middleware畫的一個簡單框架圖 html

Koa搭建簡單的Node服務

經過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

  • GET
  • POST
  • PUT
  • DELETE
  • HEAD

經過修改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的實現原理

上面的例子咱們沒有引入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

koa-bodyparser內部實現

咱們去github上clone一份koa-bodyparser的源代碼,我以3.1.0版本爲例,代碼比較簡單,咱們找到入口index.js

從代碼結構能夠看出,koa-bodyparser向外暴露一個方法,方法中會在app的request屬性和respons,context屬性上添加一系列方法:

  • json
  • urlencoded
  • body
  • text
  • buffer

中間件入參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方法實現原理。

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有:

  • text/html
  • text/plain
  • application/json
  • application/x-www-form-urlencoded
  • multipart/form-data

其實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,二者有什麼區別?

  • json:post的body是json字符串,例如{pageNum: 1, pageSize:10}
  • x-www-form-urlencoded:post的body是url encoded,例如pageNum=1&pageSize=10

以後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模塊實現

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屬性上。

相關文章
相關標籤/搜索