fs 概述
在 NodeJS 中,所有與檔案操作都是通過 fs
核心模組來實現的,包括檔案目錄的建立、刪除、查詢以及檔案的讀取和寫入,在 fs
模組中,所有的方法都分為同步和非同步兩種實現,具有 sync
字尾的方法為同步方法,不具有 sync
字尾的方法為非同步方法,在瞭解檔案操作的方法之前有一些關於系統和檔案的前置知識,如檔案的許可權位 mode
、標識位 flag
、檔案描述符 fd
等,所以在瞭解 fs
方法的之前會先將這幾個概念明確。
許可權位 mode
因為 fs
模組需要對檔案進行操作,會涉及到操作許可權的問題,所以需要先清楚檔案許可權是什麼,都有哪些許可權。
檔案許可權表:
許可權分配 | 檔案所有者 | 檔案所屬組 | 其他使用者 | ||||||
---|---|---|---|---|---|---|---|---|---|
許可權項 | 讀 | 寫 | 執行 | 讀 | 寫 | 執行 | 讀 | 寫 | 執行 |
字元表示 | r | w | x | r | w | x | r | w | x |
數字表示 | 4 | 2 | 1 | 4 | 2 | 1 | 4 | 2 | 1 |
在上面表格中,我們可以看出系統中針對三種型別進行許可權分配,即檔案所有者(自己)、檔案所屬組(家人)和其他使用者(陌生人),檔案操作許可權又分為三種,讀、寫和執行,數字表示為八進位制數,具備許可權的八進位制數分別為 4
、2
、1
,不具備許可權為 0
。
為了更容易理解,我們可以隨便在一個目錄中開啟 Git
,使用 Linux 命令 ls -al
來查目錄中檔案和資料夾的許可權位,如果對 Git
和 Linux
命令不熟悉,可以看 Git 命令總結,從零到熟悉(全)。
drwxr-xr-x 1 PandaShen 197121 0 Jun 28 14:41 core
-rw-r--r-- 1 PandaShen 197121 293 Jun 23 17:44 index.md
在上面的目錄資訊當中,很容易看出使用者名稱、建立時間和檔名等資訊,但最重要的是開頭第一項(十位的字元)。
第一位代表是檔案還是資料夾,d
開頭代表資料夾,-
開頭的代表檔案,而後面九位就代表當前使用者、使用者所屬組和其他使用者的許可權位,按每三位劃分,分別代表讀(r)、寫(w)和執行(x),-
代表沒有當前位對應的許可權。
許可權引數 mode
主要針對 Linux 和 Unix 作業系統,Window 的許可權預設是可讀、可寫、不可執行,所以許可權位數字表示為 0o666
,轉換十進位制表示為 438
。
r | w | — | r | — | — | r | — | — |
---|---|---|---|---|---|---|---|---|
4 | 2 | 0 | 4 | 0 | 0 | 4 | 0 | 0 |
6 | 4 | 4 |
標識位 flag
NodeJS 中,標識位代表著對檔案的操作方式,如可讀、可寫、即可讀又可寫等等,在下面用一張表來表示檔案操作的標識位和其對應的含義。
符號 | 含義 |
---|---|
r | 讀取檔案,如果檔案不存在則丟擲異常。 |
r+ | 讀取並寫入檔案,如果檔案不存在則丟擲異常。 |
rs | 讀取並寫入檔案,指示作業系統繞開本地檔案系統快取。 |
w | 寫入檔案,檔案不存在會被建立,存在則清空後寫入。 |
wx | 寫入檔案,排它方式開啟。 |
w+ | 讀取並寫入檔案,檔案不存在則建立檔案,存在則清空後寫入。 |
wx+ | 和 w+ 類似,排他方式開啟。 |
a | 追加寫入,檔案不存在則建立檔案。 |
ax | 與 a 類似,排他方式開啟。 |
a+ | 讀取並追加寫入,不存在則建立。 |
ax+ | 與 a+ 類似,排他方式開啟。 |
上面表格就是這些標識位的具體字元和含義,但是 flag
是不經常使用的,不容易被記住,所以在下面總結了一個加速記憶的方法。
- r:讀取
- w:寫入
- s:同步
- +:增加相反操作
- x:排他方式
r+
和 w+
的區別,當檔案不存在時,r+
不會建立檔案,而會丟擲異常,但 w+
會建立檔案;如果檔案存在,r+
不會自動清空檔案,但 w+
會自動把已有檔案的內容清空。
檔案描述符 fd
作業系統會為每個開啟的檔案分配一個名為檔案描述符的數值標識,檔案操作使用這些檔案描述符來識別與追蹤每個特定的檔案,Window 系統使用了一個不同但概念類似的機制來追蹤資源,為方便使用者,NodeJS 抽象了不同作業系統間的差異,為所有開啟的檔案分配了數值的檔案描述符。
在 NodeJS 中,每操作一個檔案,檔案描述符是遞增的,檔案描述符一般從 3
開始,因為前面有 0
、1
、2
三個比較特殊的描述符,分別代表 process.stdin
(標準輸入)、process.stdout
(標準輸出)和 process.stderr
(錯誤輸出)。
檔案操作的基本方法
檔案操作中的基本方法都是對檔案進行整體操作,即整個檔案資料直接放在記憶體中操作,如讀取、寫入、拷貝和追加,由於計算機的記憶體容量有限,對檔案操作需要考慮效能,所以這些方法只針對操作佔用記憶體較小的檔案。
1、檔案讀取
(1) 同步讀取方法 readFileSync
readFileSync
有兩個引數:
- 第一個引數為讀取檔案的路徑或檔案描述符;
- 第二個引數為
options
,預設值為null
,其中有encoding
(編碼,預設為null
)和flag
(標識位,預設為r
),也可直接傳入encoding
; - 返回值為檔案的內容,如果沒有
encoding
,返回的檔案內容為 Buffer,如果有按照傳入的編碼解析。
若現在有一個檔名為 1.txt
,內容為 “Hello”,現在使用 readFileSync
讀取。
const fs = require("fs");
let buf = fs.readFileSync("1.txt");
let data = fs.readFileSync("1.txt", "utf8");
console.log(buf); // <Buffer 48 65 6c 6c 6f>
console.log(data); // Hello複製程式碼
(2) 非同步讀取方法 readFile
非同步讀取方法 readFile
與 readFileSync
的前兩個引數相同,最後一個引數為回撥函式,函式內有兩個引數 err
(錯誤)和 data
(資料),該方法沒有返回值,回撥函式在讀取檔案成功後執行。
依然讀取 1.txt
檔案:
const fs = require("fs");
fs.readFile("1.txt", "utf8", (err, data) => {
console.log(err); // null
console.log(data); // Hello
});複製程式碼
2、檔案寫入
(1) 同步寫入方法 writeFileSync
writeFileSync
有三個引數:
- 第一個引數為寫入檔案的路徑或檔案描述符;
- 第二個引數為寫入的資料,型別為 String 或 Buffer;
- 第三個引數為
options
,預設值為null
,其中有encoding
(編碼,預設為utf8
)、flag
(標識位,預設為w
)和mode
(許可權位,預設為0o666
),也可直接傳入encoding
。
若現在有一個檔名為 2.txt
,內容為 “12345”,現在使用 writeFileSync
寫入。
const fs = require("fs");
fs.writeFileSync("2.txt", "Hello world");
let data = fs.readFileSync("2.txt", "utf8");
console.log(data); // Hello world複製程式碼
(2) 非同步寫入方法 writeFile
非同步寫入方法 writeFile
與 writeFileSync
的前三個引數相同,最後一個引數為回撥函式,函式內有一個引數 err
(錯誤),回撥函式在檔案寫入資料成功後執行。
const fs = require("fs");
fs.writeFile("2.txt", "Hello world", err => {
if (!err) {
fs.readFile("2.txt", "utf8", (err, data) => {
console.log(data); // Hello world
});
}
});複製程式碼
3、檔案追加寫入
(1) 同步追加寫入方法 appendFileSync
appendFileSync
有三個引數:
- 第一個引數為寫入檔案的路徑或檔案描述符;
- 第二個引數為寫入的資料,型別為 String 或 Buffer;
- 第三個引數為
options
,預設值為null
,其中有encoding
(編碼,預設為utf8
)、flag
(標識位,預設為a
)和mode
(許可權位,預設為0o666
),也可直接傳入encoding
。
若現在有一個檔名為 3.txt
,內容為 “Hello”,現在使用 appendFileSync
追加寫入 “ world”。
const fs = require("fs");
fs.appendFileSync("3.txt", " world");
let data = fs.readFileSync("3.txt", "utf8");
console.log(data); // Hello world複製程式碼
(2) 非同步追加寫入方法 appendFile
非同步追加寫入方法 appendFile
與 appendFileSync
的前三個引數相同,最後一個引數為回撥函式,函式內有一個引數 err
(錯誤),回撥函式在檔案追加寫入資料成功後執行。
const fs = require("fs");
fs.appendFile("3.txt", " world", err => {
if (!err) {
fs.readFile("3.txt", "utf8", (err, data) => {
console.log(data); // Hello world
});
}
});複製程式碼
4、檔案拷貝寫入
(1) 同步拷貝寫入方法 copyFileSync
同步拷貝寫入方法 copyFileSync
有兩個引數,第一個引數為被拷貝的原始檔路徑,第二個引數為拷貝到的目標檔案路徑,如果目標檔案不存在,則會建立並拷貝。
現在將上面 3.txt
的內容拷貝到 4.txt
中:
const fs = require("fs");
fs.copyFileSync("3.txt", "4.txt");
let data = fs.readFileSync("4.txt", "utf8");
console.log(data); // Hello world複製程式碼
(2) 非同步拷貝寫入方法 copyFile
非同步拷貝寫入方法 copyFile
和 copyFileSync
前兩個引數相同,最後一個引數為回撥函式,在拷貝完成後執行。
const fs = require("fs");
fs.copyFile("3.txt", "4.txt", () => {
fs.readFile("4.txt", "utf8", (err, data) => {
console.log(data); // Hello world
});
});複製程式碼
(3) 模擬同步、非同步拷貝寫入檔案
使用 readFileSync
和 writeFileSync
可以模擬同步拷貝寫入檔案,使用 readFile
和 writeFile
可以模擬非同步寫入拷貝檔案,程式碼如下:
const fs = require("fs");
function copy(src, dest) {
let data = fs.readFileSync(src);
fs.writeFileSync(dest, data);
}
// 拷貝
copy("3.txt", "4.txt");
let data = fs.readFileSync("4.txt", "utf8");
console.log(data); // Hello world複製程式碼
const fs = require("fs");
function copy(src, dest, cb) {
fs.readFile(src, (err, data) => {
// 沒錯誤就正常寫入
if (!err) fs.writeFile(dest, data, cb);
});
}
// 拷貝
copy("3.txt", "4.txt", () => {
fs.readFile("4.txt", "utf8", (err, data) => {
console.log(data); // Hello world
});
});複製程式碼
檔案操作的高階方法
1、開啟檔案 open
open
方法有四個引數:
- path:檔案的路徑;
- flag:標識位;
- mode:許可權位,預設
0o666
; - callback:回撥函式,有兩個引數
err
(錯誤)和fd
(檔案描述符),開啟檔案後執行。
const fs = require("fs");
fs.open("4.txt", "r", (err, fd) => {
console.log(fd);
fs.open("5.txt", "r", (err, fd) => {
console.log(fd);
});
});
// 3
// 4複製程式碼
2、關閉檔案 close
close
方法有兩個引數,第一個引數為關閉檔案的檔案描述符 fd
,第二引數為回撥函式,回撥函式有一個引數 err
(錯誤),關閉檔案後執行。
const fs = require("fs");
fs.open("4.txt", "r", (err, fd) => {
fs.close(fd, err => {
console.log("關閉成功");
});
});
// 關閉成功複製程式碼
3、讀取檔案 read
read
方法與 readFile
不同,一般針對於檔案太大,無法一次性讀取全部內容到快取中或檔案大小未知的情況,都是多次讀取到 Buffer 中。
想了解 Buffer 可以看 NodeJS —— Buffer 解讀。
read
方法中有六個引數:
- fd:檔案描述符,需要先使用
open
開啟; - buffer:要將內容讀取到的 Buffer;
- offset:整數,向 Buffer 寫入的初始位置;
- length:整數,讀取檔案的長度;
- position:整數,讀取檔案初始位置;
- callback:回撥函式,有三個引數
err
(錯誤),bytesRead
(實際讀取的位元組數),buffer
(被寫入的快取區物件),讀取執行完成後執行。
下面讀取一個 6.txt
檔案,內容為 “你好”。
const fs = require("fs");
let buf = Buffer.alloc(6);
// 開啟檔案
fs.open("6.txt", "r", (err, fd) => {
// 讀取檔案
fs.read(fd, buf, 0, 3, 0, (err, bytesRead, buffer) => {
console.log(bytesRead);
console.log(buffer);
// 繼續讀取
fs.read(fd, buf, 3, 3, 3, (err, bytesRead, buffer) => {
console.log(bytesRead);
console.log(buffer);
console.log(buffer.toString());
});
});
});
// 3
// <Buffer e4 bd a0 00 00 00>
// 3
// <Buffer e4 bd a0 e5 a5 bd>
// 你好複製程式碼
4、同步磁碟快取 fsync
fsync
方法有兩個引數,第一個引數為檔案描述符 fd
,第二個引數為回撥函式,回撥函式中有一個引數 err
(錯誤),在同步磁碟快取後執行。
在使用 write
方法向檔案寫入資料時,由於不是一次性寫入,所以最後一次寫入在關閉檔案之前應先同步磁碟快取,fsync
方法將在後面配合 write
一起使用。
5、寫入檔案 write
write
方法與 writeFile
不同,是將 Buffer 中的資料寫入檔案,Buffer 的作用是一個資料中轉站,可能資料的源佔用記憶體太大或記憶體不確定,無法一次性放入記憶體中寫入,所以分段寫入,多與 read
方法配合。
write
方法中有六個引數:
- fd:檔案描述符,需要先使用
open
開啟; - buffer:儲存將要寫入檔案資料的 Buffer;
- offset:整數,從 Buffer 讀取資料的初始位置;
- length:整數,讀取 Buffer 資料的位元組數;
- position:整數,寫入檔案初始位置;
- callback:回撥函式,有三個引數
err
(錯誤),bytesWritten
(實際寫入的位元組數),buffer
(被讀取的快取區物件),寫入完成後執行。
下面將一個 Buffer 中間的兩個字寫入檔案 6.txt
,原內容為 “你好”。
const fs = require("fs");
let buf = Buffer.from("你還好嗎");
// 開啟檔案
fs.open("6.txt", "r+", (err, fd) => {
// 讀取 buf 向檔案寫入資料
fs.write(fd, buf, 3, 6, 3, (err, bytesWritten, buffer) => {
// 同步磁碟快取
fs.fsync(fd, err => {
// 關閉檔案
fs.close(fd, err => {
console.log("關閉檔案");
});
});
});
});
// 這裡為了看是否寫入成功簡單粗暴的使用 readFile 方法
fs.readFile("6.txt", "utf8", (err, data) => {
console.log(data);
});
// 你還好複製程式碼
上面程式碼將 “你還好嗎” 中間的 “還好” 從 Buffer 中讀取出來寫入到 6.txt
的 “你” 字之後,但是最後的 “好” 並沒有被保留,說明先清空了檔案中 “你” 字之後的內容再寫入。
6、針對大檔案實現 copy
之前我們使用 readFile
和 writeFile
實現了一個 copy
函式,那個 copy
函式是將被拷貝檔案的資料一次性讀取到記憶體,一次性寫入到目標檔案中,針對小檔案。
如果是一個大檔案一次性寫入不現實,所以需要多次讀取多次寫入,接下來使用上面的這些方法針對大檔案和檔案大小未知的情況實現一個 copy
函式。
// copy 方法
function copy(src, dest, size = 16 * 1024, callback) {
// 開啟原始檔
fs.open(src, "r", (err, readFd) => {
// 開啟目標檔案
fs.open(dest, "w", (err, writeFd) => {
let buf = Buffer.alloc(size);
let readed = 0; // 下次讀取檔案的位置
let writed = 0; // 下次寫入檔案的位置
(function next() {
// 讀取
fs.read(readFd, buf, 0, size, readed, (err, bytesRead) => {
readed += bytesRead;
// 如果都不到內容關閉檔案
if(!bytesRead) fs.close(readFd, err => console.log("關閉原始檔"));
// 寫入
fs.write(writeFd, buf, 0, bytesRead, writed, (err, bytesWritten) => {
// 如果沒有內容了同步快取,並關閉檔案後執行回撥
if (!bytesWritten) {
fs.fsync(writeFd, err => {
fs.close(writeFd, err => return !err && callback());
});
}
writed += bytesWritten;
// 繼續讀取、寫入
next();
}
);
});
})();
});
});
}複製程式碼
在上面的 copy
方法中,我們手動維護的下次讀取位置和下次寫入位置,如果引數 readed
和 writed
的位置傳入 null
,NodeJS 會自動幫我們維護這兩個值。
現在有一個檔案 6.txt
內容為 “你好”,一個空檔案 7.txt
,我們將 6.txt
的內容寫入 7.txt
中。
const fs = require("fs");
// buffer 的長度
const BUFFER_SIZE = 3;
// 拷貝檔案內容並寫入
copy("6.txt", "7.txt", BUFFER_SIZE, () => {
fs.readFile("7.txt", "utf8", (err, data) => {
// 拷貝完讀取 7.txt 的內容
console.log(data); // 你好
});
});複製程式碼
在 NodeJS 中進行檔案操作,多次讀取和寫入時,一般一次讀取資料大小為 64k
,寫入資料大小為 16k
。
檔案目錄操作方法
下面的這些操作檔案目錄的方法有一個共同點,就是傳入的第一個引數都為檔案的路徑,如:a/b/c/d
,也分為同步和非同步兩種實現。
1、檢視檔案目錄操作許可權
(1) 同步檢視操作許可權方法 accessSync
accessSync
方法傳入一個目錄的路徑,檢查傳入路徑下的目錄是否可讀可寫,當有操作許可權的時候沒有返回值,沒有許可權或路徑非法時丟擲一個 Error
物件,所以使用時多用 try...catch...
進行異常捕獲。
const fs = require("fs");
try {
fs.accessSync("a/b/c");
console.log("可讀可寫");
} catch (err) {
console.error("不可訪問");
}複製程式碼
(2) 非同步檢視操作許可權方法 access
access
方法與第一個引數為一個目錄的路徑,最後一個引數為一個回撥函式,回撥函式有一個引數為 err
(錯誤),在許可權檢測後觸發,如果有許可權 err
為 null
,沒有許可權或路徑非法 err
是一個 Error
物件。
const fs = require("fs");
fs.access("a/b/c", err => {
if (err) {
console.error("不可訪問");
} else {
console.log("可讀可寫");
}
});複製程式碼
2、獲取檔案目錄的 Stats 物件
檔案目錄的 Stats
物件儲存著關於這個檔案或資料夾的一些重要資訊,如建立時間、最後一次訪問的時間、最後一次修改的時間、文章所佔位元組和判斷檔案型別的多個方法等等。
(1) 同步獲取 Stats 物件方法 statSync
statSync
方法引數為一個目錄的路徑,返回值為當前目錄路徑的 Stats
物件,現在通過 Stats
物件獲取 a
目錄下的 b
目錄下的 c.txt
檔案的位元組大小,檔案內容為 “你好”。
const fs = require("fs");
let statObj = fs.statSync("a/b/c.txt");
console.log(statObj.size); // 6複製程式碼
(2) 非同步獲取 Stats 物件方法 stat
stat
方法的第一個引數為目錄的路徑,最後一個引數為回撥函式,回撥函式有兩個引數 err
(錯誤)和 Stats
物件,在讀取 Stats
後執行,同樣實現上面的讀取檔案位元組數的例子。
const fs = require("fs");
fs.stat("a/b/c.txt", (err, statObj) => {
console.log(statObj.size); // 6
});複製程式碼
3、建立檔案目錄
(1) 同步建立目錄方法 mkdirSync
mkdirSync
方法引數為一個目錄的路徑,沒有返回值,在建立目錄的過程中,必須保證傳入的路徑前面的檔案目錄都存在,否則會丟擲異常。
const fs = require("fs");
// 假設已經有了 a 資料夾和 a 下的 b 資料夾
fs.mkdirSync("a/b/c");複製程式碼
(2) 非同步建立目錄方法 mkdir
mkdir
方法的第一個引數為目錄的路徑,最後一個引數為回撥函式,回撥函式有一個引數 err
(錯誤),在執行建立操作後執行,同樣需要路徑前部分的資料夾都存在。
const fs = require("fs");
// 假設已經有了 a 資料夾和 a 下的 b 資料夾
fs.mkdir("a/b/c", err => {
if (!err) console.log("建立成功");
});
// 建立成功複製程式碼
4、讀取檔案目錄
(1) 同步讀取目錄方法 readdirSync
readdirSync
方法有兩個引數:
- 第一個引數為目錄的路徑,傳入的路徑前部分的目錄必須存在,否則會報錯;
- 第二個引數為
options
,其中有encoding
(編碼,預設值為utf8
),也可直接傳入encoding
; - 返回值為一個儲存檔案目錄中成員名稱的陣列。
假設現在已經存在了 a
目錄和 a
下的 b
目錄,b
目錄中有 c
目錄和 index.js
檔案,下面讀取檔案目錄結構。
const fs = require("fs");
let data = fs.readdirSync("a/b");
console.log(data); // [ 'c', 'index.js' ]複製程式碼
(2) 非同步讀取目錄方法 readdir
readdir
方法的前兩個引數與 readdirSync
相同,第三個引數為一個回撥函式,回撥函式有兩個引數 err
(錯誤)和 data
(儲存檔案目錄中成員名稱的陣列),在讀取檔案目錄後執行。
上面案例非同步的寫法:
const fs = require("fs");
fs.readdir("a/b", (err, data) => {
if (!err) console.log(data);
});
// [ 'c', 'index.js' ]複製程式碼
5、刪除檔案目錄
無論同步還是非同步,刪除檔案目錄時必須保證檔案目錄的路徑存在,且被刪除的檔案目錄為空,即不存在任何資料夾和檔案。
(1) 同步刪除目錄方法 rmdirSync
rmdirSync
的引數為要刪除目錄的路徑,現在存在 a
目錄和 a
目錄下的 b
目錄,刪除 b
目錄。
const fs = require("fs");
fs.rmdirSync("a/b");複製程式碼
(2) 非同步刪除目錄方法 rmdir
rmdir
方法的第一個引數與 rmdirSync
相同,最後一個引數為回撥函式,函式中存在一個引數 err
(錯誤),在刪除目錄操作後執行。
const fs = require("fs");
fs.rmdir("a/b", err => {
if (!err) console.log("刪除成功");
});
// 刪除成功複製程式碼
6、刪除檔案操作
(1) 同步刪除檔案方法 unlinkSync
unlinkSync
的引數為要刪除檔案的路徑,現在存在 a
目錄和 a
目錄下的 index.js
檔案,刪除 index.js
檔案。
const fs = require("fs");
fs.unlinkSync("a/inde.js");複製程式碼
(2) 非同步刪除檔案方法 unlink
unlink
方法的第一個引數與 unlinkSync
相同,最後一個引數為回撥函式,函式中存在一個引數 err
(錯誤),在刪除檔案操作後執行。
const fs = require("fs");
fs.unlink("a/index.js", err => {
if (!err) console.log("刪除成功");
});
// 刪除成功複製程式碼
實現遞迴建立目錄
我們建立一個函式,引數為一個路徑,按照路徑一級一級的建立資料夾目錄。
1、同步的實現
const fs = require("fs");
const path = require("path");
// 同步建立檔案目錄
function mkPathSync(dirPath) {
// path.sep 檔案路徑分隔符(mac 與 window 不同)
// 轉變成陣列,如 ['a', 'b', 'c']
let parts = dirPath.split(path.sep);
for(let i = 1; i <= parts.length; i++) {
// 重新拼接成 a a/b a/b/c
let current = parts.slice(0, i).join(path.sep);
// accessSync 路徑不存在則丟擲錯誤在 catch 中建立資料夾
try {
fs.accessSync(current);
} catch(e) {
fs.mkdirSync(current);
}
}
}
// 建立檔案目錄
mkPathSync(path.join("a", "b", "c"));複製程式碼
同步程式碼就是利用 accessSync
方法檢查檔案路徑是否存在,利用 try...catch...
進行錯誤捕獲,如果路徑不存在,則會報錯,會進入 catch
完成資料夾的建立。
2、非同步回撥的實現
const fs = require("fs");
const path = require("path");
function mkPathAsync(dirPath, callback) {
// 轉變成陣列,如 ['a', 'b', 'c']
let parts = dirPath.split(path.sep);
let index = 1;
// 建立資料夾方法
function next() {
// 重新拼接成 a a/b a/b/c
let current = parts.slice(0, index).join(path.sep);
index++;
// 如果路徑檢查成功說明已經有該檔案目錄,則繼續建立下一級
// 失敗則建立目錄,成功後遞迴 next 建立下一級
fs.access(current, err => {
if (err) {
fs.mkdir(current, next);
} else {
next();
}
});
}
next();
}
// 建立檔案目錄
mkPathAsync(path.join("a", "b", "c"), () => {
console.log("建立檔案目錄完成")
});
// 建立檔案目錄完成複製程式碼
上面方法中沒有通過迴圈實現每次目錄的拼接,而是通過遞迴內部函式 next
的方式並維護 index
變數來實現的,在使用 access
的時候成功說明檔案目錄已經存在,就繼續遞迴建立下一級,如果存在 err
說明不存在,則建立資料夾。
3、非同步 async/await 的實現
上面兩種方式,同步阻塞程式碼,效能不好,非同步回撥函式巢狀效能好,但是維護性差,我們想要具備效能好,程式碼可讀性又好可以使用現在 NodeJS 中正流行的 async/await
的方式進行非同步程式設計,想了解 async/await
可以看 非同步發展流程 —— 非同步程式設計的終極大招 async/await 這篇文章。
使用 async
函式中 await
等待的非同步操作必須轉換成 Promise,以前我們都使用 util
模組下的 promisify
方法進行轉換,其實 promisify
方法的原理很簡單,我們在實現遞迴建立檔案目錄之前先實現 promisify
方法。
// 將一個非同步方法轉換成 Promise
function promisify(fn) {
return function (...args) {
return new Promise((resolve, reject) => {
fn.call(null, ...args, err => err ? reject() : resolve());
});
}
}複製程式碼
其實 promisify
方法就是利用閉包來實現的,呼叫時傳入一個需要轉換成 Promise 的函式 fn
,返回一個閉包函式,在閉包函式中返回一個 Promise 例項,並同步執行了 fn
,通過 call
將閉包函式中的引數和回撥函式作為引數傳入了 fn
中,該回撥在存在錯誤的時候呼叫了 Promise 例項的 reject
,否則呼叫 resolve
;
const fs = require("fs");
const path = require("path");
// 將 fs 中用到的方法轉換成 Promise
const access = promisify(fs.access);
const mkdir = promisify(fs.mkdir);
// async/await 實現遞迴建立檔案目錄
async function mkPath(dirPath) {
// 轉變成陣列,如 ['a', 'b', 'c']
let parts = dirPath.split(path.sep);
for(let i = 1; i <= parts.length; i++) {
// 重新拼接成 a a/b a/b/c
let current = parts.slice(0, i).join(path.sep);
// accessSync 路徑不存在則丟擲錯誤在 catch 中建立資料夾
try {
await access(current);
} catch(e) {
await mkdir(current);
}
}
}
// 建立檔案目錄
mkPath(path.("a", "b", "c")).then(() => {
console.log("建立檔案目錄完成");
});
// 建立檔案目錄完成複製程式碼
使用 async/await
的寫法,程式碼更像同步的實現方式,卻是非同步執行,所以同時兼顧了效能和程式碼的可讀性,優勢顯而易見,在使用 NodeJS 框架 Koa 2.x
版本時大量使用這種方式進行非同步程式設計。
總結
在 fs
所有模組都有同步非同步兩種實現,同步方法的特點就是阻塞程式碼,導致效能差,非同步程式碼的特點就是回撥函式巢狀多,在使用 fs
應儘量使用非同步方式程式設計來保證效能,如果覺得回撥函式巢狀不好維護,可以使用 Promise 和 async/await
的方式解決。