node.js(二)


fs文件操作模块

所有与文件操作都是通过 fs 核心模块来实现的,包括文件目录的创建、删除、查询以及文件的读取和写入,在 fs 模块中,所有的方法都分为同步和异步两种实现,具有 sync 后缀的方法为同步方法,不具有 sync 后缀的方法为异步方法,在了解文件操作的方法之前有一些关于系统和文件的前置知识,如文件的权限位 mode、标识位 flag、文件描述符 fd

同步读取方法 readFileSync

readFileSync 有两个参数:

  • 第一个参数为读取文件的路径或文件描述符;
  • 第二个参数为 options,默认值为 null,其中有 encoding(编码,默认为 null)和 flag(标识位,默认为 r),也可直接传入 encoding
  • 返回值为文件的内容,如果没有 encoding,返回的文件内容为 Buffer,如果有按照传入的编码解析。
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

异步读取方法 readFilereadFileSync 的前两个参数相同,最后一个参数为回调函数,函数内有两个参数 err(错误)和 data(数据),该方法没有返回值,回调函数在读取文件成功后执行。

const fs = require("fs");

fs.readFile("1.txt", "utf8", (err, data) => {
    console.log(err); // null
    console.log(data); // Hello
});

同步写入方法 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

异步写入方法 writeFile

异步写入方法 writeFilewriteFileSync 的前三个参数相同,最后一个参数为回调函数,函数内有一个参数 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
        });
    }
});

同步追加写入方法 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

异步追加写入方法 appendFile

异步追加写入方法 appendFileappendFileSync 的前三个参数相同,最后一个参数为回调函数,函数内有一个参数 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
        });
    }
});

同步拷贝写入方法 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

异步拷贝写入方法 copyFile

异步拷贝写入方法 copyFilecopyFileSync 前两个参数相同,最后一个参数为回调函数,在拷贝完成后执行。

异步拷贝 copyFile

const fs = require("fs");

fs.copyFile("3.txt", "4.txt", () => {
    fs.readFile("4.txt", "utf8", (err, data) => {
        console.log(data); // Hello world
    });
});

文件操作的高级方法

打开文件 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);
    });
});

关闭文件 close

close 方法有两个参数,第一个参数为关闭文件的文件描述符 fd,第二参数为回调函数,回调函数有一个参数 err(错误),关闭文件后执行。

异步关闭文件

const fs = require("fs");

fs.open("4.txt", "r", (err, fd) => {
    fs.close(fd, err => {
        console.log("关闭成功");
    });
});

读取文件 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>

写入文件 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 的 “你” 字之后,但是最后的 “好” 并没有被保留,说明先清空了文件中 “你” 字之后的内容再写入。

文件目录操作方法

下面的这些操作文件目录的方法有一个共同点,就是传入的第一个参数都为文件的路径,如:a/b/c/d,也分为同步和异步两种实现。

查看文件目录操作权限

同步查看操作权限方法 accessSync

accessSync 方法传入一个目录的路径,检查传入路径下的目录是否可读可写,当有操作权限的时候没有返回值,没有权限或路径非法时抛出一个 Error 对象,所以使用时多用 try...catch... 进行异常捕获。

同步查看操作权限

const fs = require("fs");

try {
    fs.accessSync("a/b/c");
    console.log("可读可写");
} catch (err) {
    console.error("不可访问");
}

异步查看操作权限方法 access

access 方法与第一个参数为一个目录的路径,最后一个参数为一个回调函数,回调函数有一个参数为 err(错误),在权限检测后触发,如果有权限 errnull,没有权限或路径非法 err 是一个 Error 对象。

异步查看操作权限

const fs = require("fs");

fs.access("a/b/c", err => {
    if (err) {
        console.error("不可访问");
    } else {
        console.log("可读可写");
    }
})

还有很多功能,就不一一赘述了

event模块

Events 模块是Node最重要的模块,它提供了一个属性 EventEmitterEventEmitter 的核心是事件发射与事件监听器。

