讓前端覺得如獲神器的不是NodeJS能做網絡程式設計,而是NodeJS能夠操作檔案。小至檔案查找,大至代碼編譯,幾乎沒有一個前端工具不操作檔案。換個角度講,幾乎也隻需要一些資料處理邏輯,再加上一些檔案操作,就能夠編寫出大多數前端工具。本章将介紹與之相關的NodeJS内置子產品。
NodeJS提供了基本的檔案操作API,但是像檔案拷貝這種進階功能就沒有提供,是以我們先拿檔案拷貝程式練手。與<code>copy</code>指令類似,我們的程式需要能接受源檔案路徑與目标檔案路徑兩個參數。
一、檔案拷貝
1、小檔案拷貝
我們使用NodeJS内置的<code>fs</code>子產品簡單實作這個程式如下
以上程式使用<code>fs.readFileSync</code>從源路徑讀取檔案内容,并使用<code>fs.writeFileSync</code>将檔案内容寫入目标路徑。
注意:<code>process</code>是一個全局變量,可通過<code>process.argv</code>獲得指令行參數。由于<code>argv[0]</code>固定等于NodeJS執行程式的絕對路徑,<code>argv[1]</code>固定等于主子產品的絕對路徑,是以第一個指令行參數從<code>argv[2]</code>這個位置開始。
2、大檔案拷貝
上邊的程式拷貝一些小檔案沒啥問題,但這種一次性把所有檔案内容都讀取到記憶體中後再一次性寫入磁盤的方式不适合拷貝大檔案,記憶體會爆倉。對于大檔案,我們隻能讀一點寫一點,直到完成拷貝。是以上邊的程式需要改造如下。
以上程式使用<code>fs.createReadStream</code>建立了一個源檔案的隻讀資料流,并使用<code>fs.createWriteStream</code>建立了一個目标檔案的隻寫資料流,并且用<code>pipe</code>方法把兩個資料流連接配接了起來。連接配接起來後發生的事情,說得抽象點的話,水順着水管從一個桶流到了另一個桶。
二、API
我們先大緻看看NodeJS提供了哪些和檔案操作有關的API。這裡并不逐一介紹每個API的使用方法,官方文檔已經做得很好了。
1、Buffer(資料塊)
官方文檔: http://nodejs.org/api/buffer.html
JS語言自身隻有字元串資料類型,沒有二進制資料類型,是以NodeJS提供了一個與<code>String</code>對等的全局構造函數<code>Buffer</code>來提供對二進制資料的操作。除了可以讀取檔案得到<code>Buffer</code>的執行個體外,還能夠直接構造
<code> Buffer</code>與字元串有一個重要差別。字元串是隻讀的,并且對字元串的任何修改得到的都是一個新字元串,原字元串保持不變。至于<code>Buffer</code>,更像是可以做指針操作的C語言數組。例如,可以用<code>[index]</code>方式直接修改某個位置的位元組。
而.slice方法也不是傳回一個新的Buffer,而更像是傳回了指向原Buffer中間的某個位置的指針,如下所示。
是以對<code>.slice</code>方法傳回的<code>Buffer</code>的修改會作用于原<code>Buffer</code>,例如:
也是以,如果想要拷貝一份<code>Buffer</code>,得首先建立一個新的<code>Buffer</code>,并通過<code>.copy</code>方法把原<code>Buffer</code>中的資料複制過去。這個類似于申請一塊新的記憶體,并把已有記憶體中的資料複制過去。以下是一個例子。
總之,<code>Buffer</code>将JS的資料處理能力從字元串擴充到了任意二進制資料。
2、Stream(資料流)
官方文檔: http://nodejs.org/api/stream.html
當記憶體中無法一次裝下需要處理的資料時,或者一邊讀取一邊處理更加高效時,我們就需要用到資料流。NodeJS中通過各種<code>Stream</code>來提供對資料流的操作。
以上邊的大檔案拷貝程式為例,我們可以為資料來源建立一個隻讀資料流,示例如下
注意:<code>Stream</code>基于事件機制工作,所有<code>Stream</code>的執行個體都繼承于NodeJS提供的EventEmitter。
上邊的代碼中<code>data</code>事件會源源不斷地被觸發,不管<code>doSomething</code>函數是否處理得過來。代碼可以繼續做如下改造,以解決這個問題
以上代碼給<code>doSomething</code>函數加上了回調,是以我們可以在處理資料前暫停資料讀取,并在處理資料後繼續讀取資料。
此外,我們也可以為資料目标建立一個隻寫資料流,示例如下:
我們把<code>doSomething</code>換成了往隻寫資料流裡寫入資料後,以上代碼看起來就像是一個檔案拷貝程式了。但是以上代碼存在上邊提到的問題,如果寫入速度跟不上讀取速度的話,隻寫資料流内部的緩存會爆倉。我們可以根據<code>.write</code>方法的傳回值來判斷傳入的資料是寫入目标了,還是臨時放在了緩存了,并根據<code>drain</code>事件來判斷什麼時候隻寫資料流已經将緩存中的資料寫入目标,可以傳入下一個待寫資料了。是以代碼可以改造如下:
以上代碼實作了資料從隻讀資料流到隻寫資料流的搬運,并包括了防爆倉控制。因為這種使用場景很多,例如上邊的大檔案拷貝程式,NodeJS直接提供了<code>.pipe</code>方法來做這件事情,其内部實作方式與上邊的代碼類似。
3、File System(檔案系統)
官方文檔: http://nodejs.org/api/fs.html
NodeJS通過<code>fs</code>内置子產品提供對檔案的操作。<code>fs</code>子產品提供的API基本上可以分為以下三類:
檔案屬性讀寫。
其中常用的有<code>fs.stat</code>、<code>fs.chmod</code>、<code>fs.chown</code>等等。
檔案内容讀寫。
其中常用的有<code>fs.readFile</code>、<code>fs.readdir</code>、<code>fs.writeFile</code>、<code>fs.mkdir</code>等等。
底層檔案操作。
其中常用的有<code>fs.open</code>、<code>fs.read</code>、<code>fs.write</code>、<code>fs.close</code>等等。
NodeJS最精華的異步IO模型在<code>fs</code>子產品裡有着充分的展現,例如上邊提到的這些API都通過回調函數傳遞結果。以<code>fs.readFile</code>為例
如上邊代碼所示,基本上所有<code>fs</code>子產品API的回調參數都有兩個。第一個參數在有錯誤發生時等于異常對象,第二個參數始終用于傳回API方法執行結果。
此外,<code>fs</code>子產品的所有異步API都有對應的同步版本,用于無法使用異步操作時,或者同步操作更友善時的情況。同步API除了方法名的末尾多了一個<code>Sync</code>之外,異常對象與執行結果的傳遞方式也有相應變化。同樣以<code>fs.readFileSync</code>為例:
<code> fs</code>子產品提供的API很多,需要時請自行查閱官方文檔
4、Path(路徑)
官方文檔: http://nodejs.org/api/path.html
操作檔案時難免不與檔案路徑打交道。NodeJS提供了<code>path</code>内置子產品來簡化路徑相關操作,并提升代碼可讀性。以下分别介紹幾個常用的API。
path.normalize:将傳入的路徑轉換為标準路徑,具體講的話,除了解析路徑中的<code>.</code>與<code>..</code>外,還能去掉多餘的斜杠。如果有程式需要使用路徑作為某些資料的索引,但又允許使用者随意輸入路徑時,就需要使用該方法保證路徑的唯一性。
注意: 标準化之後的路徑裡的斜杠在Windows系統下是<code>\</code>,而在Linux系統下是<code>/</code>。如果想保證任何系統下都使用<code>/</code>作為路徑分隔符的話,需要用<code>.replace(/\\/g, '/')</code>再替換一下标準路徑。
path.extname:當我們需要根據不同檔案擴充名做不同操作時,該方法就顯得很好用
<code> path</code>子產品提供的其餘方法也不多,稍微看一下官方文檔就能全部掌握。
三、周遊目錄
周遊目錄是操作檔案時的一個常見需求。比如寫一個程式,需要找到并處理指定目錄下的所有JS檔案時,就需要周遊整個目錄。
1、遞歸算法
周遊目錄時一般使用遞歸算法,否則就難以編寫出簡潔的代碼。遞歸算法與數學歸納法類似,通過不斷縮小問題的規模來解決問題。
陷阱: 使用遞歸算法編寫的代碼雖然簡潔,但由于每遞歸一次就産生一次函數調用,在需要優先考慮性能時,需要把遞歸算法轉換為循環算法,以減少函數調用次數。
2、周遊算法
目錄是一個樹狀結構,在周遊時一般使用深度優先+先序周遊算法。
深度優先,意味着到達一個節點後,首先接着周遊子節點而不是鄰居節點。
先序周遊,意味着首次到達了某節點就算周遊完成,而不是最後一次傳回某節點才算數。是以使用這種周遊方式時,下邊這棵樹的周遊順序是<code>A > B > D > E > C > F</code>。
3、同步周遊
了解了必要的算法後,我們可以簡單地實作以下目錄周遊函數。
可以看到,該函數以某個目錄作為周遊的起點。遇到一個子目錄時,就先接着周遊子目錄。遇到一個檔案時,就把檔案的絕對路徑傳給回調函數。回調函數拿到檔案路徑後,就可以做各種判斷和處理。是以假設有以下目錄:
4、異步周遊
如果讀取目錄或讀取檔案狀态時使用的是異步API,目錄周遊函數實作起來會有些複雜,但原理完全相同。<code>travel</code>函數的異步版本如下。
四、文本編碼
使用NodeJS編寫前端工具時,操作得最多的是文本檔案,是以也就涉及到了檔案編碼的處理問題。我們常用的文本編碼有<code>UTF8</code>和<code>GBK</code>兩種,并且<code>UTF8</code>檔案還可能帶有BOM。在讀取不同編碼的文本檔案時,需要将檔案内容轉換為JS使用的<code>UTF8</code>編碼字元串後才能正常處理。