作為前端的JSer,是一件非常幸福的事情,因?yàn)樵谧址蠌膩?lái)沒(méi)有出現(xiàn)過(guò)任何糾結(jié)的問(wèn)題。我們來(lái)看看PHP對(duì)字符串長(zhǎng)度的判斷結(jié)果:
<?php
echo strlen("0123456789");
echo strlen("零一二三四五六七八九");
echo mb_strlen("零一二三四五六七八九", "utf-8");
echo "\n";
以上三行判斷分別返回10、30、10。對(duì)于中國(guó)人而言,strlen這個(gè)方法對(duì)于Unicode的判斷結(jié)果是非常讓人疑惑。而看看JavaScript中對(duì)字符串長(zhǎng)度的判斷,就知道這個(gè)length屬性對(duì)調(diào)用者而言是多么友好。
console.log("0123456789".length); // 10
console.log("零一二三四五六七八九".length); /10
console.log("\u00bd".length); // 1
盡管在計(jì)算機(jī)內(nèi)部,一個(gè)中文字和一個(gè)英文字占用的字節(jié)位數(shù)是不同的,但對(duì)于用戶而言,它們擁有相同的長(zhǎng)度。我認(rèn)為這是JavaScript中 String處理得精彩的一個(gè)點(diǎn)。正是由于這個(gè)原因,所有的數(shù)據(jù)從后端傳輸?shù)角岸吮徽{(diào)用時(shí),都是這般友好的字符串。所以對(duì)于前端工程師而言,他們是沒(méi)有字 符串Buffer的概念的。如果你是一名前端工程師,那么從此在與Node.js打交道的過(guò)程中,一定要小心Buffer啦,因?yàn)樗葌鹘y(tǒng)的String 要調(diào)皮一點(diǎn)。
像許多計(jì)算機(jī)的技術(shù)一樣,都是從國(guó)外傳播過(guò)來(lái)的。那些以英文作為母語(yǔ)的傳道者們應(yīng)該沒(méi)有考慮過(guò)英文以外的使用者,所以你有可能看到如下這樣一段代碼在向你描述如何在data事件中連接字符串。
var fs = require('fs');
var rs = fs.createReadStream('testdata.md');
var data = '';
rs.on("data", function (trunk){
data += trunk;
});
rs.on("end", function () {
console.log(data);
});
如果這個(gè)文件讀取流讀取的是一個(gè)純英文的文件,這段代碼是能夠正常輸出的。但是如果我們?cè)俑淖円幌聴l件,將每次讀取的buffer大小變成一個(gè)奇數(shù),以模擬一個(gè)字符被分配在兩個(gè)trunk中的場(chǎng)景。
var rs = fs.createReadStream('testdata.md', {bufferSize: 11});
我們將會(huì)得到以下這樣的亂碼輸出:
事件循???和請(qǐng)求???象構(gòu)成了Node.js???異步I/O模型的???個(gè)基本???素,這也是典???的消費(fèi)???生產(chǎn)者場(chǎng)景。
造成這個(gè)問(wèn)題的根源在于data += trunk語(yǔ)句里隱藏的錯(cuò)誤,在默認(rèn)的情況下,trunk是一個(gè)Buffer對(duì)象。這句話的實(shí)質(zhì)是隱藏了toString的變換的:
data = data.toString() + trunk.toString();
由于漢字不是用一個(gè)字節(jié)來(lái)存儲(chǔ)的,導(dǎo)致有被截破的漢字的存在,于是出現(xiàn)亂碼。解決這個(gè)問(wèn)題有一個(gè)簡(jiǎn)單的方案,是設(shè)置編碼集:
var rs = fs.createReadStream('testdata.md', {encoding: 'utf-8', bufferSize: 11});
這將得到一個(gè)正常的字符串響應(yīng):
事件循環(huán)和請(qǐng)求對(duì)象構(gòu)成了Node.js的異步I/O模型的兩個(gè)基本元素,這也是典型的消費(fèi)者生產(chǎn)者場(chǎng)景。
遺憾的是目前Node.js僅支持hex、utf8、ascii、binary、base64、ucs2幾種編碼的轉(zhuǎn)換。對(duì)于那些因?yàn)闅v史遺留問(wèn)題依舊還生存著的GBK,GB2312等編碼,該方法是無(wú)能為力的。
在這個(gè)例子中,如果仔細(xì)觀察,會(huì)發(fā)現(xiàn)一件有趣的事情發(fā)生在設(shè)置編碼集之后。我們提到data += trunk等價(jià)于data = data.toString() + trunk.toString()。通過(guò)以下的代碼可以測(cè)試到一個(gè)漢字占用三個(gè)字節(jié),而我們按11個(gè)字節(jié)來(lái)截取trunk的話,依舊會(huì)存在一個(gè)漢字被分割在兩個(gè)trunk中的情景。
console.log("事件循環(huán)和請(qǐng)求對(duì)象".length);
console.log(new Buffer("事件循環(huán)和請(qǐng)求對(duì)象").length);
按照猜想的toString()方式,應(yīng)該返回的是事件循xxx和請(qǐng)求xxx象才對(duì),其中“環(huán)”字應(yīng)該變成亂碼才對(duì),但是在設(shè)置了encoding(默認(rèn)的utf8)之后,結(jié)果卻正常顯示了,這個(gè)結(jié)果十分有趣。
在好奇心的驅(qū)使下可以探查到data事件調(diào)用了string_decoder來(lái)進(jìn)行編碼補(bǔ)足的行為。通過(guò)string_decoder對(duì)象輸出第一個(gè)截取Buffer(事件循xx)時(shí),只返回事件循這個(gè)字符串,保留xx。第二次通過(guò)string_decoder對(duì)象輸出時(shí)檢測(cè)到上次保留的xx,將上次剩余內(nèi)容和本次的Buffer進(jìn)行重新拼接輸出。于是達(dá)到正常輸出的目的。
string_decoder,目前在文件流讀取和網(wǎng)絡(luò)流讀取中都有應(yīng)用到,一定程度上避免了粗魯拼接trunk導(dǎo)致的亂碼錯(cuò)誤。但是,遺憾在于string_decoder目前只支持utf8編碼。它的思路其實(shí)還可以擴(kuò)展到其他編碼上,只是最終是否會(huì)支持目前尚不可得知。
那么萬(wàn)能的適應(yīng)各種編碼而且正確的拼接Buffer對(duì)象的方法是什么呢?我們從Node.js在github上的源碼中找出這樣一段正確讀取文件,并連接buffer對(duì)象的方法:
var buffers = [];
var nread = 0;
readStream.on('data', function (chunk) {
buffers.push(chunk);
nread += chunk.length;
});
readStream.on('end', function () {
var buffer = null;
switch(buffers.length) {
case 0: buffer = new Buffer(0);
break;
case 1: buffer = buffers[0];
break;
default:
buffer = new Buffer(nread);
for (var i = 0, pos = 0, l = buffers.length; i < l; i++) {
var chunk = buffers[i];
chunk.copy(buffer, pos);
pos += chunk.length;
}
break;
}
});
在end事件中通過(guò)細(xì)膩的連接方式,最后拿到理想的Buffer對(duì)象。這時(shí)候無(wú)論是在支持的編碼之間轉(zhuǎn)換,還是在不支持的編碼之間轉(zhuǎn)換(利用iconv模塊轉(zhuǎn)換),都不會(huì)導(dǎo)致亂碼。
上述一大段代碼僅只完成了一件事情,就是連接多個(gè)Buffer對(duì)象,而這種場(chǎng)景需求將會(huì)在多個(gè)地方發(fā)生,所以,采用一種更優(yōu)雅的方式來(lái)完成該過(guò)程是必要的。筆者基于以上的代碼封裝出一個(gè)bufferhelper模塊,用于更簡(jiǎn)潔地處理Buffer對(duì)象??梢酝ㄟ^(guò)NPM進(jìn)行安裝:
npm install bufferhelper
下面的例子演示了如何調(diào)用這個(gè)模塊。與傳統(tǒng)data += trunk之間只是bufferHelper.concat(chunk)的差別,既避免了錯(cuò)誤的出現(xiàn),又使得代碼可以得到簡(jiǎn)化而有效地編寫(xiě)。
var http = require('http');
var BufferHelper = require('bufferhelper');
http.createServer(function (request, response) {
var bufferHelper = new BufferHelper();
request.on("data", function (chunk) {
bufferHelper.concat(chunk);
});
request.on('end', function () {
var html = bufferHelper.toBuffer().toString();
response.writeHead(200);
response.end(html);
});
}).listen(8001);
所以關(guān)于Buffer對(duì)象的操作的最佳實(shí)踐是:
更多建議: