第七节:fs模块-文件系统、event模块-事件处理、二进制和buffer、Stream流
一. fs文件系统
1. 文件的读取
nodejs中API大多数都提供三种操作方式:
方式一:同步操作文件:代码会被阻塞,不会继续执行;【eg:readFileSync】
方式二:异步回调函数操作文件:代码不会被阻塞,需要传入回调函数,当获取到结果时,回调函数被执行;【eg:readFile】
方式三:异步Promise操作文件:代码不会被阻塞,通过 fs.promises 调用方法操作,会返回一个Promise,可以通过then、catch进行处理;
A. 可以 fs.promises.readFile.then.catch
B. 使用promise+async/await
(1). 同步读取
let data1 = fs.readFileSync("./test1.txt", { encoding: "utf8" });
console.log("读取的结果为:" + data1);
console.log("后续业务代码");
const fs = require("fs");
fs.readFile("./test1.txt", { encoding: "utf8" }, (error, data) => {
if (error) {
console.log("出错了");
return;
}
console.log("读取的结果为:" + data);
});
console.log("后续业务代码");
fs.promises
.readFile("./test1.txt", { encoding: "utf8" })
.then(res => {
console.log("读取的结果为:" + res);
})
.catch(error => {
console.log("出错了:" + error);
});
const { readFile } = require("node:fs/promises");
async function logFile() {
try {
const contents = await readFile("./test1.txt", { encoding: "utf8" });
console.log(contents);
} catch (err) {
console.error(err.message);
}
}
//调用
logFile();
2. 文件的写入
(1). 说明
写入的方式有很多种
flag的值有很多:https://nodejs.org/dist/latest-v14.x/docs/api/fs.html#fs_file_system_flags
w 打开文件写入,默认值;【默认是覆盖】
w+ 打开文件进行读写(可读可写),如果不存在则创建文件;
r 打开文件读取,读取时的默认值;
r+ 打开文件进行读写,如果不存在那么抛出异常;
a 打开要写入的文件,将流放在文件末尾。如果不存在则创建文件;【追加】
a+ 打开文件以进行读写(可读可写),将流放在文件末尾。如果不存在则创建文件
const fs = require("fs");
let content = `123456789`;
fs.writeFile(
"./test2.txt",
content,
{ encoding: "utf-8", flag: "w" },
err => {
if (err) {
console.log("写入失败:" + err);
} else {
console.log("文件写入成功");
}
}
);
const fs = require("fs");
let content = `123456789`;
fs.writeFile(
"./test2.txt",
content,
{ encoding: "utf-8", flag: "a" },
err => {
if (err) {
console.log("写入失败:" + err);
} else {
console.log("文件写入成功");
}
}
);
3. 文件描述符
(1). 文件描述符(File descriptors)是什么呢?
在常见的操作系统上,对于每个进程,内核都维护着一张当前打开着的文件和资源的表格。
每个打开的文件都分配了一个称为文件描述符的简单的数字标识符。
在系统层,所有文件系统操作都使用这些文件描述符来标识和跟踪每个特定的文件。
Windows 系统使用了一个虽然不同但概念上类似的机制来跟踪资源。
(2). 为了简化用户的工作,Node.js 抽象出操作系统之间的特定差异,并为所有打开的文件分配一个数字型的文件描述符。
(3). fs.open() 方法用于分配新的文件描述符。一旦被分配,则文件描述符可用于从文件读取数据、向文件写入数据、或请求关于文件的信息。
let fs = require("fs");
fs.open("./test2.txt", (err, fd) => {
if (err) {
console.log("出错了:" + err);
return;
}
// 1. 获取文件描述符
console.log(fd);
// 2. 读取文件信息
fs.fstat(fd, (error, stats) => {
if (error) return;
console.log(stats);
// 3. 手动关闭文件
fs.close(fd);
});
});
4. 文件夹操作
(1). 创建文件夹
使用异步fs.mkdir()或同步fs.mkdirSync()创建一个新文件夹。
const fs = require("fs");
fs.mkdir("./YpfDocument", error => {
if (error) {
console.log(error);
return;
}
console.log("创建成功");
});
(2). 读取文件夹
异步readdir()方法
// 2.1 只读取一级
{
fs.readdir("./YpfDocument", (error, files) => {
console.log(files);
});
// 输出的内容中包含文件类型
fs.readdir("./YpfDocument", { withFileTypes: true }, (error, files) => {
console.log(files);
});
}
// 2.2 写个递归
{
/**
* 递归读取所有文件
* @param {string} path 文件路径
*/
const ReadDirectory = path => {
fs.readdir(path, { withFileTypes: true }, (error, files) => {
files.forEach(item => {
if (item.isDirectory()) {
// 表示是文件夹
console.log("文件夹名称:" + item.name);
ReadDirectory(`${path}/${item.name}`);
} else {
console.log("文件名称:" + item.name);
}
});
});
};
// 调用
ReadDirectory("./YpfDocument");
}
(3). 重命名文件夹 和文件
{
// 1.对文件夹进行重命名(ypf1改为ypf2)
fs.rename("./YpfDocument/ypf1", "./YpfDocument/ypf2", err => {
console.log("重命名结果:", err);
});
// 2.对文件重命名(c1改为d1)
fs.rename("./c1.txt", "./d1.txt", err => {
console.log("重命名结果:", err);
});
}
二. event事件处理
1. 事件发送和监听
发出事件和监听事件都是通过EventEmitter类来完成的,它们都属于events对象。
A. emitter.on(eventName, listener):监听事件,也可以使用addListener;
B. emitter.off(eventName, listener):移除事件监听,也可以使用removeListener;
注:事件取消必须传递一个方法名
C. emitter.emit(eventName[, ...args]):发出事件,可以携带一些参数
注:监听事件的注册要在发送事件之前
(1). 发出和监听
{
//发出
setInterval(() => {
emitter.emit("key1");
}, 2000);
// 监听
emitter.on("key1", () => {
console.log("我是key1的监听事件");
});
}
(2). 事件取消
{
//发出
setInterval(() => {
emitter.emit("key1");
}, 2000);
// 监听
function test1() {
console.log("我是key1的监听事件");
}
emitter.on("key1", test1);
// 取消
setTimeout(() => {
emitter.off("key1", test1);
}, 10000);
}
(3). 参数传递
{
//发送
setTimeout(() => {
emitter.emit("key1", "ypf", 18);
}, 2000);
// 监听
emitter.on("key1", (myName, age) => {
console.log(`我是key1的监听事件:${myName},${age}`);
});
}
2. 其它方法
(一). EventEmitter的实例有一些属性,可以记录一些信息:
(1). emitter.eventNames():返回当前 EventEmitter对象注册的事件字符串数组;
(2). emitter.getMaxListeners():返回当前 EventEmitter对象的最大监听器数量,可以通过setMaxListeners()来修改,默认是10;
(3). emitter.listenerCount(事件名称):返回当前 EventEmitter对象某一个事件名称,监听器的个数;
(4). emitter.listeners(事件名称):返回当前 EventEmitter对象某个事件监听器上所有的监听器数组;
(二). EventEmitter的实例方法补充:
(1). emitter.once(eventName, listener):事件监听一次
(2). emitter.prependListener():将监听事件添加到最前面
(3). emitter.prependOnceListener():将监听事件添加到最前面,但是只监听一次
(4). emitter.removeAllListeners([eventName]):移除所有的监听器
A. 不传递参数的情况下, 移除所有事件名称的所有事件监听
B. 在传递参数的情况下, 只会移除传递的事件名称的事件监听
const EventEmitter = require("events"); //事件总线
const emitter = new EventEmitter(); //实例对象
// 1. 实例属性
{
emitter.on("ypf1", () => {});
emitter.on("ypf1", () => {});
emitter.on("ypf1", () => {});
emitter.on("ypf2", () => {});
emitter.on("ypf2", () => {});
// 1.获取所有监听事件的名称
console.log(emitter.eventNames());
// 2.获取监听最大的监听个数
console.log(emitter.getMaxListeners());
// 3.获取某一个事件名称对应的监听器个数
console.log(emitter.listenerCount("ypf1"));
// 4.获取某一个事件名称对应的监听器函数(数组)
console.log(emitter.listeners("ypf2"));
}
三. buffer和二进制
1. 二进制
(1). Node为了可以方便开发者完成更多功能,提供给了我们一个类Buffer,并且它是全局的。
(2). Buffer中存储的是二进制数据,那么到底是如何存储呢?
A. 我们可以将Buffer看成是一个存储二进制的数组;
B. 这个数组中的每一项,可以保存8位二进制: 0000 0000
(3). 字节(byte)
通常将 8位(bit)合并在一起作为一个单元, 这个单元称之为1个字节(byte)
1byte=8bit (即: 1个字节 等于 8比特, 也叫8位)
1kb=1024byte
(4). 其它
比如很多编程语言中的int类型是4个字节,long类型时8个字节;
比如TCP传输的是字节流,在写入和读取时都需要说明字节的个数;
比如RGB的值分别都是255,所以本质上在计算机中都是用一个字节存储的;
2. buffer
(1). buffer的创建
A. new Buffer('xxx') 不推荐
B. Buffer.from('xxx')
注:如果是中文,默认是utf8编码
{
// 方式1 不推荐使用了
let bf1 = new Buffer("ypf1");
console.log(bf1);
// 方式2
let bf2 = Buffer.from("ypf2");
console.log(bf2);
// 字符串中包含中文
let bf3 = Buffer.from("ypf1 你好");
console.log(bf3);
console.log(bf3.toString());
}
(2). Buffer.alloc申请内存空间
{
// 8个字节大小的buffer内存空间
const buf = Buffer.alloc(8);
// console.log(buf)
// 2.手动对每个字节进行访问
// console.log(buf[0])
// console.log(buf[1])
// 3.手动对每个字节进行操作
buf[0] = 100;
buf[1] = 0x66;
console.log(buf);
console.log(buf.toString());
buf[2] = "m".charCodeAt();
console.log(buf);
}
(3). 从文件中读取buffer
A. 文件的读取
B. 图片的读取
{
// 读文件
fs.readFile("./test1.txt", (err, data) => {
console.log(data.toString());
});
// 读图片(node中有一个库sharp)
fs.readFile("./test2.jpg", (err, data) => {
console.log(data);
});
}
四. Stream流
1. 什么是流?
是连续字节的一种表现形式和抽象概念; 流应该是可读的,也是可写的;
注:Node中很多对象是基于流实现的: http模块的Request和Response对象;另外所有的流都是EventEmitter的实例
2. 既然可以直接通过 readFile或者 writeFile方式读写文件,为什么还需要流呢?
(1). 直接读写文件的方式,虽然简单,但是无法控制一些细节的操作;
(2). 比如从什么位置开始读、读到什么位置、一次性读取多少个字节;
(3). 读到某个位置后,暂停读取,某个时刻恢复继续读取等等;
(4). 或者这个文件非常大,比如一个视频文件,一次性全部读取并不合适;
3. Node.js中有四种基本流类型:
(1). Readable:可以从中读取数据的流(例如 fs.createReadStream())。
(2). Writable:可以向其写入数据的流(例如 fs.createWriteStream())。
(3). Duplex:同时为Readable和Writable(例如 net.Socket)。
(4). Transform:Duplex可以在写入和读取数据时修改或转换数据的流(例如zlib.createDeflate())
4. Readable可读流
(1). 使用方法 createReadStream,可以配置参数:
start:文件读取开始的位置;
end:文件读取结束的位置;
highWaterMark:一次性读取字节的长度,默认是64kb;
然后通过过监听data事件,获取读取到的数据 on("data")
(2). 其它方法
pause:暂停 resume:恢复
(3). 常用的监听
data: 监听获取数据
open: 监听流将文件打开
end:监听读取到结束位置
close:监听文件读取结束,且关闭
const fs = require("fs");
/*
1. 通过流读取文件
参数如下:
start:文件读取开始的位置;
end:文件读取结束的位置;
highWaterMark:一次性读取字节的长度,默认是64kb
*/
{
const readStream = fs.createReadStream("./test1.txt", {
start: 2,
end: 8,
highWaterMark: 2, //一次两个字符
});
// 监听读取结果
readStream.on("data", result => {
console.log(result.toString());
readStream.pause(); //暂停
setTimeout(() => readStream.resume(), 2000); //重置
});
// 其它监听
readStream.on("open", fd => {
console.log("通过流将文件打开~", fd);
});
readStream.on("end", () => {
console.log("已经读取到end位置");
});
readStream.on("close", () => {
console.log("文件读取结束, 并且被关闭");
});
}
5. Writeable可读流
(1). 用法
A. 使用方法 createWriteStream 创建流, 参数有:
flags:默认是w,覆盖写入,如果我们希望是追加写入,可以使用 a或者 a+;
start:写入的位置
B. 使用 write方法写入内容,使用close方法关闭
C. end方法相当于做了两步操作: write传入的数据和调用close方法
{
// 创建写入流
const writeStream = fs.createWriteStream("./test2.txt", { flags: "a" });
// 开始写入
writeStream.write("ypf1");
writeStream.write("ypf2");
writeStream.write("ypf3");
// 写入完成,需要手动关闭
writeStream.close();
// 事件监听
writeStream.on("finish", () => {
console.log("写入完成了");
});
writeStream.on("close", () => {
console.log("文件被关闭~");
});
// end 方法测试
// writeStream.end("hhhhh");
}
(2). 监听事件
A. finish: 监听事件完成
B. close:监听事件关闭
(3). start属性剖析
mac上使用 flags: 'a+', windows上使用 flags: 'r+'
{
// 创建写入流
const writeStream = fs.createWriteStream("./test2.txt", {
flags: "r+",
start: 2,
});
// 开始写入
writeStream.write("----------------");
// 写入完成,需要手动关闭
writeStream.close();
}
6. 拷贝流
如何读取一个文件中的内容,然后写到另外一个文件中呢?
方案1 一次性的读取和写入文件
{
fs.readFile("./test1.txt", (err, data) => {
fs.writeFile("./test1_copy01.txt", data, err => {
console.log("写入完成:", err);
});
});
}
方案2 利用可读流和可写流
{
const readStream = fs.createReadStream("./test1.txt");
const writeStream = fs.createWriteStream("./test1_copy02.txt");
// 监听读取结果进行写入
readStream.on("data", result => {
writeStream.write(result);
});
// 读取结束,进行关闭
readStream.on("end", () => {
writeStream.close();
});
}
方案3 读写流+管道
{
const readStream = fs.createReadStream("./test1.txt");
const writeStream = fs.createWriteStream("./test1_copy03.txt");
readStream.pipe(writeStream);
}
!
- 作 者 : Yaopengfei(姚鹏飞)
- 博客地址 : http://www.cnblogs.com/yaopengfei/
- 声 明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
- 声 明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。