node實踐 Express 常用中間件 body-parser 實現(xiàn)解析(ok)

寫在前面

body-parser是非常常用的一個express中間件,作用是對http請求體進行解析郊霎。使用非常簡單钉汗,以下兩行代碼已經(jīng)覆蓋了大部分的使用場景。

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));

本文從簡單的例子出發(fā)傻谁,探究body-parser的內(nèi)部實現(xiàn)孝治。至于body-parser如何使用,感興趣的同學(xué)可以參考官方文檔审磁。

入門基礎(chǔ)

在正式講解前谈飒,我們先來看一個POST請求的報文,如下所示态蒂。

POST /test HTTP/1.1
Host: 127.0.0.1:3000
Content-Type: text/plain; charset=utf8
Content-Encoding: gzip

chyingp

其中需要我們注意的有Content-Type杭措、Content-Encoding以及報文主體:

  • Content-Type:請求報文主體的類型、編碼钾恢。常見的類型有text/plain手素、application/jsonapplication/x-www-form-urlencoded瘩蚪。常見的編碼有utf8泉懦、gbk等。
  • Content-Encoding:聲明報文主體的壓縮格式疹瘦,常見的取值有gzip祠斧、deflateidentity拱礁。
  • 報文主體:這里是個普通的文本字符串chyingp琢锋。

body-parser主要做了什么

body-parser實現(xiàn)的要點如下:

  1. 處理不同類型的請求體:比如textjson呢灶、urlencoded等吴超,對應(yīng)的報文主體的格式不同。
  2. 處理不同的編碼:比如utf8鸯乃、gbk等鲸阻。
  3. 處理不同的壓縮類型:比如gzipdeflare等缨睡。
  4. 其他邊界鸟悴、異常的處理。

一奖年、處理不同類型請求體

為了方便讀者測試细诸,以下例子均包含服務(wù)端、客戶端代碼陋守,完整代碼可在筆者github上找到震贵。

解析text/plain

客戶端請求的代碼如下利赋,采用默認(rèn)編碼,不對請求體進行壓縮猩系。請求體類型為text/plain媚送。

var http = require('http');

var options = {
    hostname: '127.0.0.1',
    port: '3000',
    path: '/test',
    method: 'POST',
    headers: {
        'Content-Type': 'text/plain',
        'Content-Encoding': 'identity'
    }
};

var client = http.request(options, (res) => {
    res.pipe(process.stdout);
});

client.end('chyingp');

服務(wù)端代碼如下。text/plain類型處理比較簡單寇甸,就是buffer的拼接塘偎。

var http = require('http');

var parsePostBody = function (req, done) {
    var arr = [];
    var chunks;

    req.on('data', buff => {
        arr.push(buff);
    });

    req.on('end', () => {
        chunks = Buffer.concat(arr);
        done(chunks);
    });
};

var server = http.createServer(function (req, res) {
    parsePostBody(req, (chunks) => {
        var body = chunks.toString();
        res.end(`Your nick is ${body}`)
    });
});

server.listen(3000);

解析application/json

客戶端代碼如下,把Content-Type換成application/json拿霉。

var http = require('http');
var querystring = require('querystring');

var options = {
    hostname: '127.0.0.1',
    port: '3000',
    path: '/test',
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'Content-Encoding': 'identity'
    }
};

var jsonBody = {
    nick: 'chyingp'
};

var client = http.request(options, (res) => {
    res.pipe(process.stdout);
});

client.end( JSON.stringify(jsonBody) );

服務(wù)端代碼如下吟秩,相比text/plain,只是多了個JSON.parse()的過程友浸。

var http = require('http');

var parsePostBody = function (req, done) {
    var length = req.headers['content-length'] - 0;
    var arr = [];
    var chunks;

    req.on('data', buff => {
        arr.push(buff);
    });

    req.on('end', () => {
        chunks = Buffer.concat(arr);
        done(chunks);
    });
};

var server = http.createServer(function (req, res) {
    parsePostBody(req, (chunks) => {
        var json = JSON.parse( chunks.toString() );    // 關(guān)鍵代碼    
        res.end(`Your nick is ${json.nick}`)
    });
});

server.listen(3000);

解析application/x-www-form-urlencoded

客戶端代碼如下,這里通過querystring對請求體進行格式化偏窝,得到類似nick=chyingp的字符串收恢。

var http = require('http');
var querystring = require('querystring');

var options = {
    hostname: '127.0.0.1',
    port: '3000',
    path: '/test',
    method: 'POST',
    headers: {
        'Content-Type': 'form/x-www-form-urlencoded',
        'Content-Encoding': 'identity'
    }
};

var postBody = { nick: 'chyingp' };

var client = http.request(options, (res) => {
    res.pipe(process.stdout);
});

