首页 / Node.js 教程 / 文件路径与目录操作

Node.js 教程

文件路径与目录操作

本教程共 76 篇 · 第 19 篇 · 更新于 2026-07-25 · 约 5 分钟阅读

Node.jspath目录文件系统跨平台

19. 文件路径与目录操作

本节目标:path 模块跨平台处理路径,以及目录的创建、遍历和删除。

操作文件之前,先得把路径搞对。Windows 用反斜杠,C:\Users\name,Linux 和 macOS 用正斜杠,/home/namepath 模块的存在就是为了抹平这个差异,让你写出跨平台的代码。目录操作则靠 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.posixpath.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 文件状态与权限

statlstat 的区别在于:如果路径是符号链接,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 内置了 globglobSync,不再需要额外安装包:

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-globglob npm 包替代,API 几乎一样。

19.6 实战:遍历目录并统计文件大小

pathfs/promisesglob 串起来,写一个递归统计目录大小的函数:

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`);