nodejs api 学习6:路径与文件操作

0 阅读6分钟

import.meta

import.meta.dirname 和 import.meta.filename

import.meta.dirname 是 Node.js、Deno 等运行时在 ES 模块(ESM) 中提供的属性,用于获取当前模块文件所在目录的绝对文件系统路径,是 CommonJS 中 __dirname 的 ESM 替代方案。

同理,import.meta.filename是 CommonJS 中 __filename 的 ESM 替代方案。

关系import.meta.dirnamepath.dirname(import.meta.filename)

Node.js:v20.6.0+ 实验性v22.16.0 /v24.0.0+ 稳定

// 模块文件:/project/src/utils.js console.log(import.meta.dirname); 
// Unix/macOS 
输出:/project/src 
// Windows 
输出:C:\project\src

import.meta.url

在 ES 模块(ESM)中,import.meta.url 会返回当前模块文件的 URL 字符串,格式是:

file:///Users/xxx/project/index.js
  • 是 URL 格式,不是普通文件路径

  • file:// 协议头

  • 路径分隔符是 /

fileURLToPath,Node.js 内置工具函数,来自:

import { fileURLToPath } from 'node:url';

file://xxx 这种文件 URL 转换成操作系统原生文件路径

合起来:fileURLToPath(import.meta.url),它的结果就是:当前执行文件的绝对路径(字符串)

import { fileURLToPath } from 'node:url'; 
const __filename = fileURLToPath(import.meta.url); 
console.log(__filename);

在 macOS/Linux 输出:
/Users/xxx/project/index.jsWindows 输出:
C:\Users\xxx\project\index.js

以前 CommonJS 里有:

  • __filename:当前文件完整路径
  • __dirname:当前文件所在目录

ESM 里没有这两个变量,所以标准兼容写法是:

import { fileURLToPath } from 'node:url'; 
import path from 'node:path'; 
const __filename = fileURLToPath(import.meta.url); 
const __dirname = path.dirname(__filename);

为什么不直接用 import.meta.dirname

  • import.meta.dirname 是 Node.js 新版本才支持(v22.16+ / v24+)
  • 老项目、服务器环境可能不支持
  • fileURLToPath(import.meta.url) 兼容性极强,几乎所有现代 Node.js ESM 都能用

总结:

  • import.meta.url → 拿到 file:// 格式的当前文件地址

  • fileURLToPath(...) → 把它转成系统能用的正常绝对文件路径

path

path 模块专门用来处理文件路径,解决跨平台(Windows/macOS/Linux)路径格式不一致的问题,是 Node.js 必备工具。

path.join()

拼接路径(自动处理斜杠、跨平台兼容,最常用!)

import path from 'node:path'; 
const fullPath = path.join('a', 'b', 'c'); // a/b/c (mac/linux)

2. path.resolve()

解析为绝对路径(从右往左拼接,遇到绝对路径停止)

path.resolve('foo', '/bar', 'baz'); // /bar/baz

3. path.basename()

获取文件名(含 / 不含后缀)

path.basename('/a/b/c.js'); // c.js
path.basename('/a/b/c.js', '.js'); // c

4. path.dirname()

获取文件所在目录(就是 __dirname

path.dirname('/a/b/c.js'); // /a/b

5. path.extname()

获取文件后缀名

path.extname('/a/b/c.js'); // .js
path.extname('index.html'); // .html

6. path.relative()

计算从一个路径到另一个路径的相对路径

path.relative('/a/b', '/a/c/d'); // ../c/d

7. path.normalize()

规范化路径(解析 .. . 多余斜杠)

path.normalize('/a//b/c/../d'); // /a/b/d

URL

node:url 是 Node.js 官方用来解析、构造、转换 URL的核心模块,专门处理各种网址、文件路径(file://)、data: 协议等。

import url from 'node:url';

const myURL =
  new url.URL('https://user:pass@sub.example.com:8080/xxx/yyy?a=1&b=2#hash'); 

console.log(myURL.hash, myURL.host, myURL.searchParams);

console.log(myURL.searchParams.get('a'));

myURL.searchParams.set('b', 222);
myURL.searchParams.append('c', 333);

console.log(myURL.searchParams.toString())

创建一个 URL 实例,传入 url 字符串。

它会解析出 url 中各部分的内容,并且会把 query string 封装成 URLSearchParams 的实例。

URLSearchParams 有 get、set、append、toString 等方法。

image.png

当然,你也可以直接 new URLSearhParams:

const params = new url.URLSearchParams('?aa=1&bb=2');
console.log(params);
for (const [name, value] of params) {
    console.log(name, value);
}

fs

rm

删除文件 或 删除文件夹(包括非空)

Node.js ≥14.14 支持,现在所有项目都应该用它。

import fs from 'node:fs/promises';

await fs.rm('要删的路径', {
  recursive: true, // 递归删除(删文件夹必须加)
  force: true      // 不存在也不报错
});

能删什么?

  • 文件
  • 空文件夹
  • 非空文件夹(加 recursive: true

注意:fs.rmdir(旧版)仅删空文件夹,文件夹里有东西,直接报错。

mkdir

import fs from 'node:fs/promises'; 
// 1. 创建单层文件夹 await fs.mkdir('my-folder'); 

// 2. 创建多层文件夹(最常用!) 
await fs.mkdir('a/b/c/d', { recursive: true }); 

// 3. 安全创建(已存在不报错) 
await fs.mkdir('dist', { recursive: true });

注意:

  • 创建多层文件夹时,一定要加recursive: true;

  • 创建的路径不能是绝对路径,可以是aaaa/bbb,但不能是/aaa/bbb,这是 在系统根目录 / 下创建(需要 root 权限),可以这样写process.cwd() + '/aaa/bbb'

还有就是同步的写法:

fs.mkdirSync('dist/assets', { recursive: true });

下面介绍下对文件的操作。

writeFile

fs.writeFile 是异步写入文件**,不会阻塞主线程,Node.js 开发更推荐用它(尤其是服务端)。

  • writeFileSync:同步,阻塞代码

  • writeFile:异步,非阻塞(现代写法用 async/await

import fs from 'node:fs/promises'

async function write() {
  const data = { name: '张三', age: 22 }

  // 异步写入 JSON 文件
  await fs.writeFile('data.json', JSON.stringify(data, null, 2), 'utf-8')

  console.log('写入成功!')
}

write()

console.log('先执行') // 这一行会先打印

JSON.stringify(data, null, 2)这里的第三个参数的意思是,每个层级缩进 2 个空格,作用是让生成的 JSON 格式化、好看、人类可读

image.png

fs.writeFile 第三个参数可以是对象,也可以是字符串,如果是字符串一般是'utf-8'。

await fs.writeFile('log.txt', '新内容\n', {
  encoding: 'utf-8',
  flag: 'a' // 追加,不覆盖
})
  • 'w' → 覆盖写入(默认)

  • 'a' → 追加写入(不覆盖)

还有一点要注意,目录不存在时,要先创建目录,然后在写入:

import fs from 'node:fs/promises'
import path from 'node:path'

async function safeWrite() {
  const filePath = 'dist/data.json'

  // 自动创建目录
  await fs.mkdir(path.dirname(filePath), { recursive: true })

  // 再写入
  await fs.writeFile(filePath, '内容', 'utf-8')
}

readFile

readFile 就是 异步读取文件内容,和 writeFile 是一对。

import fs from 'node:fs/promises';

async function read() {
  // 读取文本文件
  const content = await fs.readFile('test.txt', 'utf-8');
  console.log(content);
}

read();

不传编码,返回 Buffer(二进制),如:<Buffer 68 65 6c 6c 6f>。

appendFile

  • writeFile覆盖整个文件
  • appendFile追加到文件末尾
import fs from 'node:fs/promises'; 
await fs.appendFile('log.txt', '异步追加一行\n', 'utf-8');

unlink

只能删文件,不能删文件夹,文件不存在会报错。

import fs from 'node:fs'

try {
  fs.unlinkSync('test.txt')
  console.log('文件删除成功')
} catch (err) {
  // 文件不存在也不崩溃
  if (err.code !== 'ENOENT') throw err
}

image.png

ENOENT 是 Node.js(以及所有类 Unix 系统)里最常见的文件系统错误码,你可以把它直接理解成:文件或目录不存在

  • E = Error
  • NO = No
  • ENT = Entry

直译:没有这个条目,也就是:你要操作的路径不存在。

readdir

用来读取一个目录下的所有文件和子文件夹名称

import fs from 'node:fs/promises';

async function readDir() {
  // 读取当前目录
  const files = await fs.readdir('./');

  console.log(files); // [ 'a.txt', 'b.js', 'dist', ... ]
}

readDir();

带上文件信息(withFileTypes):

const files = fs.readdirSync('aaa', { withFileTypes: true })

for (const item of files) {
  console.log(item.name) // 文件名
  console.log(item.isFile()) // 是否是文件
  console.log(item.isDirectory()) // 是否是文件夹
}

递归读取所有文件(recursive):包括文件和文件夹。

image.png

stat

用来获取文件 / 文件夹的详细信息:大小、创建时间、是不是文件、是不是文件夹等。

async function showStat() {
  const stat = await fs.stat('data.json')
  console.log(stat)
}
showStat()

image.png

stat.size // 文件大小(字节) 
stat.birthtimeMs // 创建时间戳 
stat.mtimeMs // 修改时间戳 
stat.uid / stat.gid // 用户/组ID

最常用的判断方法:

stat.isFile() // 是否是文件 
stat.isDirectory() // 是否是文件夹 
stat.isSymbolicLink() // 是否是软链接
async function info() {
  const s = await fs.stat('package.json')

  console.log('是文件:', s.isFile())
  console.log('大小:', s.size)
  console.log('修改时间:', new Date(s.mtimeMs))
}
info()

image.png

copyFile vs cp

copyFile 只复制文件,不能复制文件夹。目标文件已存在会直接覆盖。

import fs from 'node:fs/promises';

// 源文件 → 目标文件
await fs.copyFile('source.txt', 'target.txt');

console.log('复制成功!');

cp是 Node.js 现代、全能、推荐的复制 API,既能复制文件,也能复制整个文件夹

不过 fs 这个 cpSync 方法直到 node 22 才不再是实验性的。

image.png

所以低版本 node 还是要自己实现,或者用 fs-extra:

image.png

实现原理也很简单:

import fs from 'node:fs'
import path from 'node:path'

function copyDir(srcDir, destDir) {
  fs.mkdirSync(destDir, { recursive: true })

  for (const file of fs.readdirSync(srcDir)) {
    const srcFile = path.resolve(srcDir, file)
    const destFile = path.resolve(destDir, file)
    copy(srcFile, destFile)
  }
}

function copy(src, dest) {
  const stat = fs.statSync(src)
  if (stat.isDirectory()) {
    copyDir(src, dest)
  } else {
    fs.copyFileSync(src, dest)
  }
}

copy('aaa', 'aaa2')

用 fs.statSync 拿到文件的信息,如果是目录就递归复制目录,否则就 fs.copyFileSync 复制文件。

目录的复制就是用先用 fs.mkdirSync 创建目录,然后用 fs.readdirSync 读取目录的内容。

用 path.resolve 拼接下路径为绝对路径,之后继续递归 copy。