Node.js進階編程:用JavaScript構建可伸縮應用(7)3.7 檔案,進程,流和網路-查詢和讀寫檔案

來源:互聯網
上載者:User
文章目錄
  • 處理檔案路徑
  • fs模組介紹

本系列文章列表和翻譯進度,請移步:Node.js進階編程:用Javascript構建可伸縮應用(〇)

本文對應原文第三部分第七章:Files, Processes, Streams, and Networking:Querying, Reading from, and Writing to Files

文章是從Word複製到過來的,版面有些不一致,可以點這裡下載本文的PDF版。

 

第七章:查詢和讀寫檔案本章內容:
  • 處理檔案路徑
  • 從檔案路徑萃取資訊
  • 理解檔案描述符
  • 使用fs.stat()擷取檔案資訊
  • 開啟,讀寫,關閉檔案
  • 避免檔案描述符泄露

 

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函數來標準化一個路徑,而且它還能處理“..”,“.”“//”。比如:

                  var path = require('path');                   path.normalize('/foo/bar//baz/asdf/quux/..');                   // => '/foo/bar/baz/asdf'
串連路徑

使用path.join()函數,可以串連任意多個路徑字串,只用把所有路徑字串依次傳遞給join()函數就可以:

                   var path = require('path');                   path.join('/foo', 'bar', 'baz/asdf', 'quux', '..');                   // => '/foo/bar/baz/asdf'

         如你所見,path.join()內部會自動將路徑標準化。

解析路徑

用path.resolve()可以把多個路徑解析為一個絕對路徑。它的功能就像對這些路徑挨個不斷進行“cd”操作,和cd命令的參數不同,這些路徑可以是檔案,並且它們不必真實存在——path.resolve()方法不會去訪問底層檔案系統來確定路徑是否存在,它只是一些字串操作。

比如:

                   var path = require('path');                   path.resolve('/foo/bar', './baz');                   // => /foo/bar/baz                   path.resolve('/foo/bar', '/tmp/file/');                   // => /tmp/file

         如果解析結果不是絕對路徑,path.resolve()會把當前工作目錄作為路徑附加到解析結果前面,比如:

        path.resolve('wwwroot', 'static_files/png/', '../gif/image.gif');        // 如果當前工作目錄是/home/myself/node, 將返回        // => /home/myself/node/wwwroot/static_files/gif/image.gif'
計算兩個絕對路徑的相對路徑

path.relative()可以告訴你如果從一個絕對位址跳轉到另外一個絕對位址,比如:

                var path = require('path');                path.relative('/data/orandea/test/aaa', '/data/orandea/impl/bbb');                // => http://www.cnblogs.com/impl/bbb
從路徑提取資料

以路徑“/foo/bar/myfile.txt”為例,如果你想擷取父目錄(/foo/bar)的所有內容,或者讀取同級目錄的其它檔案,為此,你必須用path.dirname(filePath)獲得檔案路徑的目錄部分,比如:

                   var path = require('path');                   path.dirname('/foo/bar/baz/asdf/quux.txt');                   // => /foo/bar/baz/asdf

         或者,你想從檔案路徑裡得到檔案名稱,也就是檔案路徑的最後那一部分,可以使用path.basename函數:

                   var path = require('path');                   path.basename('/foo/bar/baz/asdf/quux.html')                   // => quux.html

         檔案路徑裡可能還包含副檔名,通常是檔案名稱中最後一個“.”字元之後的那部分字串。

         path.basename還可以接受一個副檔名字串作為第二個參數,這樣返回的檔案名稱就會自動去掉副檔名,僅僅返迴文件的名稱部分:

                   var path = require('path');                   path.basename('/foo/bar/baz/asdf/quux.html', '.html');                   // => quux

         要想這麼做你首先還得知道檔案的副檔名,可以用path.extname()來擷取副檔名:

                   var path = require('path');                   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():

                   var path = require('path');                   path.exists('/etc/passwd', function(exists) {                            console.log('exists:', exists);                            // => true                   });                   path.exists('/does_not_exist', function(exists) {                            console.log('exists:', exists);                            // => false                   });

         注意:從Node0.8版本開始,exists從path模組移到了fs模組,變成了fs.exists,除了命名空間不同,其它都沒變:

                   var fs = require('fs');                   fs.exists('/does_not_exist', function(exists) {                            console.log('exists:', exists);                            // => false                   });

         path.exists()是個I/O操作,因為它是非同步,因此需要一個回呼函數,當I/O操作返回後調用這個回呼函數,並把結果傳遞給它。你還可以使用它的同步版本path.existsSync(),功能完全一樣,只是它不會調用回呼函數,而是直接返回結果:

                  var path = require('path');                 path.existsSync('/etc/passwd');                 // => true
fs模組介紹

fs模組包含所有檔案查詢和處理的相關函數,用這些函數,可以查詢檔案資訊,讀寫和關閉檔案。這樣匯入fs模組:

         var fs = require(‘fs’)
查詢檔案資訊

有時你可能需要知道檔案的大小,建立日期或者許可權等檔案資訊,可以使用fs.stath函數來查詢檔案或目錄的元資訊:

                   var fs = require('fs');                  fs.stat('/etc/passwd', function(err, stats) {                                    if (err) { throw err;}                                    console.log(stats);                  });

這塊代碼片斷會有類似下面的輸出:

 { dev: 234881026,ino: 95028917,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 }

         fs.stat()調用會將一個stats類的執行個體作為參數傳遞給它的回呼函數,可以像下面這樣使用stats執行個體:

  • stats.isFile() —— 如果是個標準檔案,而不是目錄,socket,符號連結或者裝置,則返回true,否則false
  • stats.isDiretory() —— 如果是目錄則返回tue,否則false
  • stats.isBlockDevice() —— 如果是塊裝置則返回true,在大多數UNIX系統中塊裝置通常都在/dev目錄下
  • stats.isChracterDevice() —— 如果是字元裝置返回true
  • stats.isSymbolickLink() —— 如果是檔案連結返回true
  • stats.isFifo() —— 如果是個FIFO(UNIX具名管道的一個特殊類型)返回true
  • stats.isSocket() —— 如果是個UNIX socket(TODO:googe it)
開啟檔案

在讀取或處理檔案之前,必須先使用fs.open函數開啟檔案,然後你提供的回呼函數會被調用,並得到這個檔案的描述符,稍後你可以用這個檔案描述符來讀寫這個已經開啟的檔案:

                   var fs = require('fs');                   fs.open('/path/to/file', 'r', function(err, fd) {                        // got fd file descriptor                   });

fs.open的第一個參數是檔案路徑,第二個參數是一些用來指示以什麼模式開啟檔案的標記,這些標記可以是r,r+,w,w+,a或者a+。下面是這些標記的說明(來自UNIX文檔的fopen頁)

  • r —— 以唯讀方式開啟檔案,資料流的初始位置在檔案開始
  • r+ —— 以可讀寫方式開啟檔案,資料流的初始位置在檔案開始
  • w ——如果檔案存在,則將檔案長度清0,即該檔案內容會丟失。如果不存在,則嘗試建立它。資料流的初始位置在檔案開始
  • w+ —— 以可讀寫方式開啟檔案,如果檔案不存在,則嘗試建立它,如果檔案存在,則將檔案長度清0,即該檔案內容會丟失。資料流的初始位置在檔案開始
  • a —— 以唯寫方式開啟檔案,如果檔案不存在,則嘗試建立它,資料流的初始位置在檔案末尾,隨後的每次寫操作都會將資料追加到檔案後面。
  • a+ ——以可讀寫方式開啟檔案,如果檔案不存在,則嘗試建立它,資料流的初始位置在檔案末尾,隨後的每次寫操作都會將資料追加到檔案後面。
讀檔案

一旦開啟了檔案,就可以開始讀取檔案內容,但是在開始之前,你得先建立一個緩衝區(buffer)來放置這些資料。這個緩衝區對象將會以參數形式傳遞給fs.read函數,並被fs.read填充上資料。

var fs = require('fs');fs.open('./my_file.txt', 'r', function opened(err, fd) {if (err) { throw err }var readBuffer = new Buffer(1024),bufferOffset = 0,bufferLength = readBuffer.length,filePosition = 100;fs.read(fd,         readBuffer,         bufferOffset,         bufferLength,         filePosition,         function read(err, readBytes) {                   if (err) { throw err; }                   console.log('just read ' + readBytes + ' bytes');                   if (readBytes > 0) {                            console.log(readBuffer.slice(0, readBytes));                   }});});

         上面代碼嘗試開啟一個檔案,當成功開啟後(調用opened函數),開始請求從檔案流第100個位元組開始讀取隨後1024個位元組的資料(第11行)。

         fs.read()的最後一個參數是個回呼函數(第16行),當下面三種情況發生時,它會被調用:

  • 有錯誤發生
  • 成功讀取了資料
  • 沒有資料可讀

         如果有錯誤發生,第一個參數(err)會為回呼函數提供一個包含錯誤資訊的對象,否則這個參數為null。如果成功讀取了資料,第二個參數(readBytes)會指明被讀到緩衝區裡資料的大小,如果值是0,則表示到達了檔案末尾。

         注意:一旦把緩衝區對象傳遞給fs.open(),緩衝對象的控制權就轉移給給了read命令,只有當回呼函數被調用,緩衝區對象的控制權才會回到你手裡。因此在這之前,不要讀寫或者讓其它函數調用使用這個緩衝區對象;否則,你可能會讀到不完整的資料,更糟的情況是,你可能會並發地往這個緩衝區對象裡寫資料。

寫檔案

通過傳遞給fs.write()傳遞一個包含資料的緩衝對象,來往一個已開啟的檔案裡寫資料:

var fs = require('fs');fs.open('./my_file.txt', 'a', function opened(err, fd) {    if (err) { throw err; }    var writeBuffer = new Buffer('writing this string'),    bufferPosition = 0,    bufferLength = writeBuffer.length, filePosition = null;    fs.write( fd,        writeBuffer,        bufferPosition,        bufferLength,        filePosition,        function wrote(err, written) {           if (err) { throw err; }           console.log('wrote ' + written + ' bytes');        });});

         這個例子裡,第2(譯者註:原文為3)行代碼嘗試用追加模式(a)開啟一個檔案,然後第7行代碼(譯者註:原文為9)向檔案寫入資料。緩衝區對象需要附帶幾個資訊一起做為參數:

  • 緩衝區的資料
  • 待寫資料從緩衝區的什麼位置開始
  • 待寫資料的長度
  • 資料寫到檔案的哪個位置
  • 當操作結束後被調用的回呼函數wrote

         這個例子裡,filePostion參數為null,也就是說write函數將會把資料寫到檔案指標當前所在的位置,因為是以追加模式開啟的檔案,因此檔案指標在檔案末尾。

         跟read操作一樣,千萬不要在fs.write執行過程中使用哪個傳入的緩衝區對象,一旦fs.write開始執行它就獲得了那個緩衝區對象的控制權。你只能等到回呼函數被調用後才能再重新使用它。

關閉檔案

你可能注意到了,到目前為止,本章的所有例子都沒有關閉檔案的代碼。因為它們只是些僅使用一次而且又小又簡單的例子,當Node進程結束時,作業系統會確保關閉所有檔案。

但是,在實際的應用程式中,一旦開啟一個檔案你要確保最終關閉它。要做到這一點,你需要追蹤所有那些已開啟的檔案描述符,然後在不再使用它們的時候調用fs.close(fd[,callback])來最終關閉它們。如果你不仔細的話,很容易就會遺漏某個檔案描述符。下面的例子提供了一個叫openAndWriteToSystemLog的函數,展示了如何小心的關閉檔案:

var fs = require('fs');function openAndWriteToSystemLog(writeBuffer, callback){    fs.open('./my_file', 'a', function opened(err, fd) {        if (err) { return callback(err); }        function notifyError(err) {            fs.close(fd, function() {                callback(err);            });        }        var bufferOffset = 0,        bufferLength = writeBuffer.length,        filePosition = null;        fs.write( fd, writeBuffer, bufferOffset, bufferLength, filePosition,            function wrote(err, written) {                if (err) { return notifyError(err); }                fs.close(fd, function() {                    callback(err);                });            }        );    });}openAndWriteToSystemLog(    new Buffer('writing this string'),    function done(err) {        if (err) {            console.log("error while opening and writing:", err.message);            return;        }        console.log('All done with no errors');    });

         在這兒,提供了一個叫openAndWriteToSystemLog的函數,它接受一個包含待寫資料的緩衝區對象,以及一個操作完成或者出錯後被調用的回呼函數,如果有錯誤發生,回呼函數的第一個參數會包含這個錯誤對象。

         注意那個內建函式notifyError,它會關閉檔案,並報告發生的錯誤。

         注意:到此為止,你知道了如何使用底層的原子操作來開啟,讀,寫和關閉檔案。然而,Node還有一組更進階的建構函式,允許你用更簡單的方式來處理檔案。

         比如,你想用一種安全的方式,讓兩個或者多個write操作並發的往一個檔案裡追加資料,這時你可以使用WriteStream

         還有,如果你想讀取一個檔案的某個地區,可以考慮使用ReadStream。這兩種用例會在第九章“資料的讀,寫流”裡介紹。

小結

當你使用檔案時,多數情況下都需要處理和提取檔案路徑資訊,通過使用path模組你可以串連路徑,標準化路徑,計算路徑的差別,以及將相對路徑轉化成絕對路徑。你可以提取指定檔案路徑的副檔名,檔案名稱,目錄等路徑組件。

Node在fs模組裡提供了一套底層API來訪問檔案系統,底層API使用檔案描述符來操作檔案。你可以用fs.open開啟檔案,用fs.write寫檔案,用fs.read讀檔案,並用fs.close關閉檔案。

當有錯誤發生時,你應該總是使用正確的錯誤處理邏輯來關閉檔案——以確保在調用返回前關閉那些已開啟的檔案描述符。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.