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