client.end( querystring.stringify(postBody) );

服務(wù)端代碼如下,同樣跟text/plain的解析差不多祭往,就多了個querystring.parse()的調(diào)用伦意。

var http = require('http');
var querystring = require('querystring');

var parsePostBody = function (req, done) {
    var length = req.headers['content-length'] - 0;
    var arr = [];
    var chunks;

    req.on('data', buff => {
        arr.push(buff);
    });

    req.on('end', () => {
        chunks = Buffer.concat(arr);
        done(chunks);
    });
};

var server = http.createServer(function (req, res) {
    parsePostBody(req, (chunks) => {
        var body = querystring.parse( chunks.toString() );  // 關(guān)鍵代碼
        res.end(`Your nick is ${body.nick}`)
    });
});

server.listen(3000);

二、處理不同編碼

很多時候硼补,來自客戶端的請求驮肉,采用的不一定是默認(rèn)的utf8編碼,這個時候已骇,就需要對請求體進行解碼處理离钝。

客戶端請求如下,有兩個要點褪储。

  1. 編碼聲明:在Content-Type最后加上;charset=gbk
  2. 請求體編碼:這里借助了iconv-lite卵渴,對請求體進行編碼iconv.encode('程序猿小卡', encoding)
var http = require('http');
var iconv = require('iconv-lite');

var encoding = 'gbk';  // 請求編碼

var options = {
    hostname: '127.0.0.1',
    port: '3000',
    path: '/test',
    method: 'POST',
    headers: {
        'Content-Type': 'text/plain; charset=' + encoding,
        'Content-Encoding': 'identity',        
    }
};

// 備注:nodejs本身不支持gbk編碼,所以請求發(fā)送前鲤竹,需要先進行編碼
var buff = iconv.encode('程序猿小卡', encoding);

var client = http.request(options, (res) => {
    res.pipe(process.stdout);
});

client.end(buff, encoding);

服務(wù)端代碼如下浪读,這里多了兩個步驟:編碼判斷、解碼操作辛藻。首先通過Content-Type獲取編碼類型gbk碘橘,然后通過iconv-lite進行反向解碼操作。

var http = require('http');
var contentType = require('content-type');
var iconv = require('iconv-lite');

var parsePostBody = function (req, done) {
    var obj = contentType.parse(req.headers['content-type']);
    var charset = obj.parameters.charset;  // 編碼判斷:這里獲取到的值是 'gbk'

    var arr = [];
    var chunks;

    req.on('data', buff => {
        arr.push(buff);
    });

    req.on('end', () => {
        chunks = Buffer.concat(arr);
        var body = iconv.decode(chunks, charset);  // 解碼操作
        done(body);
    });
};

var server = http.createServer(function (req, res) {
    parsePostBody(req, (body) => {
        res.end(`Your nick is ${body}`)
    });
});

server.listen(3000);

三吱肌、處理不同壓縮類型

這里舉個gzip壓縮的例子痘拆。客戶端代碼如下氮墨,要點如下:

  1. 壓縮類型聲明:Content-Encoding賦值為gzip错负。
  2. 請求體壓縮:通過zlib模塊對請求體進行g(shù)zip壓縮坟瓢。
var http = require('http');
var zlib = require('zlib');

var options = {
    hostname: '127.0.0.1',
    port: '3000',
    path: '/test',
    method: 'POST',
    headers: {
        'Content-Type': 'text/plain',
        'Content-Encoding': 'gzip'
    }
};

var client = http.request(options, (res) => {
    res.pipe(process.stdout);
});

// 注意:將 Content-Encoding 設(shè)置為 gzip 的同時,發(fā)送給服務(wù)端的數(shù)據(jù)也應(yīng)該先進行g(shù)zip
var buff = zlib.gzipSync('chyingp');

client.end(buff);

服務(wù)端代碼如下犹撒,這里通過zlib模塊折联,對請求體進行了解壓縮操作(guzip)。

var http = require('http');
var zlib = require('zlib');

var parsePostBody = function (req, done) {
    var length = req.headers['content-length'] - 0;
    var contentEncoding = req.headers['content-encoding'];
    var stream = req;

    // 關(guān)鍵代碼如下
    if(contentEncoding === 'gzip') {
        stream = zlib.createGunzip();
        req.pipe(stream);
    }

    var arr = [];
    var chunks;

    stream.on('data', buff => {
        arr.push(buff);
    });

    stream.on('end', () => {
        chunks = Buffer.concat(arr);        
        done(chunks);
    });

    stream.on('error', error => console.error(error.message));
};

var server = http.createServer(function (req, res) {
    parsePostBody(req, (chunks) => {
        var body = chunks.toString();
        res.end(`Your nick is ${body}`)
    });
});

server.listen(3000);

寫在后面

