文件IO
I/O:input output
- 对外部设备的输入输出
- 外部设备:磁盘、网卡、显卡、打印机、其他…
- I/O的速度往往远低于内存和CPU的交互速度!
<<现代操作系统>>,英语水平允许的话,建议看原版。中文翻译的。。
fs模块—内置模块
具体用法去官网查api,fs中常用api(下述都是方法名)如下:
- fs.promises.xxx:node12(ES6之后)之后出现,它会返回一个Promise
- 后续我们都用这个promises下面的api读写文件等操作 ```javascript const fs = require(“fs”); const path = require(“path”);
const filename = path.resolve(__dirname, “./myfiles/1.txt”); // fs.readFile( // filename, // { // encoding: “utf-8”, //只有encoding配置时,可以直接把这个配置作为readFile的第二个参数 // }, // (err, content) => { // console.log(content); // } // );
// Sync函数是同步的,会导致JS运行阻塞,极其影响性能 // 通常,在程序启动时运行有限的次数即可
// const content = fs.readFileSync(filename, “utf-8”); // console.log(content);
async function test() { const content = await fs.promises.readFile(filename, “utf-8”); console.log(content); }
test();
- fs.readFile:读取一个文件,编码格式默认是**buffer**
- fs.writeFile:向文件写入数据,编码格式默认是**utf-8**。**若目录不存在则会报错,文件不存在会新建**
- **fs.unlink: 删除文件,传一个路径就行**
- fs.copyFile:复制文件内容到另外一个文件
- fs.stat:获取**文件或者目录**的状态信息
- size:占用的字节数。目录的size为0,在OS中,目录其实也是一种特殊的空文件,记录一个指针
- birthTime:创建文件的时间戳
- atime: 上次访问文件的时间
- mtime:上次文件内容被修改时间
- ctime:上次文件状态被修改时间
- isDirectory():是否是目录
- isFile():是文件
- 其他很多属性,查api文档
- fs.readdir:获取目录中所有的 **直接** 文件和子目录的名字,由所有字符串组成一个数组
- fs.mkdir:创建目录
- fs.exists:判断文件或目录是否存在,官方把它废弃了。说使用,fs.stat中的那俩属性
<a name="J5DNO"></a>
## 练习:读取一个目录中的所有子目录和文件
每个目录或文件都是一个对象,具有一些属性、方法。还有个filename:文件/目录的绝对路径<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/1413110/1631873500625-87b2e295-872c-444e-a4bf-bd1ba458e479.png#clientId=u684f05aa-b52b-4&from=paste&height=713&id=u3b3ceef9&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1426&originWidth=1760&originalType=binary&ratio=1&size=1102766&status=done&style=none&taskId=uaf40ea1a-2b49-4fbd-816f-24decf32d40&width=880)
```javascript
const fs = require("fs");
const path = require("path");
class File {
constructor(filename, name, ext, isFile, size, createTime, updateTime) {
this.filename = filename;
this.name = name;
this.ext = ext;
this.isFile = isFile;
this.size = size;
this.createTime = createTime;
this.updateTime = updateTime;
}
async getContent(isBuffer = false) {
if (this.isFile) {
if (isBuffer) {
return await fs.promises.readFile(this.filename);
} else {
return await fs.promises.readFile(this.filename, "utf-8");
}
}
return null;
}
async getChildren() {
if (this.isFile) {
//文件不可能有子文件
return [];
}
let children = await fs.promises.readdir(this.filename);
children = children.map(name => {
const result = path.resolve(this.filename, name);
return File.getFile(result);
});
return Promise.all(children);
}
static async getFile(filename) {
const stat = await fs.promises.stat(filename);
const name = path.basename(filename);
const ext = path.extname(filename);
const isFile = stat.isFile();
const size = stat.size;
const createTime = new Date(stat.birthtime);
const updateTime = new Date(stat.mtime);
return new File(filename, name, ext, isFile, size, createTime, updateTime);
}
}
async function readDir(dirname) {
const file = await File.getFile(dirname);
return await file.getChildren();
}
async function test() {
const dirname = path.resolve(__dirname, "./myfiles");
const result = await readDir(dirname);
const datas = await result[0].getChildren();
console.log(datas);
}
test();
文件流
什么是流
- 流是指数据的流动,数据从一个地方缓缓的、一点点的流动到另外一个地方
流是有方向的
其他介质和内存的数据规模不一致
- 其他介质和内存的数据处理能力不一致
在node中有个流模块—stream
const { Readable, Writable } = require("stream");//其他类型的流,都应该继承自这俩个
文件流
什么是文件流?—— 内存数据和磁盘文件数据之间的流动
文件流的创建
文件可读流
fs.createReadStream(path[, options])
- 含义:创建一个文件可读流,用于读取文件内容
- path:要读取的文件路径
- options:可选配置
- autoClose: 默认为true,文件被读完后就会自动关闭
- encoding: 编码方式
- start:起始字节
- end:结束字节
- highWaterMark: 每次读取的数量,默认是64KB,但它的单位不一定是kb,还要看encoding的值
- encoding有值,为utf-8(它认为汉字是三个字节)时,highWaterMark为1就代表一个汉字。
- encoding为null,highWaterMark数量就代表字节数
返回值:Readable的子类ReadStream,简称rs
- 事件:rs.on(‘事件名’, 回调/处理函数)
事件名有:
- open:文件被正常打开后触发回调
- error:打开文件失败
- close:文件被关闭后就会触发回调
- 可通过rs.close();手动关闭,然后就会触发回调
- 文件读完了也会自动关闭,然后触发回调
data:读取到一部分数据后就会触发回调,所以会反复触发
- 只有注册了这个data事件,才会真正读取数据!否则不读!
- 每次读取highWaterMark指定的数量
- 回调函数中会附带每次读到的数据
rs.on("data", chunk => {
console.log("读到了一部分数据:", chunk);
rs.pause(); //暂停
rs.resume();//恢复
});
end:全部数据读取完毕后触发回调,先触发end,再触发close
- pause:读取数据被暂停时触发回调
- re.pause():手动暂停读取,会触发pause事件
- resume:重新恢复读取数据时触发回调
- rs.resume():手动恢复读取,会触发resume事件
文件可写流
fs.createWriteStream(path[, options])
- 含义:创建一个文件可写流,用于向文件写入数据
- path:要写入数据的文件路径
- options:可选配置
- flags:操作文件的方式
- w:直接覆盖掉原有内容,a:追加内容
- autoClose: 默认为true,数据被写完后就会自动关闭
- encoding: 编码方式,默认为utf-8
- start:起始字节
- highWaterMark: 默认16KB。这里代表的就是每次最多写入的字节数!这点与可读流不一样
- 它的大小就是下述写入通道的长度。单位是字节
- flags:操作文件的方式
- 返回值:Writable的子类WriteStream,简称ws
- ws.on(“事件名”, 处理函数)。用法,基本同可读流
- drain事件:写入通道清空时触发回调
- ws.write(data); 写入一组数据
- data可以是字符串或Buffer
- 返回一个boolean值。还是因为内存比硬盘的处理速度快多了
- true:写入通道没有被填满,接下来的数据可以直接写入,无须排队
- false:写入通道目前已被填满,接下来的数据将进入写入队列
3. **背压问题**:写入队列中积压了过多的数据,导致占用内存很高
1. 当返回false时我们就不写write数据,这样就不会产生背压问题,但是时间依旧节约不了
1. 利用rs.pipe(ws) 或者 我们自己实现的类似pipe
4. 写入通道排满了,然后等它清空时(也就是都通道里都写到磁盘里后)!就会触发drain事件!
- ws.end([data]); 手动结束写入,然后会自动关闭文件(还取决于autoClose)
- data是可选的,表示关闭前最后一次写入的数据 ```javascript const fs = require(“fs”); const path = require(“path”);
const filename = path.resolve(__dirname, “./temp/abc.txt”);
const ws = fs.createWriteStream(filename, { encoding: “utf-8”, highWaterMark: 16 * 1024 });
let i = 0; //一直写,直到到达上限,或无法再直接写入 function write() { let flag = true; while (i < 1024 1024 10 && flag) { flag = ws.write(“a”); //写入a,得到下一次还能不能直接写 i++; } }
write();
ws.on(“drain”, () => { write(); });
<a name="wxT81"></a>
## 实现:将文件A内容复制到另外一个文件
```javascript
const fs = require("fs");
const path = require("path");
//方式1, 利用现有api,一次性读写, 占用内存很大
async function method1() {
const from = path.resolve(__dirname, "./temp/abc.txt");
const to = path.resolve(__dirname, "./temp/abc2.txt");
console.time("方式1");
const content = await fs.promises.readFile(from);
await fs.promises.writeFile(to, content);
console.timeEnd("方式1");
console.log("复制完成");
}
//方式2, 读一点写一点,停止、恢复, 其实就是rs.pipe(ws)的原理
async function method2() {
const from = path.resolve(__dirname, "./temp/abc.txt");
const to = path.resolve(__dirname, "./temp/abc2.txt");
console.time("方式2");
const rs = fs.createReadStream(from);
const ws = fs.createWriteStream(to);
rs.on("data", chunk => {
//读到一部分数据
const flag = ws.write(chunk);
if (!flag) {
//表示下一次写入,会造成背压
rs.pause(); //暂停读取
}
});
ws.on("drain", () => {
//可以继续写了
rs.resume();
});
//上述两个on极其内容可以替换为一句: rs.pipe(ws);
rs.on("close", () => {
//写完了
ws.end(); //完毕写入流
console.timeEnd("方式2");
console.log("复制完成");
});
}
method2();
rs.pipe(ws)
- 将可读流连接到可写流
- 返回值就是ws
- 该方法可解决背压问题