文件拷貝
NodeJS 提供了基本的文件操作 API,但是像文件拷貝這種高級功能就沒有提供,因此我們先拿文件拷貝程式練手。與 copy 指令類似,我們的程式需要能接受來源檔案路徑與目標檔案路徑兩個參數。
小文件拷貝
我們使用 NodeJS 內建的 fs 模組簡單實作這個程式如下。
var fs = require('fs'); function copy(src, dst) { fs.writeFileSync(dst, fs.readFileSync(src)); } function main(argv) { copy(argv[0], argv[1]); } main(process.argv.slice(2));
以上程式使用 fs.readFileSync 從來源路徑讀取檔案內容,並使用 fs.writeFileSync 將檔案內容寫入目標路徑。
豆知識: process 是一個全域變量,可透過 process.argv 取得命令列參數。由於 argv[0] 固定等於 NodeJS 執行程式的絕對路徑,argv[1] 固定等於主模組的絕對路徑,因此第一個命令列參數從 argv[2] 這個位置開始。
大檔案拷貝
上邊的程式拷貝一些小檔案沒啥問題,但這種一次性把所有文件內容都讀取到內存中後再一次性寫入磁碟的方式不適合拷貝大文件,內存會爆倉。對於大文件,我們只能讀一點寫一點,直到完成拷貝。因此上邊的程序需要改造如下。
var fs = require('fs'); function copy(src, dst) { fs.createReadStream(src).pipe(fs.createWriteStream(dst)); } function main(argv) { copy(argv[0], argv[1]); } main(process.argv.slice(2));
以上程式使用 fs.createReadStream 建立了一個原始檔案的唯讀資料流,並使用 fs.createWriteStream 建立了一個目標檔案的只寫資料流,並且用 pipe 方法把兩個資料流連接了起來。連接起來後發生的事情,說得抽象點的話,水順著水管從一個桶子流到了另一個桶子。
遍歷目錄
遍歷目錄是操作檔案時的常見需求。例如寫一個程序,需要找到並處理指定目錄下的所有JS檔案時,就需要遍歷整個目錄。
遞歸演算法
遍歷目錄時一般使用遞歸演算法,否則就難以寫出簡潔的程式碼。遞歸演算法與數學歸納法類似,透過不斷縮小問題的規模來解決問題。以下範例說明了這種方法。
function factorial(n) { if (n === 1) { return 1; } else { return n * factorial(n - 1); } }
上邊的函數用來計算 N 的階乘(N!)。可以看到,當 N 大於 1 時,問題簡化為計算 N 乘以 N-1 的階乘。當 N 等於 1 時,問題達到最小規模,不需要再簡化,因此直接回傳 1。
陷阱: 使用遞歸演算法編寫的程式碼雖然簡潔,但由於每遞歸一次就產生一次函數調用,在需要優先考慮效能時,需要把遞歸演算法轉換為循環演算法,以減少函數調用次數。
遍歷演算法
目錄是一個樹狀結構,在遍歷時一般使用深度優先+先序遍歷演算法。深度優先,意味著到達一個節點後,首先接著遍歷子節點而不是鄰居節點。先序遍歷,意味著首次到達了某節點就算遍歷完成,而不是最後一次返回某節點才算數。因此使用這種遍歷方式時,下邊這棵樹的遍歷順序是 A > B > D > E > C > F。
A / \ B C / \ \ D E F
同步遍歷
了解了必要的演算法後,我們可以簡單地實作以下目錄遍歷函數。
function travel(dir, callback) { fs.readdirSync(dir).forEach(function (file) { var pathname = path.join(dir, file); if (fs.statSync(pathname).isDirectory()) { travel(pathname, callback); } else { callback(pathname); } }); }
可以看到,函數以某個目錄作為遍歷的起點。遇到子目錄時,就先接著遍歷子目錄。遇到一個檔案時,就把檔案的絕對路徑傳給回呼函數。回呼函數拿到檔案路徑後,就可以做各種判斷和處理。因此假設有以下目錄:
- /home/user/ - foo/ x.js - bar/ y.js z.css
使用以下程式碼遍歷該目錄時,所得到的輸入如下。
travel('/home/user', function (pathname) { console.log(pathname); });
/home/user/foo/x.js /home/user/bar/y.js /home/user/z.css
非同步遍歷
如果讀取目錄或讀取檔案狀態時使用的是非同步API,目錄遍歷函數實作起來會有些複雜,但原理完全相同。 travel函數的非同步版本如下。
function travel(dir, callback, finish) { fs.readdir(dir, function (err, files) { (function next(i) { if (i < files.length) { var pathname = path.join(dir, files[i]); fs.stat(pathname, function (err, stats) { if (stats.isDirectory()) { travel(pathname, callback, function () { next(i + 1); }); } else { callback(pathname, function () { next(i + 1); }); } }); } else { finish && finish(); } }(0)); }); }
這裡不詳細介紹非同步遍歷函數的編寫技巧,在後續章節中會詳細介紹這個。總之我們可以看到非同步程式設計還是蠻複雜的。