原文出自:https://www.pandashen.com
本文所有代碼git地址:https://gitee.com/vr2/node/tree/master/fs
fs 概述
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
注意:同步讀取不存在的文件的時候可以使用try catch. try catch只能同步捕獲異常
const fs = require('fs') // 當前目錄下並不存在txt.txt文件 try{ fs.readFileSync('./txt.txt') }catch(e) { console.log(e) } //{ Error: ENOENT: no such file or directory, open './txt.txt' // at Object.openSync ...
(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 }); });
文件操作的高級方法
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("刪除成功"); }); // 刪除成功
總結
在 fs
所有模塊都有同步異步兩種實現,同步方法的特點就是阻塞代碼,導致性能差,異步代碼的特點就是回調函數嵌套多,在使用 fs
應盡量使用異步方式編程來保證性能,如果覺得回調函數嵌套不好維護,可以使用 Promise 和 async/await
的方式解決。