文件路径与目录操作
本教程共 76 篇 · 第 19 篇 · 更新于 2026-07-25 · 约 5 分钟阅读
19. 文件路径与目录操作
本节目标:path 模块跨平台处理路径,以及目录的创建、遍历和删除。
操作文件之前,先得把路径搞对。Windows 用反斜杠,C:\Users\name,Linux 和 macOS 用正斜杠,/home/name。path 模块的存在就是为了抹平这个差异,让你写出跨平台的代码。目录操作则靠 fs 模块,和上一章的读写是同一套 API。
19.1 path 模块:拼接与解析
拼接路径
最常用的是 path.join,按当前平台的分隔符把多段路径接起来:
import path from 'node:path';
const full = path.join('/usr', 'local', 'bin', 'node');
// Linux/macOS: /usr/local/bin/node
// Windows: \usr\local\bin\node
别再用字符串拼接了:__dirname + '/config.json' 在 Windows 上会拼出混合斜杠的怪路径。
path.resolve 会把路径解析成绝对路径,从右往左处理,遇到第一个绝对路径就停:
import path from 'node:path';
path.resolve('foo', 'bar'); // /当前工作目录/foo/bar
path.resolve('/a', '/b', 'c'); // /b/c
path.resolve('a', '../b', 'c'); // /当前工作目录/b/c
提取路径信息
import path from 'node:path';
const p = '/home/user/projects/app.js';
path.dirname(p); // /home/user/projects
path.basename(p); // app.js
path.basename(p, '.js'); // app
path.extname(p); // .js
path.parse 一次性拆完:
path.parse('/home/user/app.js');
// { root: '/', dir: '/home/user', base: 'app.js', ext: '.js', name: 'app' }
反过来,path.format 可以把对象拼回字符串:
path.format({ dir: '/home/user', base: 'app.js' }); // /home/user/app.js
相对路径与绝对路径
import path from 'node:path';
path.isAbsolute('/etc/passwd'); // true(POSIX)
path.isAbsolute('C:\\Windows'); // true(Windows)
path.isAbsolute('README.md'); // false
path.relative('/data/images', '/data/upload/2024/photo.jpg');
// ../upload/2024/photo.jpg
跨平台显式处理
如果你明确需要生成某种格式的路径(比如 URL 路径永远是正斜杠),可以用 path.posix 或 path.win32:
import path from 'node:path';
// 在 Windows 上也能生成 POSIX 风格的路径
path.posix.join('static', 'js', 'main.js'); // static/js/main.js
19.2 __dirname 与 import.meta.url
CommonJS 模块里有 __dirname 和 __filename,表示当前模块所在的目录和完整路径。ES 模块(import 语法)里没有这两个变量,需要用 import.meta.url 自己构造:
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// 读取同目录下的 config.json
const configPath = join(__dirname, 'config.json');
这段代码几乎是 ESM 项目的标配,建议封装成一个工具函数或直接记熟。
19.3 目录操作
创建目录
import { mkdir } from 'node:fs/promises';
// 创建单层目录
await mkdir('logs');
// 递归创建多级目录(v10.12.0 起支持)
await mkdir('data/2024/07', { recursive: true });
recursive: true 相当于命令行的 mkdir -p,目录已存在也不会报错。
读取目录内容
import { readdir } from 'node:fs/promises';
import { join } from 'node:path';
// 返回文件名数组
const names = await readdir('uploads');
console.log(names); // ['a.jpg', 'b.png']
// 带文件类型信息
const entries = await readdir('uploads', { withFileTypes: true });
for (const entry of entries) {
console.log(entry.name, entry.isFile(), entry.isDirectory());
}
withFileTypes: true 是个省事的选项,不用每个文件再单独 stat 一遍就能知道是文件还是目录。
重命名与移动
import { rename } from 'node:fs/promises';
await rename('old-folder', 'new-folder');
对目录和文件都有效。跨分区移动可能会抛 EXDEV 错误,需要手动复制再删除。
删除目录
import { rmdir, rm } from 'node:fs/promises';
// 只能删空目录
await rmdir('empty-folder');
// 递归删除目录及其内容(v14.14.0 起推荐)
await rm('temp', { recursive: true, force: true });
force: true 让删除在目录不存在时也不报错,写清理脚本时很方便。
19.4 文件状态与权限
stat 和 lstat 的区别在于:如果路径是符号链接,stat 会跟到目标文件,lstat 则返回链接本身的信息。
import { stat } from 'node:fs/promises';
const s = await stat('document.pdf');
console.log(s.size); // 字节数
console.log(s.isFile()); // 是否普通文件
console.log(s.isDirectory()); // 是否目录
console.log(s.isSymbolicLink()); // 是否符号链接(stat 始终 false,用 lstat)
// 时间戳
console.log(s.birthtime); // 创建时间
console.log(s.mtime); // 修改时间
19.5 通配符匹配 glob
自 v22.0.0 / v20.12.0 起,Node.js 内置了 glob 和 globSync,不再需要额外安装包:
import { glob } from 'node:fs/promises';
// 查找所有 js 文件
const jsFiles = await glob('**/*.js', { cwd: './src' });
console.log(jsFiles);
// 排除 node_modules
const sources = await glob('**/*.js', {
cwd: './src',
exclude: (name) => name.includes('node_modules')
});
Note如果你用的 Node 版本还没内置
glob,可以用fast-glob或globnpm 包替代,API 几乎一样。
19.6 实战:遍历目录并统计文件大小
把 path、fs/promises、glob 串起来,写一个递归统计目录大小的函数:
import { stat, readdir } from 'node:fs/promises';
import { join } from 'node:path';
async function getDirSize(dirPath) {
let total = 0;
const entries = await readdir(dirPath, { withFileTypes: true });
for (const entry of entries) {
const fullPath = join(dirPath, entry.name);
if (entry.isDirectory()) {
total += await getDirSize(fullPath);
} else {
const { size } = await stat(fullPath);
total += size;
}
}
return total;
}
const bytes = await getDirSize('./node_modules');
console.log(`目录大小: ${(bytes / 1024 / 1024).toFixed(2)} MB`);