Node有一組資料流API,可以像處理網路流那樣處理文件,用起來很方便,但是它只允許順序處理文件,不能隨機讀寫文件。因此,需要使用一些更底層的檔案系統操作。
本章涵蓋了文件處理的基礎知識,包括如何開啟文件,讀取文件某一部分,寫數據,以及關閉文件。
Node的許多檔案API幾乎是UNIX(POSIX)中對應檔案API 的翻版,例如使用檔案描述子的方式,就像UNIX裡一樣,檔案描述子在Node裡也是一個整數數字,代表一個實體在進程檔案描述符表裡的索引。
有3個特殊的文件描述符-1,2和3。他們分別代表標準輸入,標準輸出和標準錯誤檔案描述符。標準輸入,顧名思義,是個唯讀流,進程用它來從控制台或進程通道讀取資料。標準輸出和標準錯誤是僅用來輸出資料的檔案描述符,他們經常被用來向控制台,其它進程或檔案輸出資料。標準錯誤負責錯誤訊息輸出,而標準輸出負責普通的進程輸出。
一旦進程啟動完畢,就能使用這幾個檔案描述符了,它們其實並不存在對應的實體檔案。你不能讀寫某個隨機位置的數據,(譯者註:原文是You can write to and read from specific positions within the file.根據上下文,作者可能少寫了個「not」),只能像操作網絡資料流那樣順序的讀取和輸出,已寫入的資料就不能再修改了。
普通文件不受這種限制,例如Node裡,你即可以創建只能向尾部追加資料的文件,還可以創建讀寫隨機位置的文件。
幾乎所有跟文件相關的操作都會涉及到處理文件路徑,本章先將介紹這些工具函數,然後再深入講解文件讀寫和資料操作
處理檔案路徑
檔案路徑分為相對路徑和絕對路徑兩種,用它們來表示具體的檔案。你可以合併文件路徑,可以提取文件名信息,甚至可以檢測文件是否存在。
Node裡,可以用字串來操處理檔案路徑,但是那樣會使問題變得複雜,例如你要連接路徑的不同部分,有些部分以「/」結尾有些卻沒有,而且路徑分割符在不同作業系統裡也可能會不一樣,所以,當你連接它們時,程式碼就會非常囉嗦和麻煩。
幸運的是,Node有個叫path的模組,可以幫你標準化,連接,解析路徑,從絕對路徑轉換到相對路徑,從路徑中提取各部分信息,檢測文件是否存在。總的來說,path模組其實只是些字串處理,而且也不會到檔案系統去做驗證(path.exists函數例外)。
路徑的標準化
在儲存或使用路徑之前將它們標準化通常是個好主意。例如,由使用者輸入或設定檔所獲得的檔案路徑,或是由兩個或多個路徑連接起來的路徑,一般都應該被標準化。可以用path模組的normalize函數來標準化一個路徑,而且它還能處理“..”,“.”“//”。如:
path.normalize('/foo/bar//baz/asdf/quux/..');
// => '/foo/bar/baz/asdf'
連接路徑
使用path.join()函數,可以連接任意多個路徑字串,只用把所有路徑字串依序傳遞給join()函數就可以:
path.join('/foo', 'bar', 'baz/asdf', 'quux', '..');
// => '/foo/bar/baz/asdf'
如你所見,path.join()內部會自動將路徑標準化。
解析路徑
用path.resolve()可以把多條路徑解析為一個絕對路徑。它的功能就像對這些路徑挨個不斷進行「cd」操作,和cd指令的參數不同,這些路徑可以是文件,而且它們不必真實存在——path.resolve()方法不會去存取底層文件系統來確定路徑是否存在,它只是一些字串操作。
例如:
path.resolve('/foo/bar', './baz');
// =>/foo/bar/baz
path.resolve('/foo/bar', '/tmp/file/');
// =>/tmp/file
如果解析結果不是絕對路徑,path.resolve()會把目前工作目錄當作路徑附加到解析結果前面,例如:
計算兩條絕對路徑的相對路徑
path.relative()可以告訴你如果從一個絕對位址跳到另一個絕對位址,例如:
path.relative('/data/orandea/test/aaa', '/data/orandea/impl/bbb');
// => ../../impl/bbb
從路徑擷取資料
以路徑“/foo/bar/myfile.txt”為例,如果你想獲取父目錄(/foo/bar)的所有內容,或者讀取同級目錄的其它文件,為此,你必須用path.dirname(filePath)取得檔案路徑的目錄部分,例如:
path.dirname('/foo/bar/baz/asdf/quux.txt');
// =>/foo/bar/baz/asdf
或者,你想從檔案路徑裡得到檔名,也就是檔案路徑的最後一部分,可以使用path.basename函數:
path.basename('/foo/bar/baz/asdf/quux.html')
// => quux.html
檔案路徑裡可能還包含檔案副檔名,通常是檔案名稱中最後一個「.」字元之後的那部分字串。
path.basename也可以接受一個副檔名字串作為第二個參數,這樣傳回的檔名就會自動去掉副檔名,只回傳檔案的名稱部分:
path.basename('/foo/bar/baz/asdf/quux.html', '.html');
// => quux
要想這麼做你首先還要知道檔案的副檔名,可以用path.extname()來取得副檔名:
path.extname('/a/b/index.html');
// => '.html'
path.extname('/a/b.c/index');
// => ''
path.extname('/a/b.c/.');
// => ''
path.extname('/a/b.c/d.');
// => '.'
檢查路徑是否存在
目前為止,前面涉及到的路徑處理操作都跟底層檔案系統無關,只是一些字串操作。然而,有些時候你需要判斷一個檔案路徑是否存在,例如,你有時候需要判斷檔案或目錄是否存在,如果不存在的話才建立它,可以用path.exsits():
path.exists('/etc/passwd', function(exists) {
console.log('exists:', exists);
;// => true
});
path.exists('/does_not_exist', function(exists) {
console.log('exists:', exists);
;
// => 中
注意:從Node0.8版本開始,exists從path模組移到了fs模組,變成了fs.exists,除了命名空間不同,其它都沒變:
fs.exists('/does_not_exist', function(exists) {
console.log('exists:', exists);
;// => 中
});
path.exists()是個I/O操作,因為它是非同步的,因此需要一個回呼函數,當I/O操作返回後呼叫這個回調函數,並且把結果傳遞給它。你也可以使用它的同步版本path.existsSync(),功能完全一樣,只是它不會呼叫回呼函數,而是直接回傳結果:
程式碼如下:
path.existsSync('/etc/passwd');
// => true
fs模組介紹
fs模組包含所有文件查詢和處理的相關函數,用這些函數,可以查詢文件信息,讀寫和關閉文件。這樣導入fs模組:
查詢文件資訊
有時你可能需要知道文件的大小,創建日期或權限等文件信息,可以使用fs.stath函數來查詢文件或目錄的元信息:
fs.stat('/etc/passwd', function(err, stats) {
});
mode: 33188,
nlink: 1,
uid: 0,
gid: 0,
rdev: 0,
size: 5086,
blksize: 4096,
blocks: 0,
atime: Fri, 18 Nov 2011 22:44:47 GMT,
mtime: Thu, 08 Sep 2011 23:50:04 GMT,
ctime: Thu, 08 Sep 2011 23:50:04 GMT }
1.fs.stat()呼叫會將一個stats類別的實例作為參數傳遞給它的回調函數,可以像下面這樣使用stats實例:
2.stats.isFile() —— 如果是標準文件,而不是目錄,socket,符號連結或設備,則傳回true,否則false
3.stats.isDiretory() —— 如果是目錄則回傳tue,否則false
4.stats.isBlockDevice() —— 如果是區塊裝置則回傳true,在大多數UNIX系統中區塊裝置通常都在/dev目錄下
5.stats.isChracterDevice() —— 如果是字元裝置回傳true
6.stats.isSymbolickLink() —— 如果是檔案連結回傳true
7.stats.isFifo() —— 如果是FIFO(UNIX命名管道的一個特殊類型)回傳true
8.stats.isSocket() —— 如果是個UNIX socket(TODO:googe it)
開啟檔案
在讀取或處理文件之前,必須先使用fs.open函數打開文件,然後你提供的回調函數會被調用,並得到這個文件的描述符,稍後你可以用這個文件描述符來讀寫這個已經開啟的檔案:
fs.open('/path/to/file', 'r', function(err, fd) {
// got fd file descriptor
});
fs.open的第一個參數是檔案路徑,第二個參數是一些用來指示以什麼模式開啟檔案的標記,這些標記可以是r,r ,w,w ,a或a 。以下是這些標記的說明(來自UNIX文件的fopen頁)
1.r —— 以唯讀方式開啟文件,資料流的初始位置在文件開始
2.r —— 以可讀寫方式開啟文件,資料流的初始位置在文件開始
3.w ——如果文件存在,則將文件長度清0,即該文件內容會遺失。如果不存在,則嘗試建立它。資料流的初始位置在檔案開始
4.w —— 以可讀寫方式開啟文件,如果文件不存在,則嘗試建立它,如果文件存在,則將文件長度清除,即該文件內容會遺失。資料流的初始位置在檔案開始
5.a —— 以只寫方式開啟文件,如果文件不存在,則嘗試建立它,資料流的初始位置在文件末尾,隨後的每次寫入操作都會將資料追加到文件後面。
6.a ——以可讀寫方式開啟文件,如果文件不存在,則嘗試建立它,資料流的初始位置在文件末尾,隨後的每次寫入操作都會將資料追加到文件後面。
讀取檔案
一旦開啟了文件,就可以開始讀取文件內容,但是在開始之前,你得先創建一個緩衝區(buffer)來放置這些資料。這個緩衝區物件將會以參數形式傳遞給fs.read函數,並被fs.read填入上資料。
fs.open('./my_file.txt', 'r', 函數開啟(err, fd) {
if (err) { 拋出錯誤 }
var readBuffer = new Buffer(1024),
bufferOffset = 0,
bufferLength = readBuffer.length,
檔案位置 = 100;
fs.read(fd,
readBuffer,
bufferOffset,
bufferLength,
檔案位置,
函數 read(err, readBytes) {
if (err) { throw err; }
在已讀出' readBytes '位元組');
if (readBytes > 0) {
console.log(readBuffer.slice(0, readBytes));
}
});
});
上面程式碼嘗試開啟一個文件,當開啟成功後(呼叫開啟的函數),開始請求從文件流第100個位元組讀取開始附帶1024個位元組的資料(第11行)。
fs.read()的最後一個參數是個回呼函數(第16行),當下面發生異常情況時,它會被呼叫:
1.發生錯誤
2.成功讀取了資料
3.沒有資料差異
如果發生錯誤,第一個參數(err)會為回呼函數提供一個包含錯誤訊息的對象,否則這個參數為null。如果成功讀取了數據,第二個參數(readBytes)會指明被讀取到塔里數據的大小,如果值為0,則表示到達了檔案結構。
注意:一旦把蠟燭圖物件傳遞給fs.open(),緩衝物件的控制權就轉移給了read指令,只有當回呼函數被呼叫時,蠟燭圖物件的控制權才會回到你手因此在這之前,不要讓讀寫器或讓其他函數呼叫使用這個瀑布物件裡;否則,你可能會讀到不完整的數據,更糟糕的情況是,你可能會並發地到這個瀑布物件裡寫入數據。
寫文件
透過傳遞給fs.write()傳遞一個包含資料的緩衝對象,來往一個已開啟的檔案裡寫入資料:
fs.open('./my_file.txt', 'a', 函數開啟(err, fd) {
if (err) { throw err; }
var writeBuffer = new Buffer('寫入此字串'),
bufferPosition = 0,
bufferLength = writeBuffer.length, filePosition = null;
fs.write( fd,
writeBuffer,
bufferPosition,
bufferLength,
檔案位置,
函數 write(錯誤,已寫) {
if (err) { throw err; }
console.log('已寫了'寫了'字節');
});
});
這個例子裡,第2(譯者註:原文為3)行程式碼嘗試用追加模式(a)開啟一個文件,然後第7行程式碼(譯者註:原文為9)寫入資料到資料。緩衝區物件需要附帶幾個資訊一起做為參數:
1.緩衝區的資料
2.待寫資料從緩衝區的什麼位置開始
3.待寫資料的長度
4.資料寫到檔案的哪個位置
5.當操作結束後被呼叫的回調函數wrote
這個例子裡,filePostion參數為null,也就是說write函數會把資料寫到檔案指標目前所在的位置,因為是以追加模式開啟的文件,因此文件指標在檔案結尾。
跟read操作一樣,千萬不要在fs.write執行過程中使用哪個傳入的緩衝區對象,一旦fs.write開始執行它就獲得了那個緩衝區對象的控制權。你只能等到回調函數被呼叫後才能再重新使用它。
關閉檔案
你可能注意到了,到目前為止,本章的所有範例都沒有關閉檔案的程式碼。因為它們只是些只使用一次而且又小又簡單的例子,當Node進程結束時,作業系統會確保關閉所有檔案。
但是,在實際的應用程式中,一旦打開一個檔案你要確保最終關閉它。要做到這一點,你需要追蹤所有那些已開啟的檔案描述符,然後在不再使用它們的時候呼叫fs.close(fd[,callback])來最終關閉它們。如果你不仔細的話,很容易就會遺漏某個文件描述符。下面的範例提供了一個叫做openAndWriteToSystemLog的函數,展示如何小心的關閉檔案:
在這兒,提供了一個叫openAndWriteToSystemLog的函數,它接受一個包含待寫資料的緩衝區對象,以及一個操作完成或出錯後被調用的回調函數,如果有錯誤發生,回呼函數的第一個參數會包含這個錯誤物件。
注意那個內部函數notifyError,它會關閉文件,並報告發生的錯誤。
注意:到此為止,你知道如何使用底層的原子操作來打開,讀,寫和關閉文件。然而,Node還有一組更高階的建構函數,讓你可以用更簡單的方式來處理檔案。
例如,你想用一種安全的方式,讓兩個或多個write操作並發的往一個檔案裡追加數據,這時你可以使用WriteStream。
還有,如果你想讀取一個檔案的某個區域,可以考慮使用ReadStream。這兩種用例會在第九章「資料的讀,寫流」裡介紹。
小結
當你使用文件時,多數情況下都需要處理和提取文件路徑信息,透過使用path模組你可以連接路徑,標準化路徑,計算路徑的差別,以及將相對路徑轉化成絕對路徑。你可以提取指定檔案路徑的副檔名,檔案名,目錄等路徑元件。
Node在fs模組裡提供了一套底層API來存取檔案系統,底層API使用檔案描述子來操作檔案。你可以用fs.open開啟文件,用fs.write寫文件,用fs.read讀取文件,並用fs.close關閉文件。
當有錯誤發生時,你應該總是使用正確的錯誤處理邏輯來關閉檔案-以確保在呼叫返回之前關閉那些已開啟的檔案描述符。