我們先大致看看 NodeJS 提供了哪些和文件操作有關(guān)的 API。這里并不逐一介紹每個(gè) API 的使用方法,官方文檔已經(jīng)做得很好了。
官方文檔: http://nodejs.org/api/buffer.html
JS 語(yǔ)言自身只有字符串?dāng)?shù)據(jù)類型,沒(méi)有二進(jìn)制數(shù)據(jù)類型,因此 NodeJS 提供了一個(gè)與 String 對(duì)等的全局構(gòu)造函數(shù) Buffer 來(lái)提供對(duì)二進(jìn)制數(shù)據(jù)的操作。除了可以讀取文件得到 Buffer 的實(shí)例外,還能夠直接構(gòu)造,例如:
var bin = new Buffer([ 0x68, 0x65, 0x6c, 0x6c, 0x6f ]);
Buffer 與字符串類似,除了可以用.length
屬性得到字節(jié)長(zhǎng)度外,還可以用[index]方式讀取指定位置的字節(jié),例如:
bin[0]; // => 0x68;
Buffer 與字符串能夠互相轉(zhuǎn)化,例如可以使用指定編碼將二進(jìn)制數(shù)據(jù)轉(zhuǎn)化為字符串:
var str = bin.toString('utf-8'); // => "hello"
或者反過(guò)來(lái),將字符串轉(zhuǎn)換為指定編碼下的二進(jìn)制數(shù)據(jù):
var bin = new Buffer('hello', 'utf-8'); // => <Buffer 68 65 6c 6c 6f>
Buffer 與字符串有一個(gè)重要區(qū)別。字符串是只讀的,并且對(duì)字符串的任何修改得到的都是一個(gè)新字符串,原字符串保持不變。至于 Buffer,更像是可以做指針操作的 C 語(yǔ)言數(shù)組。例如,可以用[index]方式直接修改某個(gè)位置的字節(jié)。
bin[0] = 0x48;
而.slice
方法也不是返回一個(gè)新的 Buffer,而更像是返回了指向原 Buffer 中間的某個(gè)位置的指針,如下所示。
[ 0x68, 0x65, 0x6c, 0x6c, 0x6f ]
^ ^
| |
bin bin.slice(2)
因此對(duì).slice
方法返回的 Buffer 的修改會(huì)作用于原 Buffer,例如:
var bin = new Buffer([ 0x68, 0x65, 0x6c, 0x6c, 0x6f ]);
var sub = bin.slice(2);
sub[0] = 0x65;
console.log(bin); // => <Buffer 68 65 65 6c 6f>
也因此,如果想要拷貝一份 Buffer,得首先創(chuàng)建一個(gè)新的 Buffer,并通過(guò).copy
方法把原 Buffer 中的數(shù)據(jù)復(fù)制過(guò)去。這個(gè)類似于申請(qǐng)一塊新的內(nèi)存,并把已有內(nèi)存中的數(shù)據(jù)復(fù)制過(guò)去。以下是一個(gè)例子。
var bin = new Buffer([ 0x68, 0x65, 0x6c, 0x6c, 0x6f ]);
var dup = new Buffer(bin.length);
bin.copy(dup);
dup[0] = 0x48;
console.log(bin); // => <Buffer 68 65 6c 6c 6f>
console.log(dup); // => <Buffer 48 65 65 6c 6f>
總之,Buffer 將 JS 的數(shù)據(jù)處理能力從字符串?dāng)U展到了任意二進(jìn)制數(shù)據(jù)。
官方文檔: http://nodejs.org/api/stream.html
當(dāng)內(nèi)存中無(wú)法一次裝下需要處理的數(shù)據(jù)時(shí),或者一邊讀取一邊處理更加高效時(shí),我們就需要用到數(shù)據(jù)流。NodeJS中通過(guò)各種 Stream 來(lái)提供對(duì)數(shù)據(jù)流的操作。
以上邊的大文件拷貝程序?yàn)槔?,我們可以為?shù)據(jù)來(lái)源創(chuàng)建一個(gè)只讀數(shù)據(jù)流,示例如下:
var rs = fs.createReadStream(pathname);
rs.on('data', function (chunk) {
doSomething(chunk);
});
rs.on('end', function () {
cleanUp();
});
注意: Stream 基于事件機(jī)制工作,所有 Stream 的實(shí)例都繼承于 NodeJS 提供的 EventEmitter。
上邊的代碼中 data 事件會(huì)源源不斷地被觸發(fā),不管 doSomething 函數(shù)是否處理得過(guò)來(lái)。代碼可以繼續(xù)做如下改造,以解決這個(gè)問(wèn)題。
var rs = fs.createReadStream(src);
rs.on('data', function (chunk) {
rs.pause();
doSomething(chunk, function () {
rs.resume();
});
});
rs.on('end', function () {
cleanUp();
});
以上代碼給 doSomething 函數(shù)加上了回調(diào),因此我們可以在處理數(shù)據(jù)前暫停數(shù)據(jù)讀取,并在處理數(shù)據(jù)后繼續(xù)讀取數(shù)據(jù)。
此外,我們也可以為數(shù)據(jù)目標(biāo)創(chuàng)建一個(gè)只寫(xiě)數(shù)據(jù)流,示例如下:
var rs = fs.createReadStream(src);
var ws = fs.createWriteStream(dst);
rs.on('data', function (chunk) {
ws.write(chunk);
});
rs.on('end', function () {
ws.end();
});
我們把 doSomething 換成了往只寫(xiě)數(shù)據(jù)流里寫(xiě)入數(shù)據(jù)后,以上代碼看起來(lái)就像是一個(gè)文件拷貝程序了。但是以上代碼存在上邊提到的問(wèn)題,如果寫(xiě)入速度跟不上讀取速度的話,只寫(xiě)數(shù)據(jù)流內(nèi)部的緩存會(huì)爆倉(cāng)。我們可以根據(jù).write
方法的返回值來(lái)判斷傳入的數(shù)據(jù)是寫(xiě)入目標(biāo)了,還是臨時(shí)放在了緩存了,并根據(jù) drain 事件來(lái)判斷什么時(shí)候只寫(xiě)數(shù)據(jù)流已經(jīng)將緩存中的數(shù)據(jù)寫(xiě)入目標(biāo),可以傳入下一個(gè)待寫(xiě)數(shù)據(jù)了。因此代碼可以改造如下:
var rs = fs.createReadStream(src);
var ws = fs.createWriteStream(dst);
rs.on('data', function (chunk) {
if (ws.write(chunk) === false) {
rs.pause();
}
});
rs.on('end', function () {
ws.end();
});
ws.on('drain', function () {
rs.resume();
});
以上代碼實(shí)現(xiàn)了數(shù)據(jù)從只讀數(shù)據(jù)流到只寫(xiě)數(shù)據(jù)流的搬運(yùn),并包括了防爆倉(cāng)控制。因?yàn)檫@種使用場(chǎng)景很多,例如上邊的大文件拷貝程序,NodeJS 直接提供了.pipe
方法來(lái)做這件事情,其內(nèi)部實(shí)現(xiàn)方式與上邊的代碼類似。
官方文檔: http://nodejs.org/api/fs.html
NodeJS 通過(guò) fs 內(nèi)置模塊提供對(duì)文件的操作。fs 模塊提供的 API 基本上可以分為以下三類:
其中常用的有 fs.stat、fs.chmod、fs.chown 等等。
其中常用的有 fs.readFile、fs.readdir、fs.writeFile、fs.mkdir 等等。
其中常用的有 fs.open、fs.read、fs.write、fs.close 等等。
NodeJS 最精華的異步 IO 模型在 fs 模塊里有著充分的體現(xiàn),例如上邊提到的這些 API 都通過(guò)回調(diào)函數(shù)傳遞結(jié)果。以 fs.readFile 為例:
fs.readFile(pathname, function (err, data) {
if (err) {
// Deal with error.
} else {
// Deal with data.
}
});
如上邊代碼所示,基本上所有 fs 模塊 API 的回調(diào)參數(shù)都有兩個(gè)。第一個(gè)參數(shù)在有錯(cuò)誤發(fā)生時(shí)等于異常對(duì)象,第二個(gè)參數(shù)始終用于返回 API 方法執(zhí)行結(jié)果。
此外,fs 模塊的所有異步 API 都有對(duì)應(yīng)的同步版本,用于無(wú)法使用異步操作時(shí),或者同步操作更方便時(shí)的情況。同步 API 除了方法名的末尾多了一個(gè) Sync 之外,異常對(duì)象與執(zhí)行結(jié)果的傳遞方式也有相應(yīng)變化。同樣以fs.readFileSync 為例:
try {
var data = fs.readFileSync(pathname);
// Deal with data.
} catch (err) {
// Deal with error.
}
fs 模塊提供的 API 很多,這里不一一介紹,需要時(shí)請(qǐng)自行查閱官方文檔。
官方文檔: http://nodejs.org/api/path.html
操作文件時(shí)難免不與文件路徑打交道。NodeJS 提供了 path 內(nèi)置模塊來(lái)簡(jiǎn)化路徑相關(guān)操作,并提升代碼可讀性。以下分別介紹幾個(gè)常用的 API。
path.normalize
將傳入的路徑轉(zhuǎn)換為標(biāo)準(zhǔn)路徑,具體講的話,除了解析路徑中的.與..外,還能去掉多余的斜杠。如果有程序需要使用路徑作為某些數(shù)據(jù)的索引,但又允許用戶隨意輸入路徑時(shí),就需要使用該方法保證路徑的唯一性。以下是一個(gè)例子:
var cache = {};
function store(key, value) {
cache[path.normalize(key)] = value;
}
store('foo/bar', 1);
store('foo//baz//../bar', 2);
console.log(cache); // => { "foo/bar": 2 }
注意: 標(biāo)準(zhǔn)化之后的路徑里的斜杠在 Windows 系統(tǒng)下是
\
,而在 Linux 系統(tǒng)下是/
。如果想保證任何系統(tǒng)下都使用/
作為路徑分隔符的話,需要用.replace(/\\/g, '/')
再替換一下標(biāo)準(zhǔn)路徑。
path.join
將傳入的多個(gè)路徑拼接為標(biāo)準(zhǔn)路徑。該方法可避免手工拼接路徑字符串的繁瑣,并且能在不同系統(tǒng)下正確使用相應(yīng)的路徑分隔符。以下是一個(gè)例子:
path.join('foo/', 'baz/', '../bar'); // => "foo/bar"
path.extname
當(dāng)我們需要根據(jù)不同文件擴(kuò)展名做不同操作時(shí),該方法就顯得很好用。以下是一個(gè)例子:
path.extname('foo/bar.js'); // => ".js"
path 模塊提供的其余方法也不多,稍微看一下官方文檔就能全部掌握。
更多建議: