CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Node.js 后端入门/课时/文件系统与 path
RawBlame
课时目录12 节

Node.js 后端入门

  1. 01Node.js 是什么
  2. 02运行时与 process
  3. 03模块与 npm
  4. 04文件系统与 path
  5. 05http 模块
  6. 06Express 入门
  7. 07事件循环与异步
  8. 08环境变量与配置
  9. 09Streams 基础
  10. 10错误处理
  11. 11测试 node:test
  12. 12综合练习与选型

文件系统与 path

fs 读写、path 拼接、__dirname 与 import.meta.url。

30 分钟

文件系统与 path

这一课你将能做到

  1. 用 node:fs/promises 完成读、写、追加、建目录、列目录、改名、删除等常见文件操作
  2. 用 node:path 拼出跨平台不出错的路径,说清 join 与 resolve 的区别
  3. 判断什么时候能用同步 API、什么时候必须异步,并理解 ENOENT 等常见错误码

为什么要学

Node 离开浏览器最大的红利就是能碰文件系统:内容批量替换、日志分析、生成报表、把配置落到磁盘……全都绕不开 fs。而路径错误是文件操作里最高频的事故源——Windows 用 \,macOS/Linux 用 /,C:\ 和 /home/ 的绝对路径写法也不同,全靠 path 模块兜底。

本站的很多工程脚本(把 Markdown 导入数据库、生成静态页)就是 fs + path + 第 2 课的 process.argv 组合出来的。这课学完,你就能独立写「读一批文件、处理、写回去」的完整工具。

先建立图像

fs 的三套 API(同一件事三种写法):

  node:fs/promises   → await fs.readFile(...)   ← 首选,不阻塞、写法清爽
  node:fs (callback) → fs.readFile(..., cb)     ← 老代码,保持会读即可
  node:fs (sync)     → fs.readFileSync(...)     ← 启动期/脚本一次性场景

path 的分工:

  join('a', 'b', 'c.txt')    → a\b\c.txt   拼接(自动补分隔符)
  resolve('a', 'b.txt')      → h:\work\a\b.txt  从 cwd 出发算绝对路径
  __dirname                  → 脚本自己的目录(最稳的锚点)

记忆口诀:路径用 join 拼、位置用 resolve 算、锚点是 __dirname。

跟着做

步骤 1:读文件(promises 版)

新建 node-fs 目录,npm init -y。先造一个样本文件 note.txt(内容随意),再新建 read.js:

// read.js
const fs = require('node:fs/promises');
const path = require('node:path');

async function main() {
  const filePath = path.join(__dirname, 'note.txt');
  const text = await fs.readFile(filePath, 'utf8');
  console.log('内容:', text);
}

main();
node read.js
# 内容: (你 note.txt 里的内容)

两个关键点:'utf8' 不传会拿到 Buffer(二进制对象,打印出来是十六进制);readFile 返回 Promise,必须 await 或 .then。

步骤 2:写文件与追加

新建 write.js:

// write.js
const fs = require('node:fs/promises');
const path = require('node:path');

async function main() {
  const file = path.join(__dirname, 'log.txt');

  await fs.writeFile(file, '第一行\n', 'utf8');        // 整体覆盖
  await fs.appendFile(file, '第二行\n', 'utf8');       // 追加
  await fs.appendFile(file, '第三行\n', 'utf8');

  const content = await fs.readFile(file, 'utf8');
  console.log(content);
}

main();
node write.js
# 第一行
# 第二行
# 第三行

node write.js
# 还是这三行 —— writeFile 每次整体覆盖

注意语义:writeFile 是「覆盖写」,想追加必须 appendFile。跑两次脚本输出不变,正好验证了覆盖语义。

步骤 3:目录操作

新建 dirs.js,演示建目录、列目录、判断存在:

// dirs.js
const fs = require('node:fs/promises');
const path = require('node:path');

async function main() {
  const dir = path.join(__dirname, 'data', 'posts');

  // recursive: 父目录不存在就一并创建,已存在也不报错
  await fs.mkdir(dir, { recursive: true });

  // 在里面造两个文件
  await fs.writeFile(path.join(dir, 'a.txt'), 'A');
  await fs.writeFile(path.join(dir, 'b.txt'), 'B');

  // 列出目录内容
  const entries = await fs.readdir(dir);
  console.log('文件:', entries);            // [ 'a.txt', 'b.txt' ]

  // 判断文件/目录是否存在:用 access,不要用 try-catch 包 readFile 之外的花活
  try {
    await fs.access(dir);
    console.log('目录存在');
  } catch {
    console.log('目录不存在');
  }
}

main();
node dirs.js
# 文件: [ 'a.txt', 'b.txt' ]
# 目录存在

老代码里常见 fs.existsSync(dir)(同步版),脚本里偶尔用没问题;异步代码里统一用 fs.access + try/catch。

步骤 4:stat——区分文件与目录、拿大小

新建 stat.js,遍历目录时区分类型:

// stat.js
const fs = require('node:fs/promises');
const path = require('node:path');

async function main() {
  const dir = __dirname;
  const entries = await fs.readdir(dir);

  for (const name of entries) {
    const full = path.join(dir, name);
    const s = await fs.stat(full);
    const type = s.isDirectory() ? '目录' : '文件';
    console.log(`${type}  ${String(s.size).padStart(6)} 字节  ${name}`);
  }
}

main();
node stat.js
# 文件     128  note.txt
# 目录         data
# ...

stat 返回 size(字节)、mtime(修改时间)等元数据,配合 isFile() / isDirectory() 就能写出「扫目录、按类型分发」的通用工具。

步骤 5:改名、复制、删除

新建 manage.js:

// manage.js
const fs = require('node:fs/promises');
const path = require('node:path');

async function main() {
  const dir = path.join(__dirname, 'data', 'posts');

  await fs.rename(path.join(dir, 'a.txt'), path.join(dir, 'a-renamed.txt'));
  console.log('改名完成');

  await fs.copyFile(
    path.join(dir, 'a-renamed.txt'),
    path.join(dir, 'a-copy.txt')
  );
  console.log('复制完成');

  await fs.unlink(path.join(dir, 'a-copy.txt'));   // 删除文件
  console.log('删除完成');

  await fs.rm(path.join(dir, 'b.txt'), { force: true });  // 不存在也不报错
  await fs.rm(path.join(__dirname, 'data'), { recursive: true, force: true });
  console.log('目录清理完成');
}

main();

安全红线:unlink/rm 没有回收站。涉及删除的脚本,先打印将要删除的完整路径让人确认,再真正执行;批量删除加 recursive 时务必先核对目标目录。

步骤 6:path 的核心方法

新建 path-demo.js:

// path-demo.js
const path = require('node:path');

// 1. join:拼接,自动处理分隔符和 ..
console.log(path.join('src', 'utils', 'a.js'));      // src\utils\a.js (win)
console.log(path.join('src', '..', 'public', 'x.css')); // public\x.css

// 2. resolve:从 cwd(或给定起点)算出绝对路径
console.log(path.resolve('data', 'a.txt'));
// h:\work\node-fs\data\a.txt(取决于你终端的当前目录)

// 3. 解析路径成分
const p = path.join(__dirname, 'data', 'posts', 'a.txt');
console.log(path.dirname(p));   // .../data/posts   目录
console.log(path.basename(p));  // a.txt             文件名
console.log(path.basename(p, '.txt')); // a          去后缀
console.log(path.extname(p));   // .txt              后缀(含点)

// 4. 跨平台分隔符
console.log(path.sep);          // win 是 \,linux/mac 是 /

join vs resolve 一句话:join 只做拼接(结果可相对可绝对,取决于输入),resolve 把结果算成绝对路径(以 cwd 或第一个绝对路径参数为基准)。

讲清楚要点

  1. 优先 node:fs/promises:代码线性、异常用统一 try/catch。callback 版只在改老代码时遇到;xxxSync 版会阻塞事件循环,只适合启动加载配置、一次性 CLI 脚本。
  2. 不传 'utf8' 拿到的是 Buffer:写二进制(图片、gzip 数据)时 Buffer 正合适;读写文本永远显式传 'utf8'。
  3. 相对路径参照 cwd 而不是脚本:fs.readFile('note.txt') 读的是终端当前目录的文件。锚定脚本目录用 path.join(__dirname, ...),ESM 用 import.meta.url 推导。
  4. 存在性检查与操作之间有竞态:access 通过不代表后面 readFile 一定成功(文件可能刚被删)。实践准则:先操作、用 try/catch 接住错误,而不是先检查再操作。
  5. 删除操作最危险:rm 的 recursive + force 组合等于「删整棵树、错也不报」,脚本里用它必须先校验路径来自可信配置而非用户输入。

