如何在 Node.js 中使用文件系统

前言:Web 应用程序并不总是需要写入文件系统,但 Node.js 提供了一个全面的应用程序编程接口 (API) 来实现这一点。如果您要输出调试日志、将文件传输到服务器或从服务器传输文件,或者创建命令行工具,那么它可能是必不可少的。

值得注意的是:

1、Windows、macOS 和 Linux 处理文件的方式不同。例如,您使用正斜杠 / 来分隔 macOS 和 Linux 中的目录,但 Windows 使用反斜杠 \ 并禁止使用某些文件名字符,例如 : 和 ?。

2、检查权限。用户或其他应用程序可能会删除文件或更改访问权限。始终检查此类问题并有效处理错误。

Node.js fs 模块

Node.js fs 模块提供管理文件和目录的方法。如果您正在使用其他 JavaScript 运行时:

Deno 提供自己的文件系统 API 以及对 node:fs API 的支持。

Bun 提供优化的文件 I/O API 以及 node:fs API。

浏览器在沙盒中运行,无法直接与操作系统或底层文件系统通信。也就是说,您可以通过文件系统 API 上传文件并允许有限的访问。它在概念上有所不同,超出了本教程的范围。

所有 JavaScript 运行时都在单个处理线程上运行。底层操作系统处理文件读写等操作,因此 JavaScript 程序继续并行运行。然后,操作系统会在文件操作完成时提醒运行时。

fs 文档提供了一长串函数,但有三种具有类似功能的一般类型,我们将在下文中介绍。

1. 回调函数

这些函数以完成回调函数作为参数。以下示例传递一个内联函数,该函数输出 myfile.txt 的内容。假设没有错误,则其内容在程序结束时显示在控制台中:

javascript 复制代码
import { readFile } from 'node:fs';

readFile('myfile.txt', { encoding: 'utf8' }, (err, content) => {
  if (!err) {
    console.log(content);
  }
});

console.log('end of program');

注意:{ encoding: 'utf8' } 参数确保 Node.js 返回文本内容的字符串,而不是二进制数据的 Buffer 对象。

当您需要一个接一个地运行并陷入嵌套回调地狱时,这会变得复杂!编写回调函数也很容易,这些回调函数看起来正确,但会导致难以调试的内存泄漏。

在大多数情况下,今天几乎没有理由使用回调。下面的几个示例都使用了它们。

2.同步函数

"Sync" 函数实际上忽略了 Node 的非阻塞 I/O,并提供了与其他编程语言类似的同步 API。以下示例在控制台中出现程序结束之前输出 myfile.txt 的内容:

javascript 复制代码
import { readFileSync } from 'node:fs';

try {
  const content = readFileSync('myfile.txt', { encoding: 'utf8' });
  console.log(content);
}
catch {}

console.log('end of program');

它看起来更容易,我永远不会说不要使用 Sync......但是,呃......不要使用 Sync!它会停止事件循环并暂停您的应用程序。在 CLI 程序中加载小型初始化文件时,这可能没问题,但请考虑一个有 100 个并发用户的 Node.js Web 应用程序。如果一个用户请求一个需要一秒钟才能加载的文件,他们会等待一秒钟才能得到响应------其他 99 个用户也是如此!

3. Promise 函数

ES6/2015 引入了 Promise。它们是回调的语法糖,可提供更甜蜜、更简单的语法,尤其是与 async/await 一起使用时。Node.js 还引入了"fs/promises"API,其外观和行为与同步函数语法类似,但仍然是异步的:

javascript 复制代码
import { readFile } from 'node:fs/promises';

try {
  const content = await readFile('myfile.txt', { encoding: 'utf8' });
  console.log(content);
}
catch {}

console.log('程序结束');

请注意使用 'node:fs/promises' 模块和 readFile() 之前的 await。

下面的大多数示例都使用基于承诺的语法。大多数示例为简洁起见不包括 try 和 catch,但您应该添加这些块来处理错误。

ES 模块语法

本教程中的示例还使用 ES 模块 (ESM) 导入,而不是 CommonJS 要求。ESM 是 Deno、Bun 和浏览器运行时支持的标准模块语法。

要在 Node.js 中使用 ESM,请执行以下操作之一:

1、使用 .mjs 扩展名命名 JavaScript 文件

2、在命令行上使用 --import=module 例如 node --import=module index.js,或者

3、如果您有项目 package.json 文件,请添加新的"type":"module"设置

如果需要,您仍然可以使用 CommonJS 要求。

读取文件

有几种读取文件的函数,但最简单的方法是使用 readFile 将整个文件读入内存,如上例所示:

javascript 复制代码
import { readFile } from 'node:fs/promises';
const content = await readFile('myfile.txt', { encoding: 'utf8' });

第二个选项对象也可以是字符串。它定义编码:设置"utf8"或其他文本格式以将文件内容读入字符串。

或者,您可以使用 filehandle 对象的 readLines() 方法一次读取一行:

javascript 复制代码
import { open } from 'node:fs/promises';

