|
|
|
>这篇文章是 [Working with the file system on Node.js](https://2ality.com/2022/06/nodejs-file-system.html)学习过程中的个人总结和笔记,如果你对 Nodejs 中的文件系统感兴趣,我更建议你直接去看原文章。
|
|
|
|
|
|
|
|
## Node.js 文件系统 APIs 具有多种不同的风格
|
|
|
|
|
|
|
|
- 同步风格的函数调用 `fs.readFileSync(path, options?): string | Buffer`
|
|
|
|
- 异步风格的函数调用
|
|
|
|
- 异步回调 `fs.readFile(path, options?, callback): void`
|
|
|
|
- 异步 Promise `fsPromises.readFile(path, options?): Promise<string|Buffer>`
|
|
|
|
|
|
|
|
它们在名称上很容易区分:
|
|
|
|
- 异步回调的函数名,如 `fs.readFile()`
|
|
|
|
- 异步 Promise 的函数名和异步回调的函数名相同,但是在不同的模块中,如 `fsPromises.readFile()`
|
|
|
|
- 对于同步的函数名,则是在函数结尾加上 Sync 字样,如 `fs.readFileSync`
|
|
|
|
|
|
|
|
|
|
|
|
## 访问文件的方式
|
|
|
|
|
|
|
|
1. 可以通过字符串的方式读写文件
|
|
|
|
2. 可以打开读取流或写入流,并将文件分成更小的块,一次一个。流只允许顺序访问
|
|
|
|
3. 可以使用文件描述符或文件句柄,通过一个类似流的 API 获得顺序或是随机访问
|
|
|
|
- 文件描述符 (File descriptors) 是一个用于表示文件的整数,Nodejs 中有很多函数可以管理文件描述符
|
|
|
|
4. 只有同步风格和异步回调风格的 API 使用文件描述符,对于异步 Promise 风格的 API,则有着更加抽象的类实现:文件句柄类 (class FileHandle),它基于文件描述符
|
|
|
|
|
|
|
|
|
|
|
|
## 文件系统中的一些重要类 (Important classes)
|
|
|
|
|
|
|
|
### URLs:字符串文件系统路径的替代方案
|
|
|
|
|
|
|
|
当 Nodejs 的一个文件系统 API 支持字符串形式的路径参数,那么它通常也会支持 URL 的实例
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
fs.readFileSync('/tmp/text-file.txt')
|
|
|
|
fs.readFileSync(new URL('file:///tmp/text-file.txt'))
|
|
|
|
```
|
|
|
|
|
|
|
|
手动转换文件路径看起来很容易,但是实际上隐藏着许多坑,所以更加推荐以下函数对路径进行转换:
|
|
|
|
- `url.pathToFileURL()`
|
|
|
|
- `url.fileURLToPath()`
|
|
|
|
|
|
|
|
### Buffers:在 Nodejs 中表示固定长度的字节序列,它是 Uint8Array 的子类,更多的在二进制文件中使用
|
|
|
|
|
|
|
|
由于 Buffer 实际上是 Uint8Array 的子类,和 URL 与 String 的关系相同,当 Nodejs 接受一个 Buffer 的同时,它也接受 Uint8Array,考虑到 Uint8Array 具有跨平台(cross-platform) 的特性,很多时候可能会更加合适
|
|
|
|
|
|
|
|
Buffers 可以做一件 Uint8Array 不能做到的事:以各种编码方式对文本进行编码和解码,如果我们需要在 Uint8Array 中编码或解码 UTF-8,我们可以使用 `class TextEncoder` 或是 `class TextDecoder`
|
|
|
|
|
|
|
|
|
|
|
|
## 文件系统中的文件读和写
|
|
|
|
|
|
|
|
### 读取文件并将其拆分为行
|
|
|
|
|
|
|
|
>这里将会用两个读取文件并按行拆分的例子演示 fs.readFile 和 stream 读取的差异
|
|
|
|
|
|
|
|
**fs.readFile**
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
import * as fs from 'node:fs';
|
|
|
|
fs.readFileSync('text-file.txt', {encoding: 'utf-8'})
|
|
|
|
```
|
|
|
|
|
|
|
|
这种方法的优缺点:
|
|
|
|
- 优点:使用简单,在大多数情况下可以满足需求
|
|
|
|
- 缺点:对大文件而言并不是一个好的选择,因为它需要完整的读取完数据
|
|
|
|
|
|
|
|
按行拆分
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
// 拆分不包含换行符
|
|
|
|
const RE_SPLIT_EOL = /\r?\n/;
|
|
|
|
function splitLines(str) {
|
|
|
|
return str.split(RE_SPLIT_EOL);
|
|
|
|
}
|
|
|
|
// ['there', 'are', 'multiple', 'lines']
|
|
|
|
splitLines('there\r\nare\nmultiple\nlines');
|
|
|
|
|
|
|
|
// 拆分包含换行符
|
|
|
|
const RE_SPLIT_AFTER_EOL = /(?<=\r?\n)/;
|
|
|
|
function splitLinesWithEols(str) {
|
|
|
|
return str.split(RE_SPLIT_AFTER_EOL);
|
|
|
|
}
|
|
|
|
// ['there\r\n', 'are\n', 'multiple\n', 'lines']
|
|
|
|
splitLinesWithEols('there\r\nare\nmultiple\nlines')
|
|
|
|
```
|
|
|
|
|
|
|
|
**stream**
|
|
|
|
|
|
|
|
原文章中使用的是跨平台的 `web stream`,且声明了两个 class (`ChunksToLinesStream`、`ChunksToLinesTransformer`),类的代码太长这里就不贴出来了,这两个类也是实现 `web stream` 逐行读取的关键。
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
import * as fs from "node:fs";
|
|
|
|
import { Readable } from "node:stream";
|
|
|
|
|
|
|
|
const nodeReadable = fs.createReadStream(
|
|
|
|
"text-file.txt",
|
|
|
|
{ encoding: 'utf-8' }
|
|
|
|
);
|
|
|
|
const webReadableStream = Readable.toWeb(nodeReadable);
|
|
|
|
// 逐行读取
|
|
|
|
// const lineStream = webReadableStream.pipeThrough(
|
|
|
|
// new ChunksToLinesStream();
|
|
|
|
// )
|
|
|
|
for await (const line of lineStream) {
|
|
|
|
console.log(line);
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
`web stream` 是异步可迭代的,这也是为什么上述代码中使用 `for-await-of`
|
|
|
|
|
|
|
|
这种方法的优缺点:
|
|
|
|
- 优点:对大文件友好,因为我们并不需要等到所有数据读取完
|
|
|
|
- 缺点:使用复杂且都是异步操作
|
|
|
|
|
|
|
|
### 同步的将字符串写入到文件中
|
|
|
|
|
|
|
|
>和上面相同,这里也会使用两个例子演示 `fs.writeFile` 和 `stream` 的差异
|
|
|
|
|
|
|
|
**fs.writeFile**
|
|
|
|
|
|
|
|
使用这种办法写入时,如果文件已经存在于路径上,那么会被覆盖掉
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
import * as fs from 'node:fs';
|
|
|
|
|
|
|
|
fs.writeFileSync(
|
|
|
|
'existing-file.txt',
|
|
|
|
'Appended line\n',
|
|
|
|
{ encoding: 'utf-8' }
|
|
|
|
);
|
|
|
|
```
|
|
|
|
|
|
|
|
如果并不希望已经存在的文件被覆盖掉,那么需要在 `fs.createReadStream` 的 `options` 参数中添加一个属性: `flag: "a"`,这个属性表示我们需要追加而不是覆盖文件,
|
|
|
|
|
|
|
|
ps: 在有些 fs 函数中,这个属性名叫做 `flag`,但是另外一些函数中会被叫做 `flags`
|
|
|
|
|
|
|
|
**stream**
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
import * as fs from "node:fs";
|
|
|
|
import { Writable } from "node:stream";
|
|
|
|
|
|
|
|
// 使用 fs.createWriteStream 创建一个 Nodejs srteam
|
|
|
|
const nodeWritable = fs.createWriteStream("text-file.txt", {
|
|
|
|
encoding: "utf-8",
|
|
|
|
});
|
|
|
|
// 转换为 web stream
|
|
|
|
const webWritableStream = Writable.toWeb(nodeWritable);
|
|
|
|
const writer = webWritableStream.getWriter();
|
|
|
|
|
|
|
|
try {
|
|
|
|
await writer.write("First line\n");
|
|
|
|
await writer.write("Second line\n");
|
|
|
|
await writer.close();
|
|
|
|
} finally {
|
|
|
|
writer.releaseLock();
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
以上代码和 `fs.writeFile` 相同:如果路径上依旧存在文件,那么写入的内容会将原文件的内容覆盖掉,如果需要追加而不是覆盖,操作和 `fs.writeFile` 是相同的,在 `fs.createReadStream` 的 `options` 参数中添加一个属性 `flag: "a"`
|
|
|
|
|
|
|
|
## 遍历和创建目录
|
|
|
|
|
|
|
|
|
|
|
|
### 遍历目录
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
import * as fs from "node:fs";
|
|
|
|
import * as path from "node:path";
|
|
|
|
|
|
|
|
function* traverseDirectory(dirPath: string): any {
|
|
|
|
const dirEntries = fs.readdirSync(
|
|
|
|
dirPath,
|
|
|
|
{ withFileTypes: true }
|
|
|
|
);
|
|
|
|
dirEntries.sort((a, b) => a.name.localeCompare(b.name, "en"));
|
|
|
|
for (const dirEntry of dirEntries) {
|
|
|
|
const fileName = dirEntry.name;
|
|
|
|
const pathName = path.join(dirPath, fileName);
|
|
|
|
yield pathName;
|
|
|
|
|
|
|
|
if (dirEntry.isDirectory()) {
|
|
|
|
yield* traverseDirectory(pathName);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const filePath of traverseDirectory("../../../")) {
|
|
|
|
console.log(filePath);
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
在以上代码中我们用到了 `fs.readdirSync` 用来返回子目录路径,其中当 option 参数中的 `withFileTypes` 为 `true` 时,它会返回一个可迭代的 `fs.Dirent` 实例 (directory entries),如果 `withFileTypes` 为 false 或者不传,那么会以字符串类型返回字符串
|
|
|
|
|
|
|
|
### 创建目录
|
|
|
|
|
|
|
|
我们可以使用 `fs.mkdirSync(thePath, options?)` 来创建一个目录,其中 `options.recursive` 参数用于确定如何在 `thePath` 上创建目录:
|
|
|
|
- 如果 `recursive` 参数缺失或者为 false,那么 `fs.mkidrSync` 会返回 `undefined`,且在一些情况下会抛出异常:
|
|
|
|
- 目录已经存在于 `thePath` 上
|
|
|
|
- `thePath` 的父目录不存在
|
|
|
|
- 如果 `recursive` 为 true
|
|
|
|
- 即使目录已经存在于 `thePath` 上也不会抛出错误
|
|
|
|
- 如果父目录不存在,那么会一起创建父目录,并返回第一个创建成功的目录 path
|
|
|
|
|
|
|
|
### 确保父目录存在
|
|
|
|
|
|
|
|
如果我们希望按需设置嵌套文件结构(nested file structure),我们就没法总是在创建新文件时确保其祖先目录存在,以下代码就是为了解决这个问题
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
import * as fs from "node:fs";
|
|
|
|
import * as path from "node:path";
|
|
|
|
|
|
|
|
const ensureParentDirectory = (filePath: string) => {
|
|
|
|
const parentDir = path.dirname(filePath);
|
|
|
|
if (!fs.existsSync(parentDir)) {
|
|
|
|
fs.mkdirSync(parentDir, { recursive: true });
|
|
|
|
}
|
|
|
|
};
|
|
|
|
```
|
|
|
|
|
|
|
|
### 创建临时目录
|
|
|
|
|
|
|
|
在有些场景,我们需要创建一个临时目录,那么这时我们可以使用 `fs.mkdtempSync(pathPrefix, options?)`,它会在 `pathPrefix` 目录下创建一个随机6位字符串作为名称的目录,并返回路径
|
|
|
|
|
|
|
|
如果我们需要在系统的全局临时目录中创建临时目录,我们可以借助 `os.tmpdir()`
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
import * as os from "node:os";
|
|
|
|
import * as fs from "node:fs";
|
|
|
|
import * as path from "node:path";
|
|
|
|
|
|
|
|
const pathPrefix = path.resolve(os.tmpdir(), "my-app");
|
|
|
|
const tmpPath = fs.mkdtempSync(pathPrefix);
|
|
|
|
```
|
|
|
|
|
|
|
|
需要注意的是,创建的临时目录并不会在 Nodejs 脚本结束后被删除,我们需要自己手动的去删除它,或是依赖系统自己的定期清除(不太可靠)
|
|
|
|
|
|
|
|
|
|
|
|
## 复制、重命名、移动文件或是目录
|
|
|
|
|
|
|
|
### 复制文件或是目录
|
|
|
|
|
|
|
|
`fs.cpSync(srcPath, destPath, options?)` 可以帮助我们完成这个需求,将文件从 `scrPath` 复制到 `destPath`,而其中的 `options` 有如下一些有意思的参数:
|
|
|
|
- `recursive`
|