1. 程式人生 > >body-parser 原始碼分析

body-parser 原始碼分析

# body-parser 原始碼分析 預備知識:熟悉 express 的中介軟體邏輯 閱讀事件:30min ## 1. body-parser 解決什麼問題 在 node http 模組中,您只能通過 data 事件,以 buffer 的形式來獲取請求體內容,node 沒有提供如何解析請求body的API,[body-parser](https://www.npmjs.com/package/body-parser) 提供了這個功能。 body-parser 本質是一個處理請求 body 的中介軟體函式,他負責按照您給的規則解析請求body,並且將結果賦值到 req.body 屬性上。 ## 2. 簡單的使用 body-parser ```javascript var express = require('express') var bodyParser = require('body-parser') var app = express() // parse application/x-www-form-urlencoded app.use(bodyParser.urlencoded({ extended: false })) // parse application/json app.use(bodyParser.json()) app.use(function (req, res) { res.setHeader('Content-Type', 'text/plain') res.write('you posted:\n') // 您可以通過req.body 來訪問請求體內容 res.end(JSON.stringify(req.body, null, 2)) }) ``` 通過這個例子您可以瞭解到如何簡單的使用 body-parser。 ## 3. 原始碼分析 首先 bodyParser 的原始碼結構如下: ![image.png](https://cdn.nlark.com/yuque/0/2021/png/271135/1609740505579-02d706e1-8d45-4a89-892a-277073e655a3.png#align=left&display=inline&height=294&margin=%5Bobject%20Object%5D&name=image.png&originHeight=588&originWidth=2638&size=131415&status=done&style=none&width=1319) - index.js:入口檔案 - lib:核心方法 - types:該檔案下的4個檔案,分別用於解析對應的4個型別 - json.js:將body解析為JSON物件 - raw.js - text.js:將body解析為字串 - urlencoded.js:將表單資料(urlencoded編碼)解析為JSON物件 - read.js:讀取 body 內容 ### 1. bodyParser的匯出形式 bodyParser 的定義在 index.js,這裡的邏輯非常清晰: - 建立一個用於解析 json 和 urlencoded 格式的中介軟體:bodyParser 並匯出 - 給 bodyParser 新增 json/text/raw/urlencoded 方法 ```javascript 'use strict' var deprecate = require('depd')('body-parser') // 快取 parser var parsers = Object.create(null) // 匯出一個Function exports = module.exports = deprecate.function(bodyParser, 'bodyParser: use individual json/urlencoded middlewares') // JSON parser. Object.defineProperty(exports, 'json', { configurable: true, enumerable: true, get: createParserGetter('json') }) // Raw parser. Object.defineProperty(exports, 'raw', { configurable: true, enumerable: true, get: createParserGetter('raw') }) // Text parser. Object.defineProperty(exports, 'text', { configurable: true, enumerable: true, get: createParserGetter('text') }) // URL-encoded parser. Object.defineProperty(exports, 'urlencoded', { configurable: true, enumerable: true, get: createParserGetter('urlencoded') }) // 建立一個用於解析 json 和 urlencoded 格式的中介軟體 function bodyParser (options) { var opts = {} // exclude type option if (options) { for (var prop in options) { if (prop !== 'type') { opts[prop] = options[prop] } } } var _urlencoded = exports.urlencoded(opts) var _json = exports.json(opts) return function bodyParser (req, res, next) { _json(req, res, function (err) { if (err) return next(err) _urlencoded(req, res, next) }) } } // Create a getter for loading a parser. function createParserGetter (name) { return function get () { return loadParser(name) } } // Load a parser module. function loadParser (parserName) { var parser = parsers[parserName] if (parser !== undefined) { return parser } // this uses a switch for static require analysis switch (parserName) { case 'json': parser = require('./lib/types/json') break case 'raw': parser = require('./lib/types/raw') break case 'text': parser = require('./lib/types/text') break case 'urlencoded': parser = require('./lib/types/urlencoded') break } // store to prevent invoking require() return (parsers[parserName] = parser) } ``` ## 4. text 解析流程 將 body 解析非常簡單,這隻需要將 buffer 轉換為 string即可。 所以從最簡單 text parser 開始,其他解析大體也是類似的,主要區別在於將字串解析到特定格式的方法。比如將表單資料(urlencoded form) 解析為JSON物件。 現在您希望將 text/plain 的請求體解析為一個字串,原始碼是這樣的: ```javascript // 預設將 type 為 text/plain 解析為字串 var express = require('express') var bodyParser = require('body-parser') var app = express() var port = 3000; app.use(bodyParser.text()) app.post('/text', (req, res) => res.send(req.body)) app.listen(port, () => console.log(`\nExample app listening on port ${port}!`)) ``` 當我們 curl 進行如下訪操作: ```bash $ curl -d "hello" http://localhost:3000/text hello ``` 這背後的流程是怎樣的呢? ### 1. bodyParser.text() 中介軟體 由於我們使用 bodyParser.text() 中介軟體,所以當進行上述訪問時,會訪問到 lib/types/text,原始碼如下: ```javascript 'use strict' var bytes = require('bytes') var contentType = require('content-type') var debug = require('debug')('body-parser:text') var read = require('../read') var typeis = require('type-is') // 匯出 text 中介軟體 module.exports = text // text 中介軟體 定義 function text (options) { // option 是使用該中介軟體傳入的選項 var opts = options || {} // 獲取字符集 var defaultCharset = opts.defaultCharset || 'utf-8' // 是否處理壓縮的body, true時body會被解壓,false時body不會被處理 var inflate = opts.inflate !== false // body大小限制 var limit = typeof opts.limit !== 'number' ? bytes.parse(opts.limit || '100kb') : opts.limit // 需要處理的 content-type 型別 var type = opts.type || 'text/plain' // 使用者自定義的校驗函式,若提供則會被呼叫verify(req, res, buf, encoding) var verify = opts.verify || false if (verify !== false && typeof verify !== 'function') { throw new TypeError('option verify must be function') } // create the appropriate type checking function var shouldParse = typeof type !== 'function' ? typeChecker(type) : type // 這裡是核心, 不同的解析器有不同的處理方式 // text parse 很簡單是因為它啥也不需要幹 function parse (buf) { return buf } return function textParser (req, res, next) { // 當我們進行 POST 請求時 textParser 中介軟體會被呼叫 // 這裡先判斷 body 是否已經解析過了,下游會設定為 true if (req._body) { debug('body already parsed') next() return } req.body = req.body || {} // 沒有請求體時不處理 // skip requests without bodies if (!typeis.hasBody(req)) { debug('skip empty body') next() return } debug('content-type %j', req.headers['content-type']) // determine if request should be parsed if (!shouldParse(req)) { debug('skip parsing') next() return } // get charset var charset = getCharset(req) || defaultCharset // read read(req, res, next, parse, debug, { encoding: charset, inflate: inflate, limit: limit, verify: verify }) } } // 獲取請求字符集 function getCharset (req) { try { return (contentType.parse(req).parameters.charset || '').toLowerCase() } catch (e) { return undefined } } // content-type 檢測 function typeChecker (type) { return function checkType (req) { return Boolean(typeis(req, type)) } } // 判斷是否包含請求體(這個函式是從type-is包複製出來的) function hasbody (req) { return req.headers['transfer-encoding'] !== undefined || !isNaN(req.headers['content-length']) } ``` 大概流程如下: - 使用 app.use 使用中介軟體 - 客戶端發起 POST 請求 - 進入 textParser 中介軟體 - 判斷是否已經解析過(req._body = true) - 判斷請求是否包含請求體 - 判斷請求體型別是否需要處理 - 讀取請求體,解析並設定 req.body && req._body = true - 進入 read 中介軟體(讀取請求體,解析並設定 req.body && req._body = true) ### 2. read() 中介軟體(lib/read.js) lib/types 下的4個檔案,最終都會訪問 lib/read.js,形式如下: ```javascript read(req, res, next, parse, debug, { encoding: charset, inflate: inflate, limit: limit, verify: verify }) ``` 現在我們來看下 lib/read.js 原始碼: ```javascript 'use strict' var createError = require('http-errors') var getBody = require('raw-body') var iconv = require('iconv-lite') var onFinished = require('on-finished') var zlib = require('zlib') module.exports = read function read (req, res, next, parse, debug, options) { var length var opts = options var stream // parsed flag, 上游服務有做判斷 req._body = true // read options var encoding = opts.encoding !== null ? opts.encoding : null var verify = opts.verify try { // get the content stream stream = contentstream(req, debug, opts.inflate) length = stream.length stream.length = undefined } catch (err) { return next(err) } // set raw-body options opts.length = length opts.encoding = verify ? null : encoding // assert charset is supported if (opts.encoding === null && encoding !== null && !iconv.encodingExists(encoding)) { return next(createError(415, 'unsupported charset "' + encoding.toUpperCase() + '"', { charset: encoding.toLowerCase(), type: 'charset.unsupported' })) } // read body debug('read body') // getBody 函式用於從 stream 中讀取內容 getBody(stream, opts, function (error, body) { if (error) { // 異常處理 var _error if (error.type === 'encoding.unsupported') { // echo back charset _error = createError(415, 'unsupported charset "' + encoding.toUpperCase() + '"', { charset: encoding.toLowerCase(), type: 'charset.unsupported' }) } else { // set status code on error _error = createError(400, error) } // read off entire request stream.resume() onFinished(req, function onfinished () { next(createError(400, _error)) }) return } // 使用者自定義校驗函式 verify if (verify) { try { debug('verify body') verify(req, res, body, encoding) } catch (err) { next(createError(403, err, { body: body, type: err.type || 'entity.verify.failed' })) return } } var str = body try { debug('parse body') // 如果body不是字元型別而且設定了encoding,那麼需要重新解碼 str = typeof body !== 'string' && encoding !== null ? iconv.decode(body, encoding) : body // 這裡不同解析器,會傳入不同 parse req.body = parse(str) } catch (err) { next(createError(400, err, { body: str, type: err.type || 'entity.parse.failed' })) return } next() }) } // 獲取請求體 stream // 1. 獲取壓縮編碼格式,如果有壓縮需要先解壓 // 2. 返回 stream function contentstream (req, debug, inflate) { var encoding = (req.headers['content-encoding'] || 'identity').toLowerCase() var length = req.headers['content-length'] var stream debug('content-encoding "%s"', encoding) if (inflate === false && encoding !== 'identity') { throw createError(415, 'content encoding unsupported', { encoding: encoding, type: 'encoding.unsupported' }) } switch (encoding) { case 'deflate': stream = zlib.createInflate() debug('inflate body') req.pipe(stream) break case 'gzip': stream = zlib.createGunzip() debug('gunzip body') req.pipe(stream) break case 'identity': stream = req stream.length = length break default: throw createError(415, 'unsupported content encoding "' + encoding + '"', { encoding: encoding, type: 'encoding.unsupported' }) } return stream } ``` ## 5. 一些疑問 ### 1. 為什麼要對 charset 進行處理 其實本質上來說,charset前端一般都是固定為utf-8的, 甚至在JQuery的AJAX請求中,前端請求charset甚至是不可更改,只能是charset,但是在使用fetch等API的時候,的確是可以更改charset的,這個工作嘗試滿足一些比較偏僻的更改charset需求。 ### 2. 為什麼要對 content-encoding 做處理 一般情況下我們認為,考慮到前端發的AJAX之類的請求的資料量,是不需要做Gzip壓縮的。但是向伺服器發起請求的不一定只有前端,還可能是Node的客戶端。這些Node客戶端可能會向Node服務端傳送壓縮過後的資料流。 例如下面的程式碼所示: ```javascript const zlib = require('zlib'); const request = require('request'); const data = zlib.gzipSync(Buffer.from("我是一個被Gzip壓縮後的資料")); request({ method: 'POST', url: 'http://127.0.0.1:3000/post', headers: {//設定請求頭 "Content-Type": "text/plain", "Content-Encoding": "gzip" }, body: data }) ``` ## 6. 參考以及延伸 1. npm bodyParser [https://www.npmjs.com/package/body-parser#bodyparsertextoptions](https://www.npmjs.com/package/body-parser#bodyparsertextoptions) 1. npm iconv-lite 純JS編碼轉換器 1. npm raw-body 以buffer或者string的方式獲取一個可讀流的全部內容,並且可校驗長度 1. bodyparser實現原理解析(這篇文章回答了我上述2個疑問) [https://zhuanlan.zhihu.com/p/78482006](https://zhuanlan.zhihu.com/p/78