Node.js 文件操作:path 路径与 fs 异步流程

一、path 模块 --- 路径处理

path 是 Node.js 内置模块,专门用来处理文件路径。项目中凡是跟路径打交道的地方,都离不开它。

1.1 path.join()path.resolve() 的区别

两个方法都能拼路径,但做事的方式不一样。

path.join() :把传入的各段用 / 拼接起来,然后对结果做一次归一化(消除多余的 ... 和连续斜杠)。它有一个特殊规则:如果某一段是绝对路径(以 / 开头),前面已经拼好的部分会被丢弃,从这个绝对路径段重新开始拼。

path.resolve() :像在终端里敲 cd 命令一样,一段一段地"走进"目录,最终输出一个绝对路径。如果全程没有遇到绝对路径段,就把当前工作目录(cwd)作为起点。

js 复制代码
import path from 'path';

// join --- 拼接 + 归一化
path.join('a', 'b', 'c');                        // 'a/b/c'
path.join(process.cwd(), '/hello', 'world');      // '/hello/world'
// ↑ /hello 是绝对路径,前面拼的 cwd 被丢弃,从 /hello 重新开始

// resolve --- 像 cd 一样逐段解析,始终返回绝对路径
path.resolve('a', 'b', 'c');                      // '/当前工作目录/a/b/c'
path.resolve('/hello', 'world', './a', 'b');      // '/hello/world/a/b'
path.resolve('/hello', 'world', '../a', 'b');     // '/hello/a/b'(.. 回退了一级)

// 对比:当第一个参数是绝对路径时,两者结果一样(readme 中的重要结论)
path.join('/hello', 'world', './a', 'b');         // '/hello/world/a/b'
path.join('/hello', 'world', '../a', 'b');        // '/hello/a/b'
// ↑ join 把 .. 也处理掉了,所以结果和 resolve 相同

什么时候结果不一样? 当所有参数都是相对路径时:

js 复制代码
path.resolve('a', 'b');  // '/当前工作目录/a/b' --- resolve 补上了 cwd
path.join('a', 'b');     // 'a/b'               --- join 就老老实实拼,不补绝对路径

简单记:join 拼完归一下,resolve 拼完一定给绝对路径。

实际项目里怎么用:

lua 复制代码
项目根目录 /           → process.cwd() 或 __dirname
  ├── src/             → path.resolve(__dirname, 'src')
  │   ├── assets/      → path.resolve(__dirname, 'src/assets')  静态资源目录
  │   └── libs/        → path.resolve(__dirname, 'src/libs')    工具函数目录
  └── dist/            → 构建输出目录

path.resolve(__dirname, 'src') 来定位目录,不管脚本从哪个路径执行,都能拿到正确的绝对路径。

1.2 其它常用的 path 方法

js 复制代码
import path, { normalize } from 'path';
// normalize 既可以通过 path.normalize() 调用,也可以作为具名导出直接使用

// dirname --- 取目录名
path.dirname('/a/b/c.js');        // '/a/b'
path.dirname(process.cwd());      // 上一级目录

// basename --- 取文件名,第二个参数可以指定要去掉的后缀
path.basename('a/b/c.js');        // 'c.js'
path.basename('a/b/c.js', '.js'); // 'c'(去掉了 .js 后缀)
path.basename('a/b/c.js', 'js');  // 'c.'(注意:只去掉了 'js' 两个字符,点还在)
path.basename('a/b/c.js', 's');   // 'c.j'(只去掉了末尾的 's')
// basename 去掉后缀时是纯粹做字符串尾部匹配删除,不会自动处理 . 号

// extname --- 取扩展名(含点号)
path.extname('/a/b/c.js');        // '.js'
path.extname('/a/b/.gitignore');  // ''(以点开头的文件不算有扩展名)
path.extname('/a/b/index.html');  // '.html'

// normalize --- 把路径里的 . 和 .. 和多余斜杠清理干净
normalize('a/b//c/d/e/..');       // 'a/b/c/d'

// parse --- 把路径拆成一个对象
path.parse('/home/user/dir/file.txt');
// { root: '/', dir: '/home/user/dir', base: 'file.txt', ext: '.txt', name: 'file' }

// isAbsolute --- 判断一个路径是不是绝对路径
path.isAbsolute('/a/b/c');        // true(POSIX)
path.isAbsolute('a/b/c');         // false

// relative --- 计算从一个路径到另一个路径的相对路径
path.relative('/a/b/c', '/a/b/d/e');  // '../d/e'
path.relative('/a/b/c', '/a/b/c/d');  // 'd'

// sep --- 当前平台的分隔符(这个值用的时候取,不是函数调用)
path.sep;  // Linux/Mac 上是 '/',Windows 上是 '\'

// delimiter --- 环境变量 PATH 的分隔符
path.delimiter;  // Linux/Mac 上是 ':',Windows 上是 ';'

二、fs 模块 --- 文件系统操作

fs 是 Node.js 用来读写文件的核心模块。Node.js 本身用 C++ 写底层的 fs、path 这些模块,上面包了一层 V8 引擎来跑 JavaScript。

2.1 同步读和异步读

js 复制代码
import fs from 'fs';

// 同步读取 --- 代码停在这里等结果,后面的代码全被堵住
const data = fs.readFileSync('./test.txt', 'utf-8');
console.log(data);

