在 NodeJS 中,全部與文件操做都是經過 fs
核心模塊來實現的,包括文件目錄的建立、刪除、查詢以及文件的讀取和寫入,在 fs
模塊中,全部的方法都分爲同步和異步兩種實現,具備 sync
後綴的方法爲同步方法,不具備 sync
後綴的方法爲異步方法,在瞭解文件操做的方法以前有一些關於系統和文件的前置知識,如文件的權限位 mode
、標識位 flag
、文件描述符 fd
等,因此在瞭解 fs
方法的以前會先將這幾個概念明確。編程
由於 fs
模塊須要對文件進行操做,會涉及到操做權限的問題,因此須要先清楚文件權限是什麼,都有哪些權限。數組
文件權限表:緩存
權限分配 | 文件全部者 | 文件所屬組 | 其餘用戶 | ||||||
---|---|---|---|---|---|---|---|---|---|
權限項 | 讀 | 寫 | 執行 | 讀 | 寫 | 執行 | 讀 | 寫 | 執行 |
字符表示 |
r | w | x | r | w | x | r | w | x |
數字表示 | 4 | 2 | 1 | 4 | 2 | 1 | 4 | 2 | 1 |
在上面表格中,咱們能夠看出系統中針對三種類型進行權限分配,即文件全部者(本身)、文件所屬組(家人)和其餘用戶(陌生人),文件操做權限又分爲三種,讀、寫和執行,數字表示爲八進制數,具有權限的八進制數分別爲 4
、2
、1
,不具有權限爲 0
。bash
爲了更容易理解,咱們能夠隨便在一個目錄中打開 Git
,使用 Linux 命令 ls -al
來查目錄中文件和文件夾的權限位,若是對 Git
和 Linux
命令不熟悉,能夠看 Git 命令總結,從零到熟悉(全)。閉包
drwxr-xr-x 1 PandaShen 197121 0 Jun 28 14:41 coreapp
-rw-r--r-- 1 PandaShen 197121 293 Jun 23 17:44 index.md框架
在上面的目錄信息當中,很容易看出用戶名、建立時間和文件名等信息,但最重要的是開頭第一項(十位的字符)。異步
第一位表明是文件仍是文件夾,d
開頭表明文件夾,-
開頭的表明文件,然後面九位就表明當前用戶、用戶所屬組和其餘用戶的權限位,按每三位劃分,分別表明讀(r)、寫(w)和執行(x),-
表明沒有當前位對應的權限。async
權限參數 mode
主要針對 Linux 和 Unix 操做系統,Window 的權限默認是可讀、可寫、不可執行,因此權限位數字表示爲 0o666
,轉換十進制表示爲 438
。異步編程
r | w | — | r | — | — | r | — | — |
---|---|---|---|---|---|---|---|---|
4 | 2 | 0 | 4 | 0 | 0 | 4 | 0 | 0 |
6 | 4 | 4 |
NodeJS 中,標識位表明着對文件的操做方式,如可讀、可寫、便可讀又可寫等等,在下面用一張表來表示文件操做的標識位和其對應的含義。
符號 | 含義 |
---|---|
r | 讀取文件,若是文件不存在則拋出異常。 |
r+ | 讀取並寫入文件,若是文件不存在則拋出異常。 |
rs | 讀取並寫入文件,指示操做系統繞開本地文件系統緩存。 |
w | 寫入文件,文件不存在會被建立,存在則清空後寫入。 |
wx | 寫入文件,排它方式打開。 |
w+ | 讀取並寫入文件,文件不存在則建立文件,存在則清空後寫入。 |
wx+ | 和 w+ 相似,排他方式打開。 |
a | 追加寫入,文件不存在則建立文件。 |
ax | 與 a 相似,排他方式打開。 |
a+ | 讀取並追加寫入,不存在則建立。 |
ax+ | 與 a+ 相似,排他方式打開。 |
上面表格就是這些標識位的具體字符和含義,可是 flag
是不常用的,不容易被記住,因此在下面總結了一個加速記憶的方法。
r+
和 w+
的區別,當文件不存在時,r+
不會建立文件,而會拋出異常,但 w+
會建立文件;若是文件存在,r+
不會自動清空文件,但 w+
會自動把已有文件的內容清空。
操做系統會爲每一個打開的文件分配一個名爲文件描述符的數值標識,文件操做使用這些文件描述符來識別與追蹤每一個特定的文件,Window 系統使用了一個不一樣但概念相似的機制來追蹤資源,爲方便用戶,NodeJS 抽象了不一樣操做系統間的差別,爲全部打開的文件分配了數值的文件描述符。
在 NodeJS 中,每操做一個文件,文件描述符是遞增的,文件描述符通常從 3
開始,由於前面有 0
、1
、2
三個比較特殊的描述符,分別表明 process.stdin
(標準輸入)、process.stdout
(標準輸出)和 process.stderr
(錯誤輸出)。
文件操做中的基本方法都是對文件進行總體操做,即整個文件數據直接放在內存中操做,如讀取、寫入、拷貝和追加,因爲計算機的內存容量有限,對文件操做須要考慮性能,因此這些方法只針對操做佔用內存較小的文件。
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複製代碼
異步讀取方法 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
});複製代碼
writeFileSync
有三個參數:
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複製代碼
異步寫入方法 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
});
}
});複製代碼
appendFileSync
有三個參數:
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複製代碼
異步追加寫入方法 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
});
}
});複製代碼
同步拷貝寫入方法 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複製代碼
異步拷貝寫入方法 copyFile
和 copyFileSync
前兩個參數相同,最後一個參數爲回調函數,在拷貝完成後執行。
const fs = require("fs");
fs.copyFile("3.txt", "4.txt", () => {
fs.readFile("4.txt", "utf8", (err, data) => {
console.log(data); // Hello world
});
});複製代碼
使用 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
});
});複製代碼
open
方法有四個參數:
0o666
;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複製代碼
close
方法有兩個參數,第一個參數爲關閉文件的文件描述符 fd
,第二參數爲回調函數,回調函數有一個參數 err
(錯誤),關閉文件後執行。
const fs = require("fs");
fs.open("4.txt", "r", (err, fd) => {
fs.close(fd, err => {
console.log("關閉成功");
});
});
// 關閉成功複製代碼
read
方法與 readFile
不一樣,通常針對於文件太大,沒法一次性讀取所有內容到緩存中或文件大小未知的狀況,都是屢次讀取到 Buffer 中。
想了解 Buffer 能夠看 NodeJS —— Buffer 解讀。
read
方法中有六個參數:
open
打開;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>
// 你好複製代碼
fsync
方法有兩個參數,第一個參數爲文件描述符 fd
,第二個參數爲回調函數,回調函數中有一個參數 err
(錯誤),在同步磁盤緩存後執行。
在使用 write
方法向文件寫入數據時,因爲不是一次性寫入,因此最後一次寫入在關閉文件以前應先同步磁盤緩存,fsync
方法將在後面配合 write
一塊兒使用。
write
方法與 writeFile
不一樣,是將 Buffer 中的數據寫入文件,Buffer 的做用是一個數據中轉站,可能數據的源佔用內存太大或內存不肯定,沒法一次性放入內存中寫入,因此分段寫入,多與 read
方法配合。
write
方法中有六個參數:
open
打開;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
的 「你」 字以後,可是最後的 「好」 並無被保留,說明先清空了文件中 「你」 字以後的內容再寫入。
以前咱們使用 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
,也分爲同步和異步兩種實現。
accessSync
方法傳入一個目錄的路徑,檢查傳入路徑下的目錄是否可讀可寫,當有操做權限的時候沒有返回值,沒有權限或路徑非法時拋出一個 Error
對象,因此使用時多用 try...catch...
進行異常捕獲。
const fs = require("fs");
try {
fs.accessSync("a/b/c");
console.log("可讀可寫");
} catch (err) {
console.error("不可訪問");
}複製代碼
access
方法與第一個參數爲一個目錄的路徑,最後一個參數爲一個回調函數,回調函數有一個參數爲 err
(錯誤),在權限檢測後觸發,若是有權限 err
爲 null
,沒有權限或路徑非法 err
是一個 Error
對象。
const fs = require("fs");
fs.access("a/b/c", err => {
if (err) {
console.error("不可訪問");
} else {
console.log("可讀可寫");
}
});複製代碼
文件目錄的 Stats
對象存儲着關於這個文件或文件夾的一些重要信息,如建立時間、最後一次訪問的時間、最後一次修改的時間、文章所佔字節和判斷文件類型的多個方法等等。
statSync
方法參數爲一個目錄的路徑,返回值爲當前目錄路徑的 Stats
對象,如今經過 Stats
對象獲取 a
目錄下的 b
目錄下的 c.txt
文件的字節大小,文件內容爲 「你好」。
const fs = require("fs");
let statObj = fs.statSync("a/b/c.txt");
console.log(statObj.size); // 6複製代碼
stat
方法的第一個參數爲目錄的路徑,最後一個參數爲回調函數,回調函數有兩個參數 err
(錯誤)和 Stats
對象,在讀取 Stats
後執行,一樣實現上面的讀取文件字節數的例子。
const fs = require("fs");
fs.stat("a/b/c.txt", (err, statObj) => {
console.log(statObj.size); // 6
});複製代碼
mkdirSync
方法參數爲一個目錄的路徑,沒有返回值,在建立目錄的過程當中,必須保證傳入的路徑前面的文件目錄都存在,不然會拋出異常。
const fs = require("fs");
// 假設已經有了 a 文件夾和 a 下的 b 文件夾
fs.mkdirSync("a/b/c");複製代碼
mkdir
方法的第一個參數爲目錄的路徑,最後一個參數爲回調函數,回調函數有一個參數 err
(錯誤),在執行建立操做後執行,一樣須要路徑前部分的文件夾都存在。
const fs = require("fs");
// 假設已經有了 a 文件夾和 a 下的 b 文件夾
fs.mkdir("a/b/c", err => {
if (!err) console.log("建立成功");
});
// 建立成功複製代碼
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' ]複製代碼
readdir
方法的前兩個參數與 readdirSync
相同,第三個參數爲一個回調函數,回調函數有兩個參數 err
(錯誤)和 data
(存儲文件目錄中成員名稱的數組),在讀取文件目錄後執行。
上面案例異步的寫法:
const fs = require("fs");
fs.readdir("a/b", (err, data) => {
if (!err) console.log(data);
});
// [ 'c', 'index.js' ]複製代碼
不管同步仍是異步,刪除文件目錄時必須保證文件目錄的路徑存在,且被刪除的文件目錄爲空,即不存在任何文件夾和文件。
rmdirSync
的參數爲要刪除目錄的路徑,如今存在 a
目錄和 a
目錄下的 b
目錄,刪除 b
目錄。
const fs = require("fs");
fs.rmdirSync("a/b");複製代碼
rmdir
方法的第一個參數與 rmdirSync
相同,最後一個參數爲回調函數,函數中存在一個參數 err
(錯誤),在刪除目錄操做後執行。
const fs = require("fs");
fs.rmdir("a/b", err => {
if (!err) console.log("刪除成功");
});
// 刪除成功複製代碼
unlinkSync
的參數爲要刪除文件的路徑,如今存在 a
目錄和 a
目錄下的 index.js
文件,刪除 index.js
文件。
const fs = require("fs");
fs.unlinkSync("a/inde.js");複製代碼
unlink
方法的第一個參數與 unlinkSync
相同,最後一個參數爲回調函數,函數中存在一個參數 err
(錯誤),在刪除文件操做後執行。
const fs = require("fs");
fs.unlink("a/index.js", err => {
if (!err) console.log("刪除成功");
});
// 刪除成功複製代碼
咱們建立一個函數,參數爲一個路徑,按照路徑一級一級的建立文件夾目錄。
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
完成文件夾的建立。
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
說明不存在,則建立文件夾。
上面兩種方式,同步阻塞代碼,性能很差,異步回調函數嵌套性能好,可是維護性差,咱們想要具有性能好,代碼可讀性又好可使用如今 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
的方式解決。