const file = await open('myfile.txt');

for await (const line of file.readLines()) {
  console.log(line);
}

处理文件和目录路径

您经常需要访问特定绝对路径或相对于 Node 应用程序工作目录的路径的文件。node:path 模块提供了跨平台方法来解析所有操作系统上的路径。

path.sep 属性返回目录分隔符 --- Windows 上的 \ 或 Linux 或 macOS 上的 /:

javascript 复制代码
import * as path from 'node:path';

console.log( path.sep );

但还有更多有用的属性和功能。join([...paths]) 连接所有路径段并针对操作系统进行规范化:

javascript 复制代码
console.log( path.join('/project', 'node/example1', '../example2', 'myfile.txt') );
/*
/project/node/example2/myfile.txt on macOS/Linux
\project\node\example2\myfile.txt on Windows
*/

resolve([...paths]) 类似,但返回完整的绝对路径:

javascript 复制代码
console.log( path.resolve('/project', 'node/example1', '../example2', 'myfile.txt') );
/*
/project/node/example2/myfile.txt on macOS/Linux
C:\project\node\example2\myfile.txt on Windows
*/

normalize(path) 解析所有目录 .. 和 . 引用:

javascript 复制代码
console.log( path.normalize('/project/node/example1/../example2/myfile.txt') );
/*
/project/node/example2/myfile.txt on macOS/Linux
\project\node\example2\myfile.txt on Windows
*/

relative(from, to) 计算两个绝对路径或相对路径之间的相对路径(基于 Node 的工作目录):

javascript 复制代码
console.log( path.relative('/project/node/example1', '/project/node/example2') );
/*
../example2 on macOS/Linux
..\example2 on Windows
*/

format(object) 从组成部分的对象构建完整路径:

javascript 复制代码
console.log(
  path.format({
    dir: '/project/node/example2',
    name: 'myfile',
    ext: 'txt'
  })
);
/*
/project/node/example2/myfile.txt
*/

parse(path) 执行相反的操作并返回描述路径的对象:

javascript 复制代码
console.log( path.parse('/project/node/example2/myfile.txt') );
/*
{
  root: '/',
  dir: '/project/node/example2',
  base: 'myfile.txt',
  ext: '.txt',
  name: 'myfile'
}
*/

获取文件和目录信息

您经常需要获取有关路径的信息。它是文件吗?它是目录吗?它是什么时候创建的?它上次修改的时间是什么时候?您能读取它吗?您能向其中附加数据吗?

stat(path) 函数返回一个 Stats 对象,其中包含有关文件或目录对象的信息:

javascript 复制代码
import { stat } from 'node:fs/promises';

const info = await stat('myfile.txt');
console.log(info);
/*
Stats {
  dev: 4238105234,
  mode: 33206,
  nlink: 1,
  uid: 0,
  gid: 0,
  rdev: 0,
  blksize: 4096,
  ino: 3377699720670299,
  size: 21,
  blocks: 0,
  atimeMs: 1700836734386.4246,
  mtimeMs: 1700836709109.3108,
  ctimeMs: 1700836709109.3108,
  birthtimeMs: 1700836699277.3362,
  atime: 2023-11-24T14:38:54.386Z,
  mtime: 2023-11-24T14:38:29.109Z,
  ctime: 2023-11-24T14:38:29.109Z,
  birthtime: 2023-11-24T14:38:19.277Z
}
*/

它还提供了的方法,包括:

javascript 复制代码
const isFile = info.isFile(); // true
const isDirectory = info.isDirectory(); // false

access(path) 函数测试是否可以使用通过常量设置的特定模式访问文件。如果可访问性检查成功,则承诺将不产生任何值。如果失败,则承诺将被拒绝。例如:

javascript 复制代码
import { access, constants } from 'node:fs/promises';

const info = {
  canRead: false,
  canWrite: false,
  canExec: false
};

// 可读吗?
try {
  await access('myfile.txt', constants.R_OK);
  info.canRead = true;
}
catch {}

// 可写
try {
  await access('myfile.txt', constants.W_OK);
  info.canWrite = true;
}
catch {}

console.log(info);
/*
{
  canRead: true,
  canWrite: true
}
*/

您可以测试多种模式,例如测试文件是否可读又可写:

写入文件

writeFile() 是最简单的函数,用于异步写入整个文件(如果文件已存在)并替换其内容:

javascript 复制代码
import { writeFile } from 'node:fs/promises';
await writeFile('myfile.txt', 'new file contents');

传递以下参数:

1、文件路径

2、文件内容 --- 可以是字符串、缓冲区、TypedArray、DataView、Iterable 或 Stream

3、可选的第三个参数可以是表示编码的字符串(例如"utf8")或具有编码和中止承诺的信号等属性的对象。

类似的 appendFile() 函数将新内容添加到当前文件的末尾,如果该文件不存在,则创建该文件。

对于喜欢冒险的人来说,有一个文件处理程序 write() 方法,它允许您在特定点和长度替换文件内的内容。

创建目录