body-parser的核心實現(xiàn)并不復(fù)雜识颊,翻看源碼后你會發(fā)現(xiàn)诚镰,更多的代碼是在處理異常跟邊界。

另外祥款,對于POST請求清笨,還有一個非常常見的Content-Typemultipart/form-data,這個的處理相對復(fù)雜些刃跛,body-parser不打算對其進行支持抠艾。篇幅有限,后續(xù)章節(jié)再繼續(xù)展開桨昙。

歡迎交流检号,如有錯漏請指出。

相關(guān)鏈接

https://github.com/expressjs/body-parser/

https://github.com/ashtuchkin/iconv-lite

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蛙酪,一起剝皮案震驚了整個濱河市齐苛,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌桂塞,老刑警劉巖凹蜂,帶你破解...
    沈念sama閱讀 221,576評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異阁危,居然都是意外死亡玛痊,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,515評論 3 399
  • 文/潘曉璐 我一進店門狂打,熙熙樓的掌柜王于貴愁眉苦臉地迎上來卿啡,“玉大人,你說我怎么就攤上這事菱父【蹦龋” “怎么了?”我有些...
    開封第一講書人閱讀 168,017評論 0 360
  • 文/不壞的土叔 我叫張陵浙宜,是天一觀的道長官辽。 經(jīng)常有香客問我,道長粟瞬,這世上最難降的妖魔是什么同仆? 我笑而不...
    開封第一講書人閱讀 59,626評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮裙品,結(jié)果婚禮上俗批,老公的妹妹穿的比我還像新娘俗或。我一直安慰自己,他們只是感情好岁忘,可當(dāng)我...
    茶點故事閱讀 68,625評論 6 397
  • 文/花漫 我一把揭開白布辛慰。 她就那樣靜靜地躺著,像睡著了一般干像。 火紅的嫁衣襯著肌膚如雪帅腌。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,255評論 1 308
  • 那天麻汰,我揣著相機與錄音速客,去河邊找鬼。 笑死五鲫,一個胖子當(dāng)著我的面吹牛溺职,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播位喂,決...
    沈念sama閱讀 40,825評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼浪耘,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了忆某?” 一聲冷哼從身側(cè)響起点待,我...
    開封第一講書人閱讀 39,729評論 0 276
  • 序言:老撾萬榮一對情侶失蹤阔蛉,失蹤者是張志新(化名)和其女友劉穎弃舒,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體状原,經(jīng)...
    沈念sama閱讀 46,271評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡聋呢,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,363評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了颠区。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片削锰。...
    茶點故事閱讀 40,498評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖毕莱,靈堂內(nèi)的尸體忽然破棺而出器贩,到底是詐尸還是另有隱情,我是刑警寧澤朋截,帶...
    沈念sama閱讀 36,183評論 5 350
  • 正文 年R本政府宣布蛹稍,位于F島的核電站,受9級特大地震影響部服,放射性物質(zhì)發(fā)生泄漏唆姐。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,867評論 3 333
  • 文/蒙蒙 一廓八、第九天 我趴在偏房一處隱蔽的房頂上張望奉芦。 院中可真熱鬧赵抢,春花似錦、人聲如沸声功。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,338評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽减噪。三九已至短绸,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間筹裕,已是汗流浹背醋闭。 一陣腳步聲響...
    開封第一講書人閱讀 33,458評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留朝卒,地道東北人证逻。 一個月前我還...
    沈念sama閱讀 48,906評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像抗斤,于是被迫代替她去往敵國和親囚企。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,507評論 2 359

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理瑞眼,服務(wù)發(fā)現(xiàn)龙宏,斷路器,智...
    卡卡羅2017閱讀 134,699評論 18 139
  • 前端開發(fā)者丨h(huán)ttp請求 https:www.rokub.com 前言見解有限伤疙, 如有描述不當(dāng)之處银酗, 請幫忙指出,...
    麋鹿_720a閱讀 10,934評論 11 31
  • 前言 web到目前為止走過了1.0徒像、2.0黍特、移動互聯(lián)網(wǎng)、本地應(yīng)用化幾個階段锯蛀,這使得js變得炙手可熱灭衷,許多原來在se...
    白昔月閱讀 1,767評論 3 6
  • 易經(jīng),根本就是乾坤兩個卦旁涤。所有其余卦翔曲,都是乾坤兩卦變化出來的,每一卦都有六個過程劈愚,組成象來說明事情瞳遍,是個小輪回。六...
    各帶帶閱讀 212評論 1 0
  • 美食是廣東衣襟上別著的一株紅玫瑰份蝴,馥郁的芬芳引誘著各地人的味蕾犁功。然而外地人不知道的是,除去美食的廣東婚夫,照樣擁有致命...
    孟娟游天下閱讀 234評論 1 1