# 逐行读取 readline
30 秒速记
readline.createInterface({ input, crlfDelay: Infinity })把任意可读流按行切开- 逐行读大文件:
for await (const line of rl),内存只占当前行 crlfDelay: Infinity保证\r\n被当成一个换行,Windows文件不会多出空行- 交互式命令行用
readline/promises的rl.question()(Node 17起) - 处理
Ctrl+C监听SIGINT,Ctrl+D/ 输入结束会触发close;非终端环境process.stdin.isTTY为undefined
readline 就是给可读流装一把「按行切」的刀,常用在两个场景:逐行读大文件,和命令行里跟用户一问一答。 读大文件时用 createInterface 包住 fs.createReadStream,再 for await 一行一行处理,几个 GB 的日志内存也就占一行的大小。记得加 crlfDelay: Infinity,否则 Windows 换行可能被拆成两次。交互场景用 readline/promises 的 question(),配合 await 写起来很顺;用完一定要 rl.close(),不然进程会一直挂着等输入。
理解「逐行读取 readline」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。
工程中还要验证退出码、资源清理和异常输入。属于 module 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。
readline 的两种典型用法,各给一段能直接跑的代码。
逐行读文件:
const fs = require('fs');
const readline = require('readline');
async function countErrors(file) {
const rl = readline.createInterface({
input: fs.createReadStream(file),
crlfDelay: Infinity, // \r\n 当作一个换行
});
let total = 0, errors = 0;
for await (const line of rl) {
total++;
if (line.includes('ERROR')) errors++;
}
return { total, errors }; // 读完 rl 会自动 close
}
countErrors('app.log').then(console.log); // { total: 10234, errors: 17 }
命令行交互(Node 17 起有 readline/promises):
const readline = require('readline/promises');
async function main() {
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
rl.on('SIGINT', () => { rl.close(); process.exitCode = 130; }); // Ctrl+C
const name = await rl.question('项目名称:');
console.log(`创建 ${name}`);
rl.close(); // 不关的话进程会一直等输入
}
main();
几个容易忽略的地方:逐行读取的输入源出错时,for await 不一定能接住底层文件流的错误,最好单独给文件流监听 error;交互脚本要考虑被管道或 CI 调用的情况,先看 process.stdin.isTTY;输入密码这类敏感信息时,readline 默认会回显,需要自己处理静音输出或者用 @inquirer/prompts 这类库。
面试官追问
追问 1一个 5GB 的日志文件要统计 ERROR 行数,怎么写?
createInterface({ input: fs.createReadStream(file), crlfDelay: Infinity }),然后 for await (const line of rl) if (line.includes('ERROR')) n++。内存只跟单行长度有关。
追问 2命令行工具问完问题、打印结果了,进程却不退出,为什么?
readline 接口还开着,一直在等 stdin 的输入。处理完要调 rl.close(),for await 正常读完时会自动关,但 question() 这种交互模式必须自己关。
追问 3同一个脚本在 CI 里跑卡住了,本地终端正常,可能是什么原因?
CI 里 stdin 不是终端,没人输入也没有结束信号,question() 就一直等。先判断 process.stdin.isTTY,非交互环境改从参数或环境变量读,或者直接给默认值。
追问 4用户在提问时按了 Ctrl+C,怎么优雅地退出?
rl.on('SIGINT', () => { rl.close(); process.exitCode = 130; }),在回调里做清理。没有这个监听的话,按下 Ctrl+C 后输入流会被暂停,行为不一定是你想要的。
追问 5导入一个文件,有一行有几百 MB 长,readline 扛得住吗?
扛不住,readline 会把整行攒在内存里才吐出来。不可信的输入要先限制单行长度,或者改用自己写的 Transform 在超长时直接报错。
# 模块概览
30 秒速记
readline= 把一个可读流按「行」切好喂给你,输入源可以是键盘也可以是文件- 三类典型用途:日志逐行分析 /
Tab自动补全 /npm init那种问答式脚手架 - 核心对象是
readline.createInterface({ input, output }),键盘交互接process.stdin/process.stdout,读文件就接fs.createReadStream - 用完记得
rl.close(),不关的话stdin一直挂着,进程不退出 - 新版本有
readline/promises(Node 17+),await rl.question()写问答比回调顺手
readline 就是 Node.js 自带的「按行读输入」工具,输入从哪来它不管,键盘、文件、管道都行。 你可以把它理解成一个切行器:底下是一条字节流,它帮你按换行符切好,每切出一行就通知你一次。所以同一个模块既能做日志分析,一行一行处理大文件不占内存;也能做命令行交互,比如问一句答一句的脚手架、按 Tab 出提示。小工具我一般直接用它,问题多、要选择列表了才上 inquirer 这类库。
readline是个非常实用的模块。如名字所示,主要用来实现逐行读取,比如读取用户输入,或者读取文件内容。常见使用场景有下面几种,本文会逐一举例说明。
- 文件逐行读取:比如说进行日志分析。
- 自动完成:比如输入npm,自动提示"help init install"。
- 命令行工具:比如npm init这种问答式的脚手架工具。
面试官追问
追问 1readline 这名字听着像读东西的,是不是只能用来读文件?
不是。它只认「输入流」,键盘对应 process.stdin,文件对应 fs.createReadStream(),管道进来的也是流。它真正的重点是「按行」处理。
追问 2脚本问完问题、打印完结果,终端还卡着不退出,为什么?
大概率没调 rl.close()。接口开着就会一直监听 stdin,事件循环里还有活,进程自然不退。问答流程结束的地方补一句 rl.close() 就好。
追问 3这个脚手架放到 CI 里跑就一直挂着,怎么办?
CI 没有人在终端前回答问题,question 会一直等。脚手架要支持参数或配置文件跳过问答,比如 --yes 全用默认值,或者检测 process.stdin.isTTY 为假时直接走非交互模式。
追问 4读几个 G 的日志,用 fs.readFileSync 再 split('\n') 行不行?
文件小可以,几个 G 不行,一次全读进内存可能直接 OOM,字符串长度也有上限。用 readline 接文件流,读一行处理一行,内存占用基本恒定。
追问 5问答式工具什么时候该换成 inquirer 这种库?
只有几个文本问题时 readline 够用、零依赖。要单选多选、方向键选择、输入校验和重试,自己写就是在重造轮子,直接上库更省事。
# 基础例子
30 秒速记
- 套路三步:
createInterface建接口 →rl.question(提示, 回调)拿一行输入 → 回调里处理完rl.close() input接process.stdin、output接process.stdout,提示语写到output上question是异步的:用户敲回车才进回调,拿到的是整行字符串,不做任何校验- 空输入是
''
