Node.js 读文件最常用的是 fs.readFile 异步读取:引入 fs 模块后调用 fs.readFile 方法,把文件路径和编码传进去,在回调里就能拿到字符串内容。本文一次讲清 4 种主流读文件方式:fs.readFile 异步回调、fs.readFileSync 同步阻塞、fs.promises 配合 async/await、以及适合大文件的流式读取 createReadStream。你还会看到「忘记指定编码得到 Buffer」「同步读取阻塞事件循环」「大文件一次性读进内存直接爆掉」这些真实开发里会踩的坑怎么填。读完你不仅能把文件读出来,还知道每种写法该用在哪个场景、该避开哪些坑。今天编程狮就把这块讲透,让你下次读文件不再纠结用哪一个。

一、为什么需要读取文件
读取文件,本质是把磁盘上的内容搬进内存,交给 JavaScript 去处理。最常见的场景包括:读取配置文件拿到运行参数、读取用户上传的文本做解析、读取日志做统计、读取模板渲染页面。Node.js 是服务端运行时,磁盘 IO 是它的核心能力之一,所以「怎么读文件」是每个 Node.js 开发者都绕不开的基本功。
⚠️ 注意:Node.js 里所有文件操作都来自内置的 fs 模块,使用前必须先
const fs = require('fs')。不引入直接用会报ReferenceError,这是新手第一步最容易卡住的地方。
另一个容易混的点是「读出来到底是什么」。如果你在调用时没指定编码,拿到的是 Buffer(二进制缓冲区),不是字符串;只有显式传了 'utf8',回调里才是人能直接读的文本。判断该用哪种读取方式,先看你的约束:文件大不大、要不要阻塞当前流程、下游代码是回调风格还是 async 风格。这三类情况手法完全不同,下面逐个来。另外,读取文件常常和「路径处理」「异常兜底」一起出现,理清它们的关系,处理本地资源会顺手很多。
二、方法一:fs.readFile 异步回调(最推荐)
适合谁:绝大多数业务场景,尤其是 Web 服务里读配置、读静态资源。代价:回调嵌套,多个文件顺序读时会层层缩进。
const fs = require('fs');
fs.readFile('./data.txt', 'utf8', (err, data) => {
if (err) throw err;
console.log(data);
});
fs.readFile 把文件读到内存后,才把结果交给回调,期间主线程不阻塞,事件循环可以继续处理别的请求。它是 Node.js 最经典的异步写法,想系统学 Node.js 基础,Node.js 入门教程 有完整的学习路径。
选异步还是后面三种写法,先看你的约束。接口里读一个小配置,异步最稳,不会拖慢整个服务;如果你需要边读边解析、读多个文件再合并,回调嵌套会很难看,这时候就不如用 async/await。一个常见误用是忘记判断 err——文件不存在、权限不够都会走错误分支,不处理就会让进程直接崩。性能上,fs.readFile 会一次性把整个文件读进内存,几千几万行的文本都秒回,但遇上几百 MB 的大文件就危险了,那得用第五节讲的流式读取。实际项目里,读取文件往往发生在「启动加载配置」「按路由读模板」这一步,给后续逻辑提供原材料。
三、方法二:fs.readFileSync 同步读取
适合谁:脚本启动阶段、命令行工具,或者就在文件开头读一次配置。代价:会阻塞事件循环,不能在高频请求里用。
const fs = require('fs');
try {
const data = fs.readFileSync('./data.txt', 'utf8');
console.log(data);
} catch (err) {
console.error('读取失败', err);
}
fs.readFileSync 直接把结果作为返回值吐出来,代码从上往下读,没有回调,写起来最直观。但它会卡住当前线程——在读的这几百毫秒里,同一个进程的其他请求全得等着。所以同步读取只适合「启动时读一次」或「一次性脚本」,千万不要写在 HTTP 接口的处理函数里。
这种写法在「给一组本地数据临时编号」时很顺手,比如部署脚本读环境变量、构建工具读 package.json。但它有个天生短板:同步阻塞意味着高并发下吞吐骤降。如果你的服务之后还要被很多人同时访问,那不如在源头就改用异步。另外 fs.readFileSync 抛错要用 try/catch 接住,而不是回调里的 err 参数,这是同步和异步最大的写法差异。当你需要的不只是「读」,而是「读到的内容马上就要用」,同步读取就是首选,前提是它不在性能热路径上。
四、方法三:fs.promises + async/await
适合谁:现代 Node.js 项目,下游代码已经是 Promise/async 风格。代价:需要函数标 async,对老代码要改造。
const fs = require('fs').promises;
async function main() {
const data = await fs.readFile('./data.txt', 'utf8');
console.log(data);
}
main().catch(console.error);
fs.promises 返回的是 Promise,await 之后代码看起来和同步几乎一样,却不会阻塞事件循环。多个文件顺序读时,写 await a(); await b(); 比嵌套回调清爽太多。Node.js 内置模块速查可以看 Node.js 模块速查手册,把 fs 家族的方法铺平。
这种写法在「读多个文件再汇总」时最顺手,比如先读配置再读模板再读数据,用 Promise.all 还能并行读。但它有个天生边界:await 必须在 async 函数里,顶层不能直接 await(老版本 Node 需要包一层)。如果你的代码之后还要被按步骤组合,那 async/await 比回调好维护。另外 fs.promises 和 fs 是两个对象,require('fs').promises 才是 Promise 版,别直接对 fs.readFile 用 await,那是回调风格、返回不了 Promise。把这几类的边界厘清,读文件的代码基本不会再出结构混乱的问题。
五、方法四:流式读取与怎么选(含坑)
适合谁:大文件(几十 MB 以上)、需要边读边处理(比如逐行解析日志)。代价:写法比一次性读取复杂,要监听事件。
const fs = require('fs');
const rs = fs.createReadStream('./big.log', 'utf8');
rs.on('data', chunk => process.stdout.write(chunk));
rs.on('end', () => console.log('\n读取完成'));
createReadStream 把文件切成一块块流式吐出,内存里始终只驻留当前这块,所以几百 MB 的日志也能轻松处理。流式读取专门解决「大文件一次性读进内存直接爆掉」的问题,是处理大文件的唯一正解。
四种方法怎么选,记住这张对照:小文件、异步服务用 fs.readFile;脚本启动读一次用 fs.readFileSync;现代 async 项目用 fs.promises;大文件或边读边处理用 createReadStream。高频坑只有三个:第一,忘记指定文件编码、漏传 'utf8',得到的是 Buffer 而不是字符串,打印出来是一堆乱码数字;第二,在 HTTP 接口里用同步读取,阻塞事件循环把整个服务拖垮;第三,大文件硬用 readFile 一次性读,内存直接打爆进程。第一条最阴险——它不报错,只是类型不对,排查时很难第一时间想到是编码没指定。所以凡是来源不可控的文件,读之前先想清楚「我要字符串还是 Buffer、文件有多大、这段代码在不在热路径上」。

总结
Node.js 读取文件,日常首选 fs.readFile 异步回调,不阻塞、好读;启动阶段读一次用 fs.readFileSync 同步;现代 async 项目用 fs.promises 配 async/await;大文件或边读边处理用 createReadStream 流式读取。要点带走:
- 调用时务必传
'utf8',否则拿到的是 Buffer 而不是字符串; - 同步读取会阻塞事件循环,千万别写在接口处理函数里;
- 大文件别用 readFile 一次性读,用流式读取才稳。
延伸学习
想把这块知识系统补齐,可以按这个顺序来:
- 先过一遍 JavaScript 基础实战,把回调和 Promise 铺平;
- Node.js 入门课程
- 想看另一种讲法,参考 Node.js 文件操作笔记 的实战思路;
常见问题
Q:读出来的内容是一堆数字而不是文字,怎么回事?
A:因为你调用时没传编码参数。fs.readFile 在不指定编码时会返回 Buffer(二进制缓冲区),打印出来就是一串数字。只要在第二个参数传 'utf8',拿到的就是正常的字符串。记住:要文本就显式写编码,要原始二进制才用 Buffer。
Q:fs.readFile 和 fs.readFileSync 该用哪个?
A:看代码位置。在 Web 接口、需要并发处理请求的地方,必须用异步的 fs.readFile,否则会阻塞整个事件循环;在命令行脚本、服务启动只读取一次配置的地方,用同步的 fs.readFileSync 写起来更直观。一句话:热路径用异步,启动初始化可用同步。
Q:几百 MB 的大文件怎么读才不会内存爆掉?
A:别用 readFile 一次性读进内存,改用 fs.createReadStream 流式读取。它会把文件分块吐出,内存里只保留当前这一块,配合 data 事件边读边处理。这是处理大日志、大导出文件的唯一稳妥方式。

免费 AI IDE



