NodeJS 文件操作 —— fs 基本使用


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 、21,不具備權限為 0

為了更容易理解,我們可以隨便在一個目錄中打開 Git,使用 Linux 命令 ls -al 來查目錄中文件和文件夾的權限位

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 開始,因為前面有 012 三個比較特殊的描述符,分別代表 process.stdin(標准輸入)、process.stdout(標准輸出)和 process.stderr(錯誤輸出)。

 

文件操作的基本方法

文件操作中的基本方法都是對文件進行整體操作,即整個文件數據直接放在內存中操作,如讀取、寫入、拷貝和追加,由於計算機的內存容量有限,對文件操作需要考慮性能,所以這些方法只針對操作占用內存較小的文件

1:文件讀取

(1) 同步讀取方法 readFileSync

readFileSync 有兩個參數:

  • 第一個參數為讀取文件的路徑或文件描述符;
  • 第二個參數為 options,默認值為 null,其中有 encoding(編碼,默認為 null)和 flag(標識位,默認為 r),也可直接傳入 encoding
  • 返回值為文件的內容,如果沒有 encoding,返回的文件內容為 Buffer,如果有按照傳入的編碼解析。

若現在有一個文件名為 1.txt,內容為 “Hello”,現在使用 readFileSync 讀取。

// 同步讀取 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 文件:

// 異步讀取 readFile
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 寫入。

// 同步寫入 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(錯誤),回調函數在文件寫入數據成功后執行。

// 異步寫入 writeFile
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”。

// 同步追加 appendFileSync
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(錯誤),回調函數在文件追加寫入數據成功后執行。

// 異步追加 appendFile
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 中:

// 同步拷貝 copyFileSync
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 前兩個參數相同,最后一個參數為回調函數,在拷貝完成后執行。

// 異步拷貝 copyFile
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 中。

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 文件的字節大小,文件內容為 “你好”。

// 同步獲取 Stats 對象
const fs = require("fs");

let statObj = fs.statSync("a/b/c.txt");
console.log(statObj.size); // 6

(2) 異步獲取 Stats 對象方法 stat

stat 方法的第一個參數為目錄的路徑,最后一個參數為回調函數,回調函數有兩個參數 err(錯誤)和 Stats 對象,在讀取 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 等待的異步操作必須轉換成 Promise,以前我們都使用 util 模塊下的 promisify 方法進行轉換,其實 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

// 遞歸刪除文件目錄 —— 異步 async/await
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 的方式解決。

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM