Node.js 高級進階之 fs 文件模塊學習

圖片描述

人所缺乏的不是才幹而是志向,不是成功的能力而是勤勞的意志。 —— 部爾衛

文章同步到github博客:https://github.com/koala-codi...

前言

文件操作是開發過程中並不可少的一部分。Node.js 中的 fs 模塊是文件操作的封裝,它提供了文件讀取、寫入、更名、刪除、遍歷目錄、鏈接等 POSIX 文件系統操作。與其它模塊不同的是,fs 模塊中所有的操作都提供了異步和同步的兩個版本,具有 sync 後綴的方法爲同步方法,不具有 sync 後綴的方法爲異步方法

文章概覽

  • 計算機中關於系統和文件的一些常識

    -- 權限位 mode

    -- 標識位 flag

    -- 文件描述符 fs

  • Node.js 中 fs 模塊的 api 詳細講解與對應 Demo

    -- 常規文件操作

    -- 高級文件操作

    -- 文件目錄操縱

  • Node.js 中 fs 模塊的 api 對應 demo
  • fs 模塊的應用場景及實戰訓練(大小文件實現拷貝)

面試會問

說幾個fs模塊的常用函數?什麼情況下使用fs.open的方式讀取文件?用fs模塊寫一個大文件拷貝的例子(注意大文件)?

文件常識

計算機中的一些文件知識,文件的權限位 mode、標識位 flag、文件描述符 fd等你有必要了解下。這些內容對於你接下來學習 fs 的 api ,記憶和使用都會有很多幫助。

權限位 mode

因爲 fs 模塊需要對文件進行操作,會涉及到操作權限的問題,所以需要先清楚文件權限是什麼,都有哪些權限。

文件權限表:

圖片描述

在上面表格中,我們可以看出系統中針對三種類型進行權限分配,即文件所有者(自己)、文件所屬組(家人)和其他用戶(陌生人),文件操作權限又分爲三種,讀、寫和執行,數字表示爲八進制數,具備權限的八進制數分別爲 4 21,不具備權限爲 0。

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

drwxr-xr-x 1 koala 197121 0 Jun 28 14:41 core
-rw-r--r-- 1 koala 197121 293 Jun 23 17:44 index.md

在上面的目錄信息當中,很容易看出用戶名、創建時間和文件名等信息,但最重要的是開頭第一項(十位的字符)。

第一位代表是文件還是文件夾,d 開頭代表文件夾,- 開頭的代表文件,而後面九位就代表當前用戶、用戶所屬組和其他用戶的權限位,按每三位劃分,分別代表讀(r)、寫(w)和執行(x),- 代表沒有當前位對應的權限。

權限參數 mode 主要針對 Linux 和 Unix 操作系統,Window 的權限默認是可讀、可寫、不可執行,所以權限位數字表示爲 0o666,轉換十進制表示爲 438。

圖片描述

標識位 flag

Node.js 中,標識位代表着對文件的操作方式,如可讀、可寫、即可讀又可寫等等,在下面用一張表來表示文件操作的標識位和其對應的含義。

符號 含義
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+ 會自動把已有文件的內容清空。

文件描述符 fs

操作系統會爲每個打開的文件分配一個名爲文件描述符的數值標識,文件操作使用這些文件描述符來識別與追蹤每個特定的文件,Window 系統使用了一個不同但概念類似的機制來追蹤資源,爲方便用戶,NodeJS 抽象了不同操作系統間的差異,爲所有打開的文件分配了數值的文件描述符。

在 Node.js 中,每操作一個文件,文件描述符是遞增的,文件描述符一般從 3 開始,因爲前面有 0、1、2 三個比較特殊的描述符,分別代表 process.stdin(標準輸入)、process.stdout(標準輸出)和 process.stderr(錯誤輸出)。

文件操作

完整性讀寫文件操作

文件讀取-fs.readFile

fs.readFile(filename,[encoding],[callback(error,data)]

文件讀取函數

  1. 它接收第一個必選參數filename,表示讀取的文件名。
  2. 第二個參數 encoding 是可選的,表示文件字符編碼。
  3. 第三個參數callback是回調函數,用於接收文件的內容。

說明:如果不指定 encoding ,則callback就是第二個參數。
回調函數提供兩個參數 err 和 data , err 表示有沒有錯誤發生,data 是文件內容。
如果指定 encoding , data是一個解析後的字符串,否則將會以 Buffer 形式表示的二進制數據。

demo:

const fs = require('fs');
const path = require('path');
const filePath = path.join(__dirname,'koalaFile.txt')
const filePath1 = path.join(__dirname,'koalaFile1.txt')
// -- 異步讀取文件
fs.readFile(filePath,'utf8',function(err,data){
    console.log(data);// 程序員成長指北
});

// -- 同步讀取文件
const fileResult=fs.readFileSync(filePath,'utf8');
console.log(fileResult);// 程序員成長指北

文件寫入fs.writeFile

fs.writeFile(filename,data,[options],callback)

文件寫入操作

  1. 第一個必選參數 filename ,表示讀取的文件名
  2. 第二個參數要寫的數據
  3. 第三個參數 option 是一個對象,如下
encoding {String | null} default='utf-8'
mode {Number} default=438(aka 0666 in Octal)
flag {String} default='w'

這個時候第一章節講的計算機知識就用到了,flag值,默認爲w,會清空文件,然後再寫。flag值,r代表讀取文件,w代表寫文件,a代表追加。

demo:

// 寫入文件內容(如果文件不存在會創建一個文件)
// 寫入時會先清空文件
fs.writeFile(filePath, '寫入成功:程序員成長指北', function(err) {
    if (err) {
        throw err;
    }
    // 寫入成功後讀取測試
    var data=fs.readFileSync(filePath, 'utf-8');
    console.log('new data -->'+data);
});

// 通過文件寫入並且利用flag也可以實現文件追加
fs.writeFile(filePath, '程序員成長指北追加的數據', {'flag':'a'},function(err) {
        if (err) {
            throw err;
        }
        console.log('success');
        var data=fs.readFileSync(filePath, 'utf-8')
        // 寫入成功後讀取測試
        console.log('追加後的數據 -->'+data);
    });

文件追加-appendFile

fs.appendFile(filename, data, [options], callback)
  1. 第一個必選參數 filename ,表示讀取的文件名
  2. 第二個參數 data,data 可以是任意字符串或者緩存
  3. 第三個參數 option 是一個對象,與write的區別就是[options]的flag默認值是”a”,所以它以追加方式寫入數據.

說明:該方法以異步的方式將 data 插入到文件裏,如果文件不存在會自動創建

demo:

// -- 異步另一種文件追加操作(非覆蓋方式)
// 寫入文件內容(如果文件不存在會創建一個文件)
fs.appendFile(filePath, '新數據程序員成長指北456', function(err) {
    if (err) {
        throw err;
    }
    // 寫入成功後讀取測試
    var data=fs.readFileSync(filePath, 'utf-8');
    console.log(data);
});
// -- 同步另一種文件追加操作(非覆蓋方式)

fs.appendFileSync(filePath, '同步追加一條新數據程序員成長指北789');

拷貝文件-copyFile

fs.copyFile(filenameA, filenameB,callback)
  1. 第一個參數原始文件名
  2. 第二個參數要拷貝到的文件名

demo:

// 將filePath文件內容拷貝到filePath1文件內容
fs.copyFileSync(filePath, filePath1);
let data = fs.readFileSync(filePath1, 'utf8');

console.log(data); // 程序員成長指北

刪除文件-unlink

fs.unlink(filename, callback)
  1. 第一個參數文件路徑大家應該都知道了,後面我就不重複了
  2. 第二個回調函數 callback

demo:

// -- 異步文件刪除
fs.unlink(filePath,function(err){
    if(err) return;
});
// -- 同步刪除文件
fs.unlinkSync(filePath,function(err){
    if(err) return;
});

指定位置讀寫文件操作(高級文件操作)

接下來的高級文件操作會與上面有些不同,流程稍微複雜一些,要先用fs.open來打開文件,然後纔可以用fs.read去讀,或者用fs.write去寫文件,最後,你需要用fs.close去關掉文件。

特殊說明:read 方法與 readFile 不同,一般針對於文件太大,無法一次性讀取全部內容到緩存中或文件大小未知的情況,都是多次讀取到 Buffer 中。
想了解 Buffer 可以看 NodeJS —— Buffer 解讀。(注意這裏換成我的文章)

文件打開-fs.open

fs.open(path,flags,[mode],callback)

第一個參數:文件路徑
第二個參數:與開篇說的標識符 flag 相同
第三個參數:[mode] 是文件的權限(可選參數,默認值是0666)
第四個參數:callback 回調函數

demo:

fs.open(filePath,'r','0666',function(err,fd){
   console.log('哈哈哈',fd); //返回的第二個參數爲一個整數,表示打開文件返回的文件描述符,window中又稱文件句柄
})

demo 說明:返回的第二個參數爲一個整數,表示打開文件返回的文件描述符,window中又稱文件句柄,在開篇也有對文件描述符說明。

文件讀取-fs.read

fs.read(fd, buffer, offset, length, position, callback);

六個參數

  1. fd:文件描述符,需要先使用 open 打開,使用fs.open打開成功後返回的文件描述符;
  2. buffer:一個 Buffer 對象,v8引擎分配的一段內存,要將內容讀取到的 Buffer;
  3. offset:整數,向 Buffer 緩存區寫入的初始位置,以字節爲單位;
  4. length:整數,讀取文件的長度;
  5. position:整數,讀取文件初始位置;文件大小以字節爲單位
  6. callback:回調函數,有三個參數 err(錯誤),bytesRead(實際讀取的字節數),buffer(被寫入的緩存區對象),讀取執行完成後執行。

demo:

const fs = require('fs');
let buf = Buffer.alloc(6);// 創建6字節長度的buf緩存對象

// 打開文件
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>
// 你好

文件寫入-fs.write

fs.write(fd, buffer, offset, length, position, callback);

六個參數

  1. fd:文件描述符,使用fs.open 打開成功後返回的;
  2. buffer:一個 Buffer 對象,v8 引擎分配的一段內存,存儲將要寫入文件數據的 Buffer;
  3. offset:整數,從 Buffer 緩存區讀取數據的初始位置,以字節爲單位;
  4. length:整數,讀取 Buffer 數據的字節數;
  5. position:整數,寫入文件初始位置;
  6. callback:寫入操作執行完成後回調函數,有三個參數 err(錯誤),bytesWritten(實際寫入的字節數),buffer(被讀取的緩存區對象),寫入完成後執行。

demo:

文件關閉-fs.close

fs.close(fd,callback)
  1. 第一個參數:fd 文件open時傳遞的文件描述符
  2. 第二個參數 callback 回調函數,回調函數有一個參數 err(錯誤),關閉文件後執行。

demo:

// 注意文件描述符fd
fs.open(filePath, 'r', (err, fd) => {
  fs.close(fd, err => {
    console.log('關閉成功');// 關閉成功
  });
});

目錄(文件夾)操作

1、fs.mkdir 創建目錄

fs.mkdir(path, [options], callback)
  1. 第一個參數:path 目錄路徑
  2. 第二個參數[options],recursive <boolean> 默認值: false。

mode <integer> Windows 上不支持。默認值: 0o777。 可選的 options 參數可以是指定模式(權限和粘滯位)的整數,也可以是具有 mode 屬性和 recursive 屬性(指示是否應創建父文件夾)的對象。

  1. 第三個參數回調函數,回調函數有一個參數 err(錯誤),關閉文件後執行。

demo:

fs.mkdir('./mkdir',function(err){
  if(err) return;
  console.log('創建目錄成功');
})

注意:
在 Windows 上,在根目錄上使用 fs.mkdir() (即使使用遞歸參數)也會導致錯誤:

fs.mkdir('/', { recursive: true }, (err) => {
  // => [Error: EPERM: operation not permitted, mkdir 'C:\']
});

2、fs.rmdir刪除目錄

fs.rmdir(path,callback)
  1. 第一個參數:path目錄路徑
  2. 第三個參數回調函數,回調函數有一個參數 err(錯誤),關閉文件後執行。

demo:

const fs = require('fs');
fs.rmdir('./mkdir',function(err){
  if(err) return;
  console.log('刪除目錄成功');
})
注意:在文件(而不是目錄)上使用 fs.rmdir() 會導致在 Windows 上出現 ENOENT 錯誤、在 POSIX 上出現 ENOTDIR 錯誤。

3、fs.readdir讀取目錄

fs.readdir(path, [options], callback)
  1. 第一個參數:path 目錄路徑
  2. 第二個參數[options]可選的 options 參數可以是指定編碼的字符串,也可以是具有 encoding 屬性的對象,該屬性指定用於傳給回調的文件名的字符編碼。 如果 encoding 設置爲 'buffer',則返回的文件名是 Buffer 對象。

如果 options.withFileTypes 設置爲 true,則 files 數組將包含 fs.Dirent 對象。

  1. 第三個參數回調函數,回調函數有兩個參數,第一個 err(錯誤),第二個返回 的data 爲一個數組,包含該文件夾的所有文件,是目錄中的文件名的數組(不包括 '.''..')。

demo:

const fs = require('fs');
fs.readdir('./file',function(err,data){
  if(err) return;
  //data爲一個數組
  console.log('讀取的數據爲:'+data[0]);
});

實戰訓練:

只講文件相關 Api 顯得很枯燥,下面說一些 fs 在 Node.js 中的具體應用

「示例:fs 模塊如何實現文件拷貝」

文件拷貝例子包括小文件拷貝和大文件拷貝(之前講的 fs 模塊也可以實現文件拷貝)

小文件拷貝

小文件拷貝除了上面 fs 自己提供的 api 我們自己也可以通過讀寫完成一個拷貝例子,如下:

// 文件拷貝 將data.txt文件中的內容拷貝到copyData.txt
// 讀取文件
const fileName1 = path.resolve(__dirname, 'data.txt')
fs.readFile(fileName1, function (err, data) {
    if (err) {
        // 出錯
        console.log(err.message)
        return
    }
    // 得到文件內容
    var dataStr = data.toString()

    // 寫入文件
    const fileName2 = path.resolve(__dirname, 'copyData.txt')
    fs.writeFile(fileName2, dataStr, function (err) {
        if (err) {
            // 出錯
            console.log(err.message)
            return
        }
        console.log('拷貝成功')
    })
})

我們使用 readFile 和 writeFile 實現了一個 copy 函數,那個 copy 函數是將被拷貝文件的數據一次性讀取到內存,一次性寫入到目標文件中,這種針對小文件還好。

大文件拷貝

如果是一個大文件幾百M一次性讀取寫入不現實,所以需要多次讀取多次寫入,接下來使用文件操作的高級方法對大文件和文件大小未知的情況實現一個 copy 函數。當然除了這種方式還有我在之前的文章講過的stream模塊也可以實現,而且性能更好,但是這裏就不再重複說明,本篇主要講fs模塊。

demo:

// 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。

​大家好,我是koala,在做一個一個Node.js高級進階路線,今天就分享這麼多,如果對分享的內容感興趣,可以關注公衆號「程序員成長指北」,或者加入技術交流羣,大家一起討論。

圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章