mkdir() 函数可以通过传递绝对或相对路径来创建完整的目录结构:

javascript 复制代码
import { mkdir } from 'node:fs/promises';

await mkdir('./subdir/temp', { recursive: true });

您可以传递两个参数:

1、目录路径

2、带有递归布尔值和模式字符串或整数的可选对象

将递归设置为 true 会创建整个目录结构。上面的示例在当前工作目录中创建 subdir 并将 temp 创建为其子目录。如果递归为 false(默认值),则如果 subdir 尚未定义,则承诺将拒绝。

模式是 macOS/Linux 用户、组和其他权限,默认值为 0x777。这在 Windows 上不受支持,并且会被忽略。

类似的 .mkdtemp() 函数类似,并创建一个通常用于临时数据存储的唯一目录。

读取目录内容

.readdir() 读取目录内容。承诺以包含所有文件和目录名称(除 . 和 .. 外)的数组实现。名称相对于目录,不包含完整路径:

javascript 复制代码
import { readdir } from 'node:fs/promises';

const files = await readdir('./'); // 当前工作目录
for (const file of files) {
  console.log(file);
}

/*
file1.txt
file2.txt
file3.txt
index.mjs
*/

您可以传递具有以下属性的可选第二个参数对象:

1、encoding --- 默认值为 utf8 字符串数组

2、recursive --- 设置为 true 以递归方式从所有子目录中获取所有文件。文件名将包含子目录名称。旧版本的 Node.js 可能不提供此选项。

3、withFileType --- 设置为 true 以返回 fs.Dirent 对象数组,其中包括 .name、.path、.isFile()、.isDirectory() 等属性和方法。

替代的 .opendir() 函数允许您异步打开目录进行迭代扫描:

javascript 复制代码
import { opendir } from 'node:fs/promises';

const dir = await opendir('./');
for await (const entry of dir) {
  console.log(entry.name);
}

删除文件和目录

.rm() 函数删除指定路径下的文件或目录:

javascript 复制代码
import { rm } from 'node:fs/promises';

await rm('./oldfile.txt');

您可以传递具有以下属性的可选第二个参数对象:

1、force --- 设置为 true,当路径不存在时不会引发错误

2、recursive --- 设置为 true,以递归方式删除目录和内容

3、maxRetries --- 当另一个进程锁定文件时,进行多次重试

4、retryDelay --- 重试之间的毫秒数

类似的 .rmdir() 函数仅删除目录(您不能传递文件路径)。同样,.unlink() 仅删除文件或符号链接(您不能传递目录路径)。

其他文件系统函数

上面的示例说明了读取、写入、更新和删除文件和目录的最常用选项。Node.js 还提供了其他较少使用的选项,例如复制、重命名、更改所有权、更改权限、更改日期属性、创建符号链接和监视文件更改。

监视文件更改时,最好使用基于回调的 API,因为它的代码更少、更易于使用,并且不会停止其他进程:

javascript 复制代码
import { watch } from 'node:fs';

// 当目录中有任何变化时运行回调
watch('./mydir', { recursive: true }, (event, file) => {

  console.log(`event: ${ event }`);

  if (file) {
    console.log(`file changed: ${ file }`);
  }

});

回调接收的事件参数是"change"或"rename"。

摘要:Node.js 提供了灵活的跨平台 API,用于管理任何可以使用运行时的操作系统上的文件和目录。只要稍加注意,您就可以编写可以与任何文件系统交互的强大且可移植的 JavaScript 代码。

有关更多信息,请参阅 Node.js fs 和路径文档。其他有用的库包括:

1、OS 用于查询操作系统信息

2、URL 用于解析 URL,可能用于映射到文件系统路径和从文件系统路径映射 URL

3、Stream用于处理大文件

4、BufferTypedArray对象用于处理二进制数据

5、Child processes用于生成子进程来处理长时间运行或复杂的文件操作函数。

您还可以在 npm 上找到更高级别的文件系统模块,但没有比编写自己的模块更好的体验了。

相关推荐
一斤代码2 小时前
vue3 下载图片(标签内容可转图)
前端·javascript·vue
中微子2 小时前
React Router 源码深度剖析解决面试中的深层次问题
前端·react.js
光影少年2 小时前
从前端转go开发的学习路线
前端·学习·golang
中微子3 小时前
React Router 面试指南:从基础到实战
前端·react.js·前端框架
3Katrina3 小时前
深入理解 useLayoutEffect:解决 UI "闪烁"问题的利器
前端·javascript·面试
前端_学习之路4 小时前
React--Fiber 架构
前端·react.js·架构
coderlin_4 小时前
BI布局拖拽 (1) 深入react-gird-layout源码
android·javascript·react.js
甜瓜看代码4 小时前
1.
react.js·node.js·angular.js
伍哥的传说4 小时前
React 实现五子棋人机对战小游戏
前端·javascript·react.js·前端框架·node.js·ecmascript·js
qq_424409194 小时前
uniapp的app项目,某个页面长时间无操作,返回首页
前端·vue.js·uni-app