Node中大部分的模块,都继承自 Events 模块。

  • Events 模块是Node对 发布订阅模式publish/subscribe)的实现。一个对象通过这个模块,向另一个对象传递消息。
  • 该模块通过 EventEmitter 属性,提供了一个构造函数。该构造函数的实例具有 on 方法,可以用来监听指定事件,并触发回调函数。
  • 任意对象都可以发布指定事件,被 EventEmitter 实例的on方法监听到。
  • 订阅方法on 方法用来订阅事件,订阅是将方法对应成一种一对多的关系。
  • 发布方法emit 用来执行订阅的事件。
  • 取消订阅off 方法可以移除对应的事件监听。
  • 订阅一次once 绑定事件当执行后自动删除订阅的事件。

on 方法的第一个参数用来设定类名,第二个参数也是一个函数,里面可以接收发布时传入的参数。

emit 方法第一个参数是类名,之后的参数都是传入 on 方法函数中的参数。

代码示例:

function EventEmitter() {
    this._event = {}
}
// on 方法
EventEmitter.prototype.on = function (eventName, callBack) {
    if (!this._event) {
        this._event = {}
    }
    if (this._event[eventName]) {
        this._event[eventName].push(callBack) // 相当于 {eventName:[fn1,fn2]}
    } else {
        this._event[eventName] = [callBack]; // 相当于 {eventName:[fn1]}
    }

}
// emit 方法
EventEmitter.prototype.emit = function (eventName, ...args) {
    this._event[eventName].forEach(fn => {
        fn(...args)
    });
}

off 方法的第一个参数用来设定类名,第二个参数传入需要被移除的函数回调。

// ...
setTimeout(() => {
  	// 小胡子 吃
  	// 小胖仙 睡
    cat.emit('猫咪', '小胖仙', '小胡子')
  	cat.off('猫咪', sleep);
  	// 小胡子 吃
    cat.emit('猫咪', '小胖仙', '小胡子')
}, 1000);

once

once 方法的第一个参数用来设定类名,第二个参数传入只需要执行一次的函数回调。

const demolition =() => {
    console.log('玩');
}
cat.once('玩偶', demolition)
setTimeout(() => {
  	// ...... 拆家
    cat.emit('玩偶', '草莓', '软糖')
}, 1000);

这样我们可以根据之前实现的 onoff 来实现此方法。

// once 方法
EventEmitter.prototype.once = function (eventName, callBack) {
    const one = () => {
        callBack();
        this.off(eventName, one);
    }
    this.on(eventName, one);
}

看起来这个方法好像没有什么问题,执行起来也全都是正确的。

但是在一种特殊情况下的时候,还是出现了错误。

那种情况就是如果我们在执行 once 方法之前,就已经通过 off 方法将其移除了。

我们实现的方法就不能实现这个需求了,所以我们还需要对 once 方法进行一些修改 off 方法已经处理过了)

添加一个自定义属性,用来对函数进行 “缓存” 。

EventEmitter.prototype.once = function (eventName, callBack) {
    const one = () => {
        // ...
    }
    one.c = callBack; // 自定义一个属性
    // ...
}

这样我们就实现了 once 方法。

stream流模块

在Node中,流被分为4类:可读流,可写流,双工流,转换流。

  • Writable: 可以写入数据的流
  • Readable: 可以从中读取数据的流
  • Duplex: ReadableWritable 的流
  • Transform: 可以在写入和读取数据时修改或转换数据的 Duplex

Node.js API 创建的所有流都只对字符串Buffer(或 Uint8Array)对象进行操作。

WritableReadable 流都将数据存储在内部缓冲区(buffer)中。可缓冲的数据量取决于传给流的构造函数的 highWaterMark 选项,对于普通的流,highWaterMark 选项指定字节的总数;对于在对象模式下操作的流,highWaterMark选项指定对象的总数。

highWaterMark 选项是阈值,而不是限制:它规定了流在停止请求更多数据之前缓冲的数据量。

当实现调用 stream.push(chunk) 时,数据缓存在 Readable 流中。 如果流的消费者没有调用 stream.read(),则数据会一直驻留在内部队列中,直到被消费。

一旦内部读取缓冲区的总大小达到 highWaterMark 指定的阈值,则流将暂时停止从底层资源读取数据,直到可以消费当前缓冲的数据

当重复调用 writable.write(chunk) 方法时,数据会缓存在 Writable 流中。

在Node.js中,流也是一个对象,我们只需要响应流的事件就可以了:data事件表示流的数据已经可以读取了,end事件表示这个流已经到末尾了,没有数据可以读取了,error事件表示出错了。

代码示例:

var fs = require('fs');

// 打开一个流:
var rs = fs.createReadStream('sample.txt', 'utf-8');

rs.on('data', function (chunk) {
    console.log('DATA:')
    console.log(chunk);
});

rs.on('end', function () {
    console.log('END');
});

rs.on('error', function (err) {
    console.log('ERROR: ' + err);
});

data事件可能会有多次,每次传递的chunk是流的一部分数据。

要以流的形式写入文件,只需要不断调用write()方法,最后以end()结束:

var fs = require('fs');

var ws1 = fs.createWriteStream('output1.txt', 'utf-8');
ws1.write('使用Stream写入文本数据...\n');
ws1.write('END.');
ws1.end();

pipe 就像可以把两个水管串成一个更长的水管一样,两个流也可以串起来。一个Readable流和一个Writable流串起来后,所有的数据自动从Readable流进入Writable流,这种操作叫pipe

在Node.js中,Readable流有一个pipe()方法,就是用来干这件事的。

让我们用pipe()把一个文件流和另一个文件流串起来,这样源文件的所有数据就自动写入到目标文件里了,所以,这实际上是一个复制文件的程序:

const fs = require('fs')

const readstream = fs.createReadStream('./1.txt')
const writestream = fs.createWriteStream('./2.txt')

readstream.pipe(writestream)

crypto模块

crypto模块的目的是为了提供通用的加密和哈希算法。用纯JavaScript代码实现这些功能不是不可能,但速度会非常慢。Nodejs用C/C++实现这些算法后,通过cypto这个模块暴露为JavaScript接口,这样用起来方便,运行速度也快。

MD5是一种常用的哈希算法,用于给任意数据一个“签名”。这个签名通常用一个十六进制的字符串表示:

const crypto = require('crypto');

const hash = crypto.createHash('md5');

// 可任意多次调用update():
hash.update('Hello, world!');
hash.update('Hello, nodejs!');

console.log(hash.digest('hex')); 

update()方法默认字符串编码为UTF-8,也可以传入Buffer。

如果要计算SHA1,只需要把'md5'改成'sha1',就可以得到SHA1的结果1f32b9c9932c02227819a4151feed43e131aca40

Hmac算法也是一种哈希算法,它可以利用MD5或SHA1等哈希算法。不同的是,Hmac还需要一个密钥:

const crypto = require('crypto');

const hmac = crypto.createHmac('sha256', 'secret-key');

hmac.update('Hello, world!');
hmac.update('Hello, nodejs!');

console.log(hmac.digest('hex')); // 80f7e22570...

只要密钥发生了变化,那么同样的输入数据也会得到不同的签名,因此,可以把Hmac理解为用随机数“增强”的哈希算法。

AES是一种常用的对称加密算法,加解密都用同一个密钥。crypto模块提供了AES支持,但是需要自己封装好函数,便于使用:

const crypto = require("crypto");

function encrypt (key, iv, data) {
    let decipher = crypto.createCipheriv('aes-128-cbc', key, iv);
    // decipher.setAutoPadding(true);
    return decipher.update(data, 'binary', 'hex') + decipher.final('hex');
}

function decrypt (key, iv, crypted) {
     crypted = Buffer.from(crypted, 'hex').toString('binary');
     let decipher = crypto.createDecipheriv('aes-128-cbc', key, iv);
     return decipher.update(crypted, 'binary', 'utf8') + decipher.final('utf8');
}
// key,iv必须是16个字节

可以看出,加密后的字符串通过解密又得到了原始内容。


文章作者: 小小星仔
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 小小星仔 !
评论
  目录