你可能会懵的地方

  • ENOENT / EEXIST / EACCES 这些错误码什么意思? ENOENT = 文件或目录不存在;EEXIST = 已存在(如不带 recursive 的 mkdir);EACCES/EPERM = 无权限;EISDIR = 期望文件却是目录。catch 里按 err.code 分支处理,比看报错文案可靠。

  • fs.rename 能跨盘符移动文件吗? 不能保证:跨设备(如 C 盘到 D 盘)rename 会报 EXDEV。稳妥的「移动」是 copyFile 成功后 unlink 原文件。

  • readFile 一次读 2GB 的文件行吗? 语法上行,内存上不行——整个文件进内存。大文件用第 9 课的流(createReadStream),按块处理。

  • path.join('/a', 'b') 在 Windows 输出什么? \a\b。path 只关心分隔符语法,不校验磁盘真实存在。另外注意「字符串碰巧像路径」和「真实文件系统路径」的区别:拼接后的字符串要真正去读写才算数。

  • 遍历子目录树怎么做? fs.readdir(dir, { withFileTypes: true }) 返回带 isDirectory() 的对象数组,是目录就递归。进阶可用 Node 20 的 fs.glob(实验性)或 glob 这类包。

错法对照

错法问题正解
'data/' + name + '/file.txt' 手拼路径分隔符错、.. 不清理、跨平台崩path.join('data', name, 'file.txt')
fs.readFile(p) 打印出 <Buffer ...>忘了编码fs.readFile(p, 'utf8')
同步 API 写进 HTTP 请求处理器一次慢读写卡住所有请求用 fs/promises 异步版
先 access 再 open,不做 catch存在性检查与操作之间文件可能消失直接操作,catch 里按 err.code 处理
rm(dir, { recursive: true }) 删变量拼的路径路径算错整棵树没了删除前 console.log 确认 + 白名单校验
ESM 里直接用 __dirnameReferenceErrorfileURLToPath(import.meta.url) 推导

卡点急救

  1. ENOENT: no such file or directory:三查——相对路径是不是踩了 cwd 坑(改 path.join(__dirname, ...));文件名拼写;父目录是否已创建。
  2. 写文件后内容是乱码或 [object Object]:写对象前 JSON.stringify(obj, null, 2);读回来 JSON.parse。直接把对象传给 writeFile 会变成 [object Object] 字符串。
  3. EACCES 权限被拒:Windows 检查文件是否被占用(Excel 开着的 csv 不能写);Linux 检查文件属主与目录权限,必要时用管理员运行。
  4. EEXIST: file already exists:mkdir 没加 { recursive: true },或 writeFileSync 开了 { flag: 'wx' }(强制新建)却撞了已有文件。
  5. 改了文件但程序读到旧内容:编辑器「安全写入」会换 inode,用 rename 类保存的编辑器+监听工具会失灵;确认脚本读的路径和编辑器保存的路径是同一个。

面试常问

  • fs 的 promise / callback / sync 三套 API 怎么选? 新代码选 fs/promises(非阻塞 + 语法简洁);callback 版用于维护存量代码;sync 版只用于进程启动加载配置、一次性 CLI 脚本等「阻塞无妨」的场景,绝不能放进并发服务的请求路径。

  • path.join 和 path.resolve 的区别? join 把片段用分隔符连起来并规范化(不保证绝对路径);resolve 从右往左解析,遇到绝对路径片段就用它替换左侧,否则以 cwd 为基准,最终返回绝对路径。

  • 如何安全地批量删除目录? 校验路径来源(白名单/前缀检查)、打印目标让人确认、用 rm 的 recursive+force 前先限制在受控根目录之内;关键任务先复制或打包再删。

  • 大文件为什么不能用 readFile? readFile 把整个文件载入内存,GB 级文件会撑爆内存。用 createReadStream 按块(chunk)处理,内存占用恒定。

自测三问

  1. fs.writeFile 和 fs.appendFile 的语义区别?
  2. 在 h:\work 目录执行 node tools/a.js,脚本里 path.resolve('data') 和 path.join(__dirname, 'data') 各是什么?
  3. err.code === 'ENOENT' 代表什么?在什么操作里最常见?

小结一句话

fs/promises 负责文件增删改查,path 负责把路径拼对拼稳;记住「异步优先、utf8 别忘、__dirname 当锚、删除先确认」四条,文件脚本就立得住。

延伸

  • Node.js 官方文档 File system
  • Node.js 官方文档 path
  • 下一课:http 模块——不装任何依赖,用二十行代码起一个真正的 HTTP 服务器。
加载本节练习…
← 上一课模块与 npm下一课 →http 模块