从路径拼接到文件读写掌握 Node.js 两大核心模块并理解 JS 异步编程的完整进化链。 本文结构path 模块路径处理 → fs 模块文件系统→ 同步 vs 异步 → 异步进化史回调 → Promise → async/await一、 path 模块 —— 路径处理利器path是 Node.js 的内置模块无需安装专门处理文件路径和目录路径。1.1 path.join vs path.resolve 这两个 API 都能拼接路径但行为有关键区别// 1.mjs — join vs resolveimportpathfrompath;// ─── path.join单纯拼接 ───console.log(path.join(a,b,c));// a\b\cconsole.log(path.join(process.cwd(),/hello,world));// → C:\Users\...\hello\world /hello 被当作普通片段拼接// ─── path.resolve解析为绝对路径 ───console.log(path.resolve(a,b,c));// C:\...\当前目录\a\b\cconsole.log(path.resolve(/hello,world,./a,b));// → C:\hello\world\a\b 遇到 /hello 绝对路径就以此为起点console.log(path.resolve(/hello,world,../a,b));// → C:\hello\a\b ../ 回退一级目录 一张表看懂区别path.joinpath.resolve行为直接拼接成路径解析成绝对路径绝对路径参数被当作普通字符串拼接以最后一个绝对路径为起点相对路径直接拼接基于cwd当前工作目录拼出绝对路径..处理❌ 不解析✅ 解析目录回退适用场景拼接已知的安全路径片段需要得到确定的绝对路径// 当第一个参数都是绝对路径时二者结果相同path.join(/a,b,c);// /a/b/cpath.resolve(/a,b,c);// /a/b/c1.2 path 工具函数全家桶 // 2.mjs — path 常用工具方法importpathfrompath;// dirname — 获取目录名console.log(path.dirname(process.cwd()));// C:\Users\...上一级目录console.log(path.dirname(/a/b/c));// /a/b// basename — 获取文件名可去除扩展名console.log(path.basename(/a/b/c.js));// c.jsconsole.log(path.basename(/a/b/c.js,.js));// c 去掉 .jsconsole.log(path.basename(/a/b/c.js,js));// c. 去掉 jsconsole.log(path.basename(/a/b/c.js,s));// c.j 去掉末尾 s// extname — 获取扩展名console.log(path.extname(/a/b/c.js));// .jsconsole.log(path.extname(index.html));// .html// normalize — 规范化路径console.log(path.normalize(a/b//c/d/e/..));// a\b\c\d 去掉双斜杠解析 ..console.log(path.normalize(/a/b/c.js));// \a\b\c.js// parse — 解析路径为对象console.log(path.parse(/home/user/dir/file.txt));/* { root: /, dir: /home/user/dir, base: file.txt, ext: .txt, name: file } */️ path 工具速查表path.parse(/home/user/file.txt) └─────┬─────┘ └┬┘ └──┬──┘ dir name ext └──────┬──────┘ baseAPI功能示例输出path.dirname(p)取目录名/a/b/c.js→/a/bpath.basename(p[, ext])取文件名可去后缀/a/b/c.js→c.jspath.extname(p)取扩展名/a/b/c.js→.jspath.normalize(p)规范化路径a//b/..→apath.parse(p)解析为对象→{ root, dir, base, ext, name }path.join(...)拼接路径a,b→a/bpath.resolve(...)拼接 解析绝对路径a,b→/cwd/a/b1.3 ️ 工程化目录设计工程化思维用 path 模块管理项目目录结构。项目根目录 / ├── src/ ← 开发代码目录 │ ├── assets/ ← 静态资源图片、字体等 │ ├── libs/ ← 工具函数目录 │ └── index.js ← 入口文件 ├── dist/ ← 构建输出目录 └── package.jsonimportpathfrompath;// 工程化中常用的路径常量constROOTprocess.cwd();// 项目根目录constSRCpath.resolve(ROOT,src);// 源码目录constASSETSpath.resolve(SRC,assets);// 静态资源目录constLIBSpath.resolve(SRC,libs);// 工具函数目录console.log(path.join(ROOT,/hello,world));// join 遇到 /hello 不会解析为根直接拼接最佳实践需要确定的绝对路径用resolve简单拼接已知安全片段用join。二、 fs 模块 —— 文件系统操作fsFile System是 Node.js 操作文件和目录的核心模块底层用C 实现通过V8 引擎暴露给 JS 代码调用。2.1 同步 vs 异步 —— 核心选择Node.js 的 fs 模块同时提供了同步和异步两种 API 同步读取简单粗暴但会阻塞// 3.mjs — 同步读取阻塞主线程importfsfromfs;constsyncDatafs.readFileSync(./text.txt,utf-8);console.log(syncData);console.log(111);// ⚠️ 必须等文件读完才能执行 异步读取不阻塞高效// 3.mjs — 异步读取不阻塞主线程fs.readFile(./text.txt,utf-8,(err,data){if(!err){console.log(data);}else{console.log(err);}});console.log(111);// ✅ 先输出不等待文件读取⚖️ 为什么 Node.js 偏重异步JavaScript 是单线程语言 │ ▼ 同步 I/O → 阻塞线程 → 服务器卡住 → 浪费性能 │ ▼ 异步 I/O → 不阻塞 → Event Loop 调度 → 省服务器 Node.js 用异步非阻塞模型用更少的服务器支撑更高的并发——这就是它的核心竞争力。三、 fs 模块中的异步编程进化史从 fs 模块的 API 演进可以清晰地看到 JS 异步编程的完整进化链同步阻塞 → 异步回调 → Promise .then() → async/await │ │ │ │ read readFile fs/promises fs/promises FileSync callback .then() await3.1 第一阶段回调函数ES5 时代// 3.mjs — 异步回调单文件读取fs.readFile(./text.txt,utf-8,(err,data){// Node.js 约定回调第一个参数永远是 err 错误对象if(!err){console.log(data);}else{console.log(err);}});3.2 回调地狱 —— 流程控制的噩梦当多个文件有顺序依赖时先读 file1 → 再读 file2 → 最后读 file3回调层层嵌套变成金字塔// 3.mjs — 回调地狱依次读取 3 个文件fs.readFile(./file1.txt,utf-8,(err,data){if(!err){console.log(file1.txt,data);}else{console.log(err);}// 第二层fs.readFile(./file2.txt,utf-8,(err,data){if(!err){console.log(file2.txt,data);}else{console.log(err);}// 第三层fs.readFile(./file3.txt,utf-8,(err,data){if(!err){console.log(file3.txt,data);}else{console.log(err);}});});});回调地狱的三大痛点 ├── 层层嵌套可读性崩坏 ❌ ├── 错误处理重复冗余 ⚠️❌ └── 业务复杂时流程控制难以维护 ❌3.3 第二阶段Promise .then()ES6ES6 带来了 Promise用链式调用替代嵌套。fs/promises模块的 API 返回 Promise// 4.mjs — Promise 链式调用importfsfromfs/promises;fs.readFile(./file1.txt,utf-8).then((data){console.log(file1.txt,data);// then 回调返回 Promise继续链式调用returnfs.readFile(./file2.txt,utf-8);}).then((data){console.log(file2.txt,data);returnfs.readFile(./file3.txt,utf-8);}).then((data){console.log(file3.txt,data);}).catch((err){// 一个 .catch() 兜底所有错误console.error(读取失败,err);});Promise 三种状态┌──────────┐ resolve() ┌──────────────┐ │ pending │ ─────────────→ │ fulfilled │ │ (进行中) │ │ (已成功) │ └──────────┘ └──────────────┘ │ reject() ▼ ┌──────────────┐ │ rejected │ │ (已失败) │ └──────────────┘ 对比维度回调方式Promise 方式可读性 嵌套金字塔 扁平链式错误处理 每层单独处理 一个 .catch()流程控制 手动嵌套 .then() 自然串联⚠️ 但.then()链长了也像爬楼梯略显繁琐……3.4 第三阶段async/awaitES8 语法糖✨async/await 让异步代码看起来像同步代码可读性拉满// 4.mjs — async/await异步代码同步化importfsfromfs/promises;// IIFE立即执行函数表达式包裹 async(async(){try{constfile1Dataawaitfs.readFile(./file1.txt,utf-8);console.log(file1.txt,file1Data);constfile2Dataawaitfs.readFile(./file2.txt,utf-8);console.log(file2.txt,file2Data);constfile3Dataawaitfs.readFile(./file3.txt,utf-8);console.log(file3.txt,file3Data);}catch(err){// 一个 try-catch 搞定所有错误console.error(读取失败,err);}})(); async/await 的本质async/await Promise Generator 的语法糖 async function → 返回值自动包装成 Promise await → 等待右边的 Promise 完成 → 暂停 async 函数内部执行 → ⚠️ 不阻塞 Event Loop⚡ 关键误区// ❌ 错误理解await 之后代码同步执行// ✅ 正确理解await 本质还是 Promise 微任务console.log(1️⃣ 同步);awaitsomeAsyncTask();console.log(2️⃣ 微任务后);// ← 不是立即执行3.5 深入底层Event Loop、微任务、宏任务 await本质是Promise属于微任务MicrotasksetTimeout属于宏任务Macrotask。 Event Loop 运行机制┌─────────────────────────────────┐ │ Call Stack调用栈 │ │ 同步代码一行行压栈执行 │ └───────────┬─────────────────────┘ │ 遇到异步 API ▼ ┌─────────────────────────────────┐ │ libuv 线程池 / OS │ │ 文件 I/O、网络请求、定时器 │ └───────────┬─────────────────────┘ │ 完成回调入队 ▼ ┌─────────────────────────────────┐ │ 任务队列 │ │ ┌───────────────────────────┐ │ │ │ 微任务 (Microtask) │ │ ← Promise.then / await / queueMicrotask │ │ 优先级⭐⭐⭐⭐⭐ │ │ │ └───────────────────────────┘ │ │ ┌───────────────────────────┐ │ │ │ 宏任务 (Macrotask) │ │ ← setTimeout / setInterval / I/O 回调 │ │ 优先级⭐⭐⭐ │ │ │ └───────────────────────────┘ │ └─────────────────────────────────┘铁律执行一个宏任务 → 清空所有微任务 → 渲染浏览器→ 下一个宏任务 验证微任务 vs 宏任务执行顺序console.log(1️⃣ 同步开始);setTimeout((){console.log(2️⃣ 宏任务 - setTimeout);},0);Promise.resolve().then((){console.log(3️⃣ 微任务 - Promise.then);});(async(){awaitPromise.resolve();console.log(4️⃣ 微任务 - await 后);})();console.log(5️⃣ 同步结束);// 输出顺序// 1️⃣ → 5️⃣ → 3️⃣ → 4️⃣ → 2️⃣// └─── 微任务优先 ──┘ └→ 最后宏任务四、️ 全景总结path 模块 vs fs 模块┌─────────────────────────────────────────────────────────┐ │ Node.js path_fs │ ├─────────────────────┬───────────────────────────────────┤ │ path 模块 │ fs 模块 │ ├─────────────────────┼───────────────────────────────────┤ │ 处理路径字符串 │ 读写文件、操作目录 │ │ 纯字符串运算同步 │ I/O 操作可同步/异步 │ │ join / resolve │ readFileSync → 同步阻塞 │ │ dirname / basename │ readFile callback → 异步回调 │ │ extname / parse │ fs/promises .then() → Promise │ │ normalize │ fs/promises await → 语法糖 │ └─────────────────────┴───────────────────────────────────┘fs 模块异步进化总结方式所属时代写法特点readFileSync原始同步阻塞简单粗暴性能差readFile callbackES5回调嵌套异步但容易回调地狱fs/promises.then()ES6链式调用扁平化统一错误处理fs/promisesawaitES8像同步代码 语法糖本质仍是 Promise 微任务 核心认知async/await 让异步代码拥有了同步代码的书写体验但本质仍然是 Promise 微任务 —— 它不会阻塞 Event Loopawait 只是让你等得更优雅。
Node.js 核心模块实战:path 与 fs
发布时间:2026/7/12 1:13:44
从路径拼接到文件读写掌握 Node.js 两大核心模块并理解 JS 异步编程的完整进化链。 本文结构path 模块路径处理 → fs 模块文件系统→ 同步 vs 异步 → 异步进化史回调 → Promise → async/await一、 path 模块 —— 路径处理利器path是 Node.js 的内置模块无需安装专门处理文件路径和目录路径。1.1 path.join vs path.resolve 这两个 API 都能拼接路径但行为有关键区别// 1.mjs — join vs resolveimportpathfrompath;// ─── path.join单纯拼接 ───console.log(path.join(a,b,c));// a\b\cconsole.log(path.join(process.cwd(),/hello,world));// → C:\Users\...\hello\world /hello 被当作普通片段拼接// ─── path.resolve解析为绝对路径 ───console.log(path.resolve(a,b,c));// C:\...\当前目录\a\b\cconsole.log(path.resolve(/hello,world,./a,b));// → C:\hello\world\a\b 遇到 /hello 绝对路径就以此为起点console.log(path.resolve(/hello,world,../a,b));// → C:\hello\a\b ../ 回退一级目录 一张表看懂区别path.joinpath.resolve行为直接拼接成路径解析成绝对路径绝对路径参数被当作普通字符串拼接以最后一个绝对路径为起点相对路径直接拼接基于cwd当前工作目录拼出绝对路径..处理❌ 不解析✅ 解析目录回退适用场景拼接已知的安全路径片段需要得到确定的绝对路径// 当第一个参数都是绝对路径时二者结果相同path.join(/a,b,c);// /a/b/cpath.resolve(/a,b,c);// /a/b/c1.2 path 工具函数全家桶 // 2.mjs — path 常用工具方法importpathfrompath;// dirname — 获取目录名console.log(path.dirname(process.cwd()));// C:\Users\...上一级目录console.log(path.dirname(/a/b/c));// /a/b// basename — 获取文件名可去除扩展名console.log(path.basename(/a/b/c.js));// c.jsconsole.log(path.basename(/a/b/c.js,.js));// c 去掉 .jsconsole.log(path.basename(/a/b/c.js,js));// c. 去掉 jsconsole.log(path.basename(/a/b/c.js,s));// c.j 去掉末尾 s// extname — 获取扩展名console.log(path.extname(/a/b/c.js));// .jsconsole.log(path.extname(index.html));// .html// normalize — 规范化路径console.log(path.normalize(a/b//c/d/e/..));// a\b\c\d 去掉双斜杠解析 ..console.log(path.normalize(/a/b/c.js));// \a\b\c.js// parse — 解析路径为对象console.log(path.parse(/home/user/dir/file.txt));/* { root: /, dir: /home/user/dir, base: file.txt, ext: .txt, name: file } */️ path 工具速查表path.parse(/home/user/file.txt) └─────┬─────┘ └┬┘ └──┬──┘ dir name ext └──────┬──────┘ baseAPI功能示例输出path.dirname(p)取目录名/a/b/c.js→/a/bpath.basename(p[, ext])取文件名可去后缀/a/b/c.js→c.jspath.extname(p)取扩展名/a/b/c.js→.jspath.normalize(p)规范化路径a//b/..→apath.parse(p)解析为对象→{ root, dir, base, ext, name }path.join(...)拼接路径a,b→a/bpath.resolve(...)拼接 解析绝对路径a,b→/cwd/a/b1.3 ️ 工程化目录设计工程化思维用 path 模块管理项目目录结构。项目根目录 / ├── src/ ← 开发代码目录 │ ├── assets/ ← 静态资源图片、字体等 │ ├── libs/ ← 工具函数目录 │ └── index.js ← 入口文件 ├── dist/ ← 构建输出目录 └── package.jsonimportpathfrompath;// 工程化中常用的路径常量constROOTprocess.cwd();// 项目根目录constSRCpath.resolve(ROOT,src);// 源码目录constASSETSpath.resolve(SRC,assets);// 静态资源目录constLIBSpath.resolve(SRC,libs);// 工具函数目录console.log(path.join(ROOT,/hello,world));// join 遇到 /hello 不会解析为根直接拼接最佳实践需要确定的绝对路径用resolve简单拼接已知安全片段用join。二、 fs 模块 —— 文件系统操作fsFile System是 Node.js 操作文件和目录的核心模块底层用C 实现通过V8 引擎暴露给 JS 代码调用。2.1 同步 vs 异步 —— 核心选择Node.js 的 fs 模块同时提供了同步和异步两种 API 同步读取简单粗暴但会阻塞// 3.mjs — 同步读取阻塞主线程importfsfromfs;constsyncDatafs.readFileSync(./text.txt,utf-8);console.log(syncData);console.log(111);// ⚠️ 必须等文件读完才能执行 异步读取不阻塞高效// 3.mjs — 异步读取不阻塞主线程fs.readFile(./text.txt,utf-8,(err,data){if(!err){console.log(data);}else{console.log(err);}});console.log(111);// ✅ 先输出不等待文件读取⚖️ 为什么 Node.js 偏重异步JavaScript 是单线程语言 │ ▼ 同步 I/O → 阻塞线程 → 服务器卡住 → 浪费性能 │ ▼ 异步 I/O → 不阻塞 → Event Loop 调度 → 省服务器 Node.js 用异步非阻塞模型用更少的服务器支撑更高的并发——这就是它的核心竞争力。三、 fs 模块中的异步编程进化史从 fs 模块的 API 演进可以清晰地看到 JS 异步编程的完整进化链同步阻塞 → 异步回调 → Promise .then() → async/await │ │ │ │ read readFile fs/promises fs/promises FileSync callback .then() await3.1 第一阶段回调函数ES5 时代// 3.mjs — 异步回调单文件读取fs.readFile(./text.txt,utf-8,(err,data){// Node.js 约定回调第一个参数永远是 err 错误对象if(!err){console.log(data);}else{console.log(err);}});3.2 回调地狱 —— 流程控制的噩梦当多个文件有顺序依赖时先读 file1 → 再读 file2 → 最后读 file3回调层层嵌套变成金字塔// 3.mjs — 回调地狱依次读取 3 个文件fs.readFile(./file1.txt,utf-8,(err,data){if(!err){console.log(file1.txt,data);}else{console.log(err);}// 第二层fs.readFile(./file2.txt,utf-8,(err,data){if(!err){console.log(file2.txt,data);}else{console.log(err);}// 第三层fs.readFile(./file3.txt,utf-8,(err,data){if(!err){console.log(file3.txt,data);}else{console.log(err);}});});});回调地狱的三大痛点 ├── 层层嵌套可读性崩坏 ❌ ├── 错误处理重复冗余 ⚠️❌ └── 业务复杂时流程控制难以维护 ❌3.3 第二阶段Promise .then()ES6ES6 带来了 Promise用链式调用替代嵌套。fs/promises模块的 API 返回 Promise// 4.mjs — Promise 链式调用importfsfromfs/promises;fs.readFile(./file1.txt,utf-8).then((data){console.log(file1.txt,data);// then 回调返回 Promise继续链式调用returnfs.readFile(./file2.txt,utf-8);}).then((data){console.log(file2.txt,data);returnfs.readFile(./file3.txt,utf-8);}).then((data){console.log(file3.txt,data);}).catch((err){// 一个 .catch() 兜底所有错误console.error(读取失败,err);});Promise 三种状态┌──────────┐ resolve() ┌──────────────┐ │ pending │ ─────────────→ │ fulfilled │ │ (进行中) │ │ (已成功) │ └──────────┘ └──────────────┘ │ reject() ▼ ┌──────────────┐ │ rejected │ │ (已失败) │ └──────────────┘ 对比维度回调方式Promise 方式可读性 嵌套金字塔 扁平链式错误处理 每层单独处理 一个 .catch()流程控制 手动嵌套 .then() 自然串联⚠️ 但.then()链长了也像爬楼梯略显繁琐……3.4 第三阶段async/awaitES8 语法糖✨async/await 让异步代码看起来像同步代码可读性拉满// 4.mjs — async/await异步代码同步化importfsfromfs/promises;// IIFE立即执行函数表达式包裹 async(async(){try{constfile1Dataawaitfs.readFile(./file1.txt,utf-8);console.log(file1.txt,file1Data);constfile2Dataawaitfs.readFile(./file2.txt,utf-8);console.log(file2.txt,file2Data);constfile3Dataawaitfs.readFile(./file3.txt,utf-8);console.log(file3.txt,file3Data);}catch(err){// 一个 try-catch 搞定所有错误console.error(读取失败,err);}})(); async/await 的本质async/await Promise Generator 的语法糖 async function → 返回值自动包装成 Promise await → 等待右边的 Promise 完成 → 暂停 async 函数内部执行 → ⚠️ 不阻塞 Event Loop⚡ 关键误区// ❌ 错误理解await 之后代码同步执行// ✅ 正确理解await 本质还是 Promise 微任务console.log(1️⃣ 同步);awaitsomeAsyncTask();console.log(2️⃣ 微任务后);// ← 不是立即执行3.5 深入底层Event Loop、微任务、宏任务 await本质是Promise属于微任务MicrotasksetTimeout属于宏任务Macrotask。 Event Loop 运行机制┌─────────────────────────────────┐ │ Call Stack调用栈 │ │ 同步代码一行行压栈执行 │ └───────────┬─────────────────────┘ │ 遇到异步 API ▼ ┌─────────────────────────────────┐ │ libuv 线程池 / OS │ │ 文件 I/O、网络请求、定时器 │ └───────────┬─────────────────────┘ │ 完成回调入队 ▼ ┌─────────────────────────────────┐ │ 任务队列 │ │ ┌───────────────────────────┐ │ │ │ 微任务 (Microtask) │ │ ← Promise.then / await / queueMicrotask │ │ 优先级⭐⭐⭐⭐⭐ │ │ │ └───────────────────────────┘ │ │ ┌───────────────────────────┐ │ │ │ 宏任务 (Macrotask) │ │ ← setTimeout / setInterval / I/O 回调 │ │ 优先级⭐⭐⭐ │ │ │ └───────────────────────────┘ │ └─────────────────────────────────┘铁律执行一个宏任务 → 清空所有微任务 → 渲染浏览器→ 下一个宏任务 验证微任务 vs 宏任务执行顺序console.log(1️⃣ 同步开始);setTimeout((){console.log(2️⃣ 宏任务 - setTimeout);},0);Promise.resolve().then((){console.log(3️⃣ 微任务 - Promise.then);});(async(){awaitPromise.resolve();console.log(4️⃣ 微任务 - await 后);})();console.log(5️⃣ 同步结束);// 输出顺序// 1️⃣ → 5️⃣ → 3️⃣ → 4️⃣ → 2️⃣// └─── 微任务优先 ──┘ └→ 最后宏任务四、️ 全景总结path 模块 vs fs 模块┌─────────────────────────────────────────────────────────┐ │ Node.js path_fs │ ├─────────────────────┬───────────────────────────────────┤ │ path 模块 │ fs 模块 │ ├─────────────────────┼───────────────────────────────────┤ │ 处理路径字符串 │ 读写文件、操作目录 │ │ 纯字符串运算同步 │ I/O 操作可同步/异步 │ │ join / resolve │ readFileSync → 同步阻塞 │ │ dirname / basename │ readFile callback → 异步回调 │ │ extname / parse │ fs/promises .then() → Promise │ │ normalize │ fs/promises await → 语法糖 │ └─────────────────────┴───────────────────────────────────┘fs 模块异步进化总结方式所属时代写法特点readFileSync原始同步阻塞简单粗暴性能差readFile callbackES5回调嵌套异步但容易回调地狱fs/promises.then()ES6链式调用扁平化统一错误处理fs/promisesawaitES8像同步代码 语法糖本质仍是 Promise 微任务 核心认知async/await 让异步代码拥有了同步代码的书写体验但本质仍然是 Promise 微任务 —— 它不会阻塞 Event Loopawait 只是让你等得更优雅。