如何在 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 上找到更高级别的文件系统模块,但没有比编写自己的模块更好的体验了。

相关推荐
m0_748236111 分钟前
Calcite Web 项目常见问题解决方案
开发语言·前端·rust
Watermelo61713 分钟前
详解js柯里化原理及用法,探究柯里化在Redux Selector 的场景模拟、构建复杂的数据流管道、优化深度嵌套函数中的精妙应用
开发语言·前端·javascript·算法·数据挖掘·数据分析·ecmascript
m0_7482489415 分钟前
HTML5系列(11)-- Web 无障碍开发指南
前端·html·html5
m0_7482356126 分钟前
从零开始学前端之HTML(三)
前端·html
一个处女座的程序猿O(∩_∩)O2 小时前
小型 Vue 项目,该不该用 Pinia 、Vuex呢?
前端·javascript·vue.js
hackeroink5 小时前
【2024版】最新推荐好用的XSS漏洞扫描利用工具_xss扫描工具
前端·xss
迷雾漫步者7 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-7 小时前
验证码机制
前端·后端
燃先生._.8 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js