1. readline 是做什么的 #
readline 用来按行从可读流里读数据(最常见是用户键盘输入),并在交互式终端里尽量帮你处理好「一行」的边界(用户按回车算一行)。如果你不用它、自己去监听 process.stdin 的 data 事件,往往会碰到:一次收到半个汉字、要自己找换行符、退格键变成乱码等问题。对新手来说,做命令行交互时优先用 readline 更省心。
下面用一张表对比「自己读流」和「用 readline」的直观差别。
| 场景 | 自己监听 process.stdin |
使用 readline |
|---|---|---|
| 想读「完整一行」 | 要自己拼缓冲区、判断 \n |
用 question 或 line 事件即可 |
| 退格、方向键 | 可能收到原始控制字符 | 在真实终端里一般由 readline 帮你处理 |
一句话:业务上你只关心「用户这一行说了什么」时,用 readline 更合适。
2. 创建接口 createInterface #
使用 readline 的第一步几乎都是:调用 readline.createInterface(配置对象),得到一个对象(下面记作 rl)。配置里至少要指定 input,从哪里来读;如果要在终端里用 question() 显示问题,通常还要指定 output: process.stdout。
下面代运行后终端会停住等待输入,你输入任意内容后按回车,会看到程序打印你输入的内容并退出。
// 加载 Node 内置的 readline 模块
const readline = require('readline');
// 创建一个「行读取」接口:从键盘读、往屏幕写
const rl = readline.createInterface({
// 指定从标准输入读取用户按键
input: process.stdin,
// 指定把提示等输出写到标准输出,终端才能看见
output: process.stdout,
});
// 监听用户按下回车后产生的一整行文本
rl.on('line', (line) => {
// 把去掉末尾换行后的一行内容打印出来
console.log('你输入的是:', line);
// 关闭接口,否则进程可能一直不结束
rl.close();
});3. 单次提问 rl.question #
rl.question(提示文字, 回调函数) 会在终端打印提示,等用户输入一行并按回车后,把去掉换行符的字符串传给回调。注意:它不会自动关闭接口,若你不 close(),Node 进程往往会一直挂着。
下面示例运行后按提示输入名字即可。
// 引入 readline
const readline = require('readline');
// 创建接口(提问必须同时有 input 和 output)
const rl = readline.createInterface({
// 从键盘读
input: process.stdin,
// 问题文字会写到这里
output: process.stdout,
});
// 向用户提问;第二个参数是用户按回车后的回调
rl.question('你叫什么名字?', (answer) => {
// 模板字符串里使用用户输入
console.log(`你好,${answer}!`);
// 问完就关闭,否则程序不会正常结束
rl.close();
});小结: 脚本里「问一句、答一句、然后结束」——用 question + 在回调里 close() 就够。
4. 接口关闭与 close 事件 #
调用 rl.close() 后:不会再触发新的 line;会触发一次 'close' 事件。适合在这里做收尾,例如打印再见、调用 process.exit(0) 明确退出
// 引入 readline
const readline = require('readline');
// 创建接口
const rl = readline.createInterface({
// 标准输入
input: process.stdin,
// 标准输出
output: process.stdout,
});
// 在关闭时做清理
rl.on('close', () => {
// 友好提示
console.log('接口已关闭,程序即将退出。');
// 以状态码 0 正常退出当前 Node 进程
process.exit(0);
});
// 提问
rl.question('按回车结束:', () => {
// 主动关闭,从而触发上面的 close 事件
rl.close();
});5. 连续多问 #
多个问题可以在回调里再嵌套下一个 question,注意最后一个问题的回答里要 rl.close()。
// 引入 readline
const readline = require('readline');
// 创建接口
const rl = readline.createInterface({
// 键盘输入
input: process.stdin,
// 屏幕输出
output: process.stdout,
});
// 先问第一个问题
rl.question('你叫什么?', (name) => {
// 在第一个答案的基础上问第二个
rl.question('你来自哪个城市?', (city) => {
// 两个答案都有了,一次性输出
console.log(`${name},欢迎来自 ${city} 的朋友!`);
// 全部问完必须关闭
rl.close();
});
});6. 命令行循环 #
当你需要反复出现提示符(例如 >),用户每次输入一行你处理一次,直到用户输入 exit
这种模式用 setPrompt + prompt + on('line') 很合适。
注意:不要在同一段逻辑里又把 question() 和 'line' 混着用,容易乱。
下面可尝试输入 hello、exit。
// 引入 readline
const readline = require('readline');
// 创建接口,并设置默认提示前缀(也可用 setPrompt 单独设)
const rl = readline.createInterface({
// 标准输入
input: process.stdin,
// 标准输出
output: process.stdout,
// 每次 prompt 时显示的前缀
prompt: 'demo> ',
});
// 启动时先显示一次提示符
rl.prompt();
// 每次用户按回车触发
rl.on('line', (line) => {
// 去掉首尾空白,避免空格误触
const cmd = line.trim();
// 用户想退出
if (cmd === 'exit') {
// 关闭接口
rl.close();
// 提前返回,下面不再执行
return;
}
// 简单 echo
if (cmd === 'hello') {
// 打个招呼
console.log('world');
} else if (cmd !== '') {
// 非空且未识别的命令
console.log('未知命令,试试 hello 或 exit');
}
// 处理完一行后再显示提示符,形成循环
rl.prompt();
});
// 关闭时退出进程
rl.on('close', () => {
// 再见信息
console.log('再见!');
// 正常退出
process.exit(0);
});7. 将 question 封装成 Promise #
回调嵌套多了不好读。可以把 question 包成返回 Promise 的函数,再用 async 函数里顺序 await。
需要 Node 支持顶层 await 时可以把主逻辑放在异步 IIFE 里。
// 引入 readline
const readline = require('readline');
// 创建全局接口(小脚本里这样写最直观)
const rl = readline.createInterface({
// 标准输入
input: process.stdin,
// 标准输出
output: process.stdout,
});
// 把 question 包装成 Promise,便于 await
function question(query) {
// 返回一个 Promise,在 question 回调里 resolve
return new Promise((resolve) => {
// 调用原生 question,答案交给 resolve
rl.question(query, resolve);
});
}
// 用立即执行的异步函数作为程序入口
(async function main() {
// 等待用户回答第一个问题
const name = await question('名字?');
// 等待第二个问题
const city = await question('城市?');
// 输出结果
console.log(`记录:${name} @ ${city}`);
// 结束必须 close
rl.close();
})();8. 从文件按行读取 #
把 input 换成文件可读流,就按行读文件。读文件时一般设置 terminal: false,避免把文件内容当成终端控制序列处理。
// 文件系统模块
const fs = require('fs');
// 路径拼接
const path = require('path');
// readline 模块
const readline = require('readline');
// 在本脚本同目录生成示例文件路径
const demoFile = path.join(__dirname, 'readline-demo-lines.txt');
// 写入三行示例内容(若文件已存在则覆盖)
fs.writeFileSync(demoFile, '第一行\n第二行\n第三行\n', 'utf8');
// 创建只读文件流
const fileStream = fs.createReadStream(demoFile, { encoding: 'utf8' });
// 用文件流作为 input,按行读取
const rl = readline.createInterface({
// 从文件流读
input: fileStream,
// 读文件不是交互终端,关闭终端特性
terminal: false,
});
// 每读完整一行触发一次
rl.on('line', (line) => {
// 打印行内容
console.log('读到:', line);
});
// 读完后触发(文件流结束)
rl.on('close', () => {
// 提示完成
console.log('文件读完了。');
// 演示文件可删可留,这里删除以免堆积
try {
// 删除临时演示文件
fs.unlinkSync(demoFile);
} catch (e) {
// 忽略删除失败
}
});9. for await...of 逐行读取 #
从较新的 Node 版本起,readline 接口可作为异步迭代器使用。
下面示例从内存里的字符串流读行,不依赖键盘。
注意: 若 input 是 process.stdin,就不要在同一程序里再使用 question(),否则行为容易纠缠不清。
// 从 stream 模块取一个可读工具
const { Readable } = require('stream');
// readline
const readline = require('readline');
// 造一段内存中的「假文件」内容,含换行
const fakeFileContent = 'alpha\nbeta\ngamma\n';
// 把字符串变成可读流
const input = Readable.from([fakeFileContent]);
// 创建按行读取的接口
const rl = readline.createInterface({
// 从内存流读
input,
// 非 TTY
terminal: false,
});
// 异步自执行函数
(async () => {
// 逐行异步迭代
for await (const line of rl) {
// 打印每一行
console.log('行:', line);
}
// 迭代结束
console.log('迭代结束');
})();10. SIGINT 事件 #
在交互终端里,用户按 Ctrl+C 时,Node 的 readline 会发出 'SIGINT' 事件。
下面示例全程只用 question,避免和 on('line') 混用。运行后先输入名字;若在读名字时按 Ctrl+C,会再问一次是否退出。
// readline
const readline = require('readline');
// 创建接口
const rl = readline.createInterface({
// 标准输入
input: process.stdin,
// 标准输出
output: process.stdout,
});
// 用户按 Ctrl+C 时进入这里(具体表现可能因终端略有差异)
rl.on('SIGINT', () => {
// 在 SIGINT 里再次用 question 询问是否退出(不要在同一流程里再叠加 on('line'))
rl.question('\n检测到 Ctrl+C,确定退出吗?(y/n) ', (ans) => {
// 用户输入 y 则退出
if (ans.trim().toLowerCase() === 'y') {
// 关闭接口
rl.close();
} else {
// 不退出则重新问名字,保持示例可继续玩
rl.question('那我们继续——你叫什么名字?', (name) => {
// 打印名字
console.log(`你好,${name}`);
// 正常结束
rl.close();
});
}
});
});
// 关闭时退出进程
rl.on('close', () => {
// 再见
console.log('bye');
// 退出码 0
process.exit(0);
});
// 程序入口:先问名字
rl.question('你叫什么名字?', (name) => {
// 打招呼
console.log(`你好,${name}`);
// 演示完就关
rl.close();
});11. 常见陷阱 #
11.1 问完不关:question 后忘记 rl.close() #
若脚本里只用 question 而从不 close(),进程可能一直等待,看起来像「卡死」。习惯在最后一个回调里 rl.close()。
11.2 不要把 question 和 on('line') 混在同一流程里 #
两种模式都能读行,但混用容易导致重复处理或状态难控。同一小段逻辑里二选一: 要么连续多个 question,要么只用 prompt + line 循环。
11.3 读文件时请设 terminal: false #
若 input 是文件流却仍按终端处理,可能遇到奇怪字符或表现不符合预期。读文件行时设 terminal: false。
12. 和第三方库怎么选 #
日常写复杂 CLI(多选、校验、密码掩码)时,很多人会选 inquirer、prompts 等库。对新手而言,先掌握 readline 的 question 与 close,再学第三方库会更容易。
13. 知识点速查 #
| 主题 | 记住这一句 |
|---|---|
| 用途 | 按行读流,常用于终端问答或按行读文件 |
| 创建 | readline.createInterface({ input, output }) |
| 单次提问 | rl.question('提示', (ans) => { ... rl.close(); }) |
| 循环提示 | rl.setPrompt('> ')、rl.prompt()、rl.on('line', ...) |
| 结束 | rl.close(),并在需要时 process.exit(0) |