// 异步读取 --- 不堵路,读完了通过回调函数拿到结果
fs.readFile('./test.txt', 'utf-8', (err, data) => {
    // Node 的惯例:回调的第一个参数永远是 err
    if (err) {
        console.log(err);
        return;
    }
    console.log(data);
});

为什么 Node.js 要用异步?

JavaScript 是单线程的。如果用同步读一个大文件,整个程序就得停下来等着,什么别的事都干不了。异步读发起之后立刻返回,等文件读完了再通过事件循环(event loop)把结果送回来,中间的时间可以处理别的请求。这也是 Node.js 能撑住高并发的秘密------一台服务器能干别的语言好几台的事。

前端环境里没有文件系统这回事,所以读写文件是 Node.js 独有的能力,同步和异步两条路都可以选,但绝大多数场景都应该走异步。

2.2 异步流程控制的进化过程

这是 Node.js 异步编程最值得搞清楚的一条线:

scss 复制代码
同步(堵路) → 异步(不堵路) → callback(回调)
    → 回调地狱(嵌套太深) → Promise.then(链式调用)
    → async/await(ES8 语法糖)

第一阶段:回调 --- 写着写着就掉进了回调地狱

js 复制代码
// 需求:按顺序读 file1 → file2 → file3
fs.readFile('./file1.txt', 'utf-8', (err, data) => {
    console.log('file1:', data);
    fs.readFile('./file2.txt', 'utf-8', (err, data) => {
        console.log('file2:', data);
        fs.readFile('./file3.txt', 'utf-8', (err, data) => {
            console.log('file3:', data);
            // 还要读 file4、file5 的话...... 一层一层往里套,人就懵了
        });
    });
});

这里的麻烦在于:每一步依赖上一步的结果,只能在前一步的回调里发起下一步。文件一多,代码就缩进成金字塔,俗称"回调地狱"。

第二阶段:Promise + then --- 链式调用

js 复制代码
import fs from 'fs/promises'; // 换用 Promise 版本的 fs

fs.readFile('./file1.txt', 'utf-8')
    .then(data => {
        console.log('file1:', data);
        return fs.readFile('./file2.txt', 'utf-8'); // 返回 promise,继续链下去
    })
    .then(data => {
        console.log('file2:', data);
        return fs.readFile('./file3.txt', 'utf-8');
    })
    .then(data => {
        console.log('file3:', data);
    })
    .catch(err => {
        console.log('出错了:', err);
    });

把嵌套拍平了,代码从左往右缩进变成从上往下展开,读起来舒服不少。catch 一个地方就能兜住所有步骤的错误。

还有点小遗憾:每个 .then() 里都得写 return,步骤多了也有点啰嗦。

第三阶段:async/await --- 看起来像同步的异步代码

js 复制代码
import fs from 'fs/promises';

(async () => {
    const file1 = await fs.readFile('./file1.txt', 'utf-8');
    console.log('file1:', file1);

    const file2 = await fs.readFile('./file2.txt', 'utf-8');
    console.log('file2:', file2);

    const file3 = await fs.readFile('./file3.txt', 'utf-8');
    console.log('file3:', file3);
})();

代码看起来跟同步写法几乎一样,从上到下一行行读下来,很直观。这里要搞清几点:

  • async/await 是语法糖,底层跑的仍然是 Promise。它只是让写法更舒服,不改变执行机制。
  • await 帮我们干了"等结果拿到再往下走"这件事,不用自己去写 .then() 链。
  • await 后面的代码进的是微任务队列(microtask),setTimeout 那种进的是宏任务(macrotask),两条队不一样。
  • 代码读起来像同步,但线程不会被堵住,这就是"异步代码同步化写"的意思。

三、总结

path 管路径怎么走:join 拼接 + 归一化,resolve 拼完一定返回绝对路径。工程里用 resolve 来定位目录比较稳,因为拿到的总是绝对路径。

fs 管文件怎么读写:能用异步就用异步,不堵路是 Node 的优势。异步写法从 callback 进化到 Promise 再进化到 async/await,每一步都是为了让人少掉头发。

async/await 让异步代码可读性好了一大截,但心里要知道它背后依然是 Promise 加事件循环那套东西。

相关推荐
小粉粉hhh2 小时前
Node.js(五)——编写接口
前端·javascript·node.js
先吃饱再说10 小时前
别再手动拼接路径了!Node.js path 模块的 9 个核心 API 详解
node.js
烬羽18 小时前
还在手动拼路径、写回调地狱?一文吃透 Node.js 的 path 和 fs
javascript·程序员·node.js
Darling噜啦啦18 小时前
Node.js path 与 fs 模块完全指南:从路径处理到异步文件操作的进化之路
node.js
子兮曰18 小时前
Node.js v26.5.0 深度解读:import Text、流式 ReadableStreamTee、以及隐藏的安全加固浪潮
前端·后端·node.js
万敏20 小时前
TypeScript 入门第一周完整记录:知识点总结 + 踩坑实录
node.js·全栈
甜味弥漫1 天前
React Router:从单页应用到前端路由
javascript·node.js
甜味弥漫1 天前
后端开发入门:业务能力、数据库与程序员的核心竞争力
javascript·node.js·全栈