使用 NodeJS 可讀流實現 “行讀取器”

在這裏插入圖片描述

原文出自:https://www.pandashen.com


前言

本文是對於 NodeJS 核心模塊 fs 可讀流 createReadeStream 的應用,實現 “行讀取器”,功能爲讀取一個文檔的內容,每讀完一行觸發一次監聽的事件,並對這一行數據進行處理。

LineReader 類的創建

實現 “行讀取器” 的整體思路是創建一個類的實例,然後在這個實例上監聽一個事件,並開始讀取文件,每次讀完一行觸發,我們這裏將這個類命名爲 LineReader,因爲類需要監聽事件,所以需要繼承 EventEmitter

// 行讀取器 LineReader 類
// 引入依賴
const EventEmitter = require("events");
const fs = require("fs");

// 行讀取器的類,參數爲讀取文件的路徑
class LineReader extends EventEmitter {
    contructor(path) {
        super();
        this.path = path; // 文件路徑
        this._rs = fs.createReadStream(this.path); // 創建可讀流
        this.current = null; // 存儲每次讀到的單個字節
        this.arr = []; // 存放文件每一行單個字節 Buffer 的數組
        this.system = null; // 默認的系統(windows 或 mac)
        this.RETURN = 13; // \r 的十六進制數
        this.Line = 10; // \n 的十六進制數

        // 監聽 newListener
        this.on("newListener", readLineCallback.bind(this));
    }
}

LineReader 實例上定義了 system(當前系統)、current(每次讀取的單個字節)、RETURN\r 十六進制編碼)和 Line\n 十六進制編碼)等屬性方便後面使用。

我們希望在監聽的事件觸發之前,就執行讀取文件一行內容的邏輯,就說明我們需要一個在監聽事件時就能執行的函數,那就需要在創建實例之前先監聽 newListener 事件,把 newListener 的回調來作爲這個函數執行,並能順帶在參數中獲取事件類型。

我們把讀取文件的核心邏輯放在了 newListener 事件的回調函數中,將這個回調函數命名爲 readLineCallback,爲了保證執行時 readLineCallback 內部使用的 thisLineReader 的實例,使用 bind 進行修正。

行讀取器核心邏輯 readLineCall 函數

如果需要默認就開始讀取,並且每次讀取一個字節後還可以進行下一次循環讀取,這種場景最符合可讀流的暫停模式 readable 事件默認觸發一次,“容器” 內讀走了一個字節,就會自動 “續杯” 的特點。

// 行讀取器的核心邏輯
function readLineCallback(type) {
    // 使用暫停模式進行讀取
    this.on("readable", () => {
        if (type === "newLine") {
            // 爲了與 \r 和 \n 對比,每次只讀一個字節
            while ((this.current = this._rs.read(1))) {
                // 結果爲 Buffer,所以使用索引取出對比
                switch (this.current[0]) {
                    case RETURN: // 針對 Windows
                        this.system = "windows";
                        this.disposeLine(); // 處理換行邏輯
                        break;
                    case LINE: // 針對 Mac
                        this.system = "mac";
                        this.disposeLine(); // 處理換行邏輯
                        break;
                    default:
                        // 每讀到換行的字符存入數組中
                        this.arr.push(current);
                }
            }
        }
    });

    // 防止最後一行丟失
    this.on("end", this.disposeLine.bind(this));
}

在上面代碼中監聽了 readable 事件並驗證了事件類型是否爲 newLine,然後循環讀取文件內容,爲了與換行的十六進制碼進行對比,每次只讀取一個字節,當遇到換行符時,明確當前系統並調用換行符處理函數 disposeLine 進行處理。

注意:在最後一次的時候文件最後一行可能沒有換行,所以不滿足 switch 內語句的條件,即沒使用 disposeLine 進行處理,所以監聽可讀流的 end 事件,並在 end 觸發時讓 disposeLine 作爲回調函數執行,注意使用 bind 修正 this 爲當前實例。

兼容 Windows 和 Mac 的換行符處理函數

在換行符處理函數中,Windows 與其他系統(Mac、Linux)系統唯一的區別就是 Window 系統的換行符爲 \r\n,比 Mac 和 Linux 的 \n 多了一個字節,而在讀取下一行時,這個字節是無用的,需要忽略。

// 換行符處理函數
LineReader.prototype.disposeLine = function() {
    // 將這一行的內容發射出來並清空數組
    this.emit("newLine", Buffer.concat(this.arr).toString());
    this.arr = [];

    // 如果是 window 系統,下一個是 \n,就往下多讀一個字節不存入組即可
    if (this.system === "windows") {
        this._rs.read(1);
    }
};

驗證 LineReader 行讀取器

創建一個 “行讀取器” 需要創建 LineReader 類的實例,並傳入被讀取文件的路徑,由於在源碼中執行的是 newListener 的回調函數,所以只需添加 newLine 事件監聽就可以了,然後會在 readable 默認觸發時在內部循環讀取,並把每行讀到的內容重新整合後發送,實現 newLine 事件的連續觸發,直到文件讀完。

// 使用行讀取器
// 創建文件 1.txt 每次內容爲 1~9 9個數字,每 3 個字符爲一行
let lineReader = new LineReader("1.txt");

lineReader.on("newLine", data => {
    console.log(`------ ${data} ------`);
});

// ------ 123 ------
// ------ 456 ------
// ------ 789 ------

“行讀取器” lineReader 對讀取到每一行的數據進行處理的邏輯主要在 newLine 事件的回調函數中,比如上面例子,在每一行的前、後添加了 ------ 並打印。

總結

在 NodeJS 中,流的應用非常廣泛,“行讀取器” 只是其中的一種表現,可以根據流的不同模式的不同特性實現更復雜的功能,所以流在 NodeJS 中還是非常重要的。

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