Node.js readline 全景指南:createInterface 配置、逐行读取与交互模式
Node.js 的 readline 模块用于从可读流中逐行读取数据,也能为终端程序提供提示符、输入编辑、历史记录和自动补全等交互能力。它既可以处理 process.stdin,也可以读取文件、网络连接和子进程输出。
使用 readline 时,需要先区分两套接口:node:readline 提供事件和回调 API,node:readline/promises 为 question() 提供 Promise API。除此之外,rl.write() 不是普通的输出方法,rl.close() 关闭的也只是 readline 接口,而不是底层输入流。
本文使用现代 Node.js 的 ESM 语法,重点说明:
createInterface()如何连接输入流与输出流;ReadLineOptions中常用配置的准确含义;question()、'line'事件和异步迭代器的区别;- 交互式 CLI、文件和管道分别应该如何配置;
write()、prompt()和close()容易混淆的行为。
一、readline 解决什么问题
Node.js 中的标准输入、文件和网络连接都可以表现为可读流。可读流提供的是连续数据块,而业务代码通常希望按行处理文本。
readline 位于这两者之间:
text
Readable 输入流
│
│ 字节或字符串数据块
▼
readline.Interface
│
│ 识别 \n、\r\n 等换行边界
▼
一行一行的字符串
创建接口时,还可以提供一个可写流:
text
Readable input ──► readline.Interface ──► Writable output
│
├── question()
├── 'line' 事件
├── for await...of
└── prompt() / 终端输入编辑
其中:
input决定从哪里读取;output决定提示符和终端编辑内容写到哪里;terminal决定是否启用终端交互行为;Interface负责识别行边界并提供上层 API。
readline 既能用于交互式 CLI,也能用于非交互式文本处理,但这两类场景的配置并不完全相同。
二、先选择正确的导入入口
Node.js 提供两套 readline API。
1. 回调与事件 API
从 node:readline 导入:
js
import { createInterface } from 'node:readline';
适合使用:
rl.question(query, callback);rl.on('line', callback);rl.prompt();for await...of。
其中 question() 通过回调返回回答:
js
import { createInterface } from 'node:readline';
import { stdin as input, stdout as output } from 'node:process';
const rl = createInterface({ input, output });
rl.question('请输入姓名:', (name) => {
console.log(`你好,${name}`);
rl.close();
});
2. Promise API
从 node:readline/promises 导入:
js
import { createInterface } from 'node:readline/promises';
此时 question() 返回 Promise<string>:
js
import { createInterface } from 'node:readline/promises';
import { stdin as input, stdout as output } from 'node:process';
const rl = createInterface({ input, output });
try {
const name = await rl.question('请输入姓名:');
console.log(`你好,${name}`);
} finally {
rl.close();
}
如果程序使用 async/await 实现连续问答,应优先选择 node:readline/promises。
下面的写法不能得到回答:
js
import { createInterface } from 'node:readline';
const rl = createInterface({
input: process.stdin,
output: process.stdout,
});
// 错误:传统 readline 的 question() 不返回回答 Promise
const answer = await rl.question('请输入内容:');
问题不在 await,而在于导入的是回调版本。
三、使用 createInterface 创建接口
createInterface() 接收一个配置对象并返回 readline.Interface:
js
const rl = createInterface({
input: process.stdin,
output: process.stdout,
});
常用配置可以概括为:
| 字段 | 作用 | 默认值或常用值 |
|---|---|---|
input |
要监听的可读流 | process.stdin、文件流、socket |
output |
提示符和终端内容的输出位置 | 交互程序常用 process.stdout |
terminal |
是否启用 TTY 交互行为 | 未指定时根据 output.isTTY 判断 |
crlfDelay |
\r 与后续 \n 合并为一个换行的最大间隔 |
默认 100 毫秒;文件常用 Infinity |
prompt |
rl.prompt() 使用的默认提示符 |
'> ' |
history |
初始输入历史 | [] |
historySize |
最多保留的历史记录数量 | 30 |
removeHistoryDuplicates |
是否移除重复历史记录 | false |
下面分别解释这些配置。
四、input:从哪里读取数据
input 是 readline 监听的可读流,也是创建接口时最核心的配置。
1. 读取键盘输入
js
const rl = createInterface({
input: process.stdin,
output: process.stdout,
});
在终端中运行时,process.stdin 通常连接到用户键盘。
但 process.stdin 不一定来自键盘。例如:
sh
printf "first\nsecond\n" | node app.mjs
此时 process.stdin 来自管道。因此,process.stdin 只表示当前进程的标准输入,不能无条件等同于交互式键盘。
2. 读取文件
js
import { createReadStream } from 'node:fs';
import { createInterface } from 'node:readline';
const input = createReadStream('./data.txt', {
encoding: 'utf8',
});
const rl = createInterface({
input,
crlfDelay: Infinity,
});
文件流负责读取文件,readline 负责把文件内容拆成一行一行的字符串。
3. 读取网络连接
net.Socket 同时实现了可读流和可写流接口,因此也可以交给 readline:
js
import { createInterface } from 'node:readline';
const rl = createInterface({
input: socket,
output: socket,
terminal: false,
});
这适合处理以换行符分隔的简单文本协议。这里需要明确设置 terminal: false,因为网络 socket 不是供用户编辑命令的终端。
4. 读取子进程输出
js
import { spawn } from 'node:child_process';
import { createInterface } from 'node:readline';
const child = spawn('node', ['worker.mjs'], {
stdio: ['ignore', 'pipe', 'inherit'],
});
const rl = createInterface({
input: child.stdout,
terminal: false,
});
rl.on('line', (line) => {
console.log('子进程输出:', line);
});
这类场景中,input 来自子进程的标准输出,同样不需要终端编辑能力。
五、output:提示符和终端编辑内容写到哪里
output 是 readline 使用的可写流:
js
const rl = createInterface({
input: process.stdin,
output: process.stdout,
});
在交互式终端中,readline 会通过 output:
- 输出
question()的问询文字; - 输出
prompt()的默认提示符; - 回显用户输入;
- 重绘当前编辑行;
- 移动光标;
- 展示历史输入和自动补全内容。
因此,output 不只是普通的日志输出位置,它还参与 readline 的终端编辑过程。
非交互场景可以省略 output
读取文件时通常不需要提示符:
js
const rl = createInterface({
input: fileStream,
terminal: false,
});
Node.js 运行时可以接受 output: null,但 TypeScript 类型定义通常将 output 声明为可选的 Writable。在严格类型检查下,显式传入 null 可能无法通过类型检查。
因此,更稳妥的写法是直接省略:
js
const rl = createInterface({
input: fileStream,
terminal: false,
});
如果没有配置 output,question() 仍可以等待输入,但问询文字不会显示。交互式程序通常不应该这样使用。
六、terminal:是否启用终端交互能力
terminal 决定 readline 是否把输入输出当作 TTY 终端处理。
js
const rl = createInterface({
input: process.stdin,
output: process.stdout,
terminal: true,
});
terminal: true
终端模式会启用:
- 左右方向键移动光标;
- 上下方向键浏览历史记录;
- Backspace 和 Delete 编辑;
- Ctrl+C、Ctrl+D 等终端按键处理;
- Tab 自动补全;
- 当前输入行的清除和重绘;
- 必要的终端控制序列。
它适合用户直接操作的交互式 CLI。
terminal: false
非终端模式只把输入视为普通文本流,重点是识别换行并产生一行一行的字符串:
js
const rl = createInterface({
input: fileStream,
terminal: false,
});
适合:
- 文件;
- 管道;
- 输入重定向;
- 子进程输出;
- 网络文本协议;
- 自动化测试。
默认值不是固定的 true
如果没有显式传入 terminal,Node.js 会根据 output.isTTY 判断:
js
terminal = Boolean(output?.isTTY);
典型结果如下:
| output 状态 | 推导出的 terminal |
|---|---|
process.stdout 连接真实终端 |
true |
process.stdout 被重定向到文件 |
false |
| 普通文件输出流 | false |
没有提供 output |
false |
判断依据主要是 output.isTTY,不是 input.isTTY。
处理文件、管道和 socket 时,建议显式设置:
js
terminal: false
不要对文件或网络输出流强制设置 terminal: true。终端模式可能向 output 写入光标移动和输入行重绘所需的控制序列,这些内容不应该出现在普通文件或网络协议中。
七、crlfDelay:正确处理 Windows 换行
常见文本换行符包括:
| 换行格式 | 字符 | 常见环境 |
|---|---|---|
| LF | \n |
Linux、现代 macOS |
| CRLF | \r\n |
Windows |
| CR | \r |
部分旧式文本格式 |
流中的数据不是按"完整行"到达的,而是以数据块到达。一个 \r\n 可能被拆到两个数据块中:
text
数据块 1:hello\r
数据块 2:\nworld\r\n
readline 收到 \r 后,会等待后续的 \n。如果 \n 在 crlfDelay 指定的时间内到达,二者会被视为同一个换行符。
js
const rl = createInterface({
input,
crlfDelay: 100,
});
需要注意:
- 默认值是
100毫秒; - 小于
100的有效值会按100处理; - 它不是读取一整行的超时时间;
- 它只控制相邻
\r和\n的组合关系。
文件读取通常使用 Infinity
读取文件时通常使用:
js
const rl = createInterface({
input: fileStream,
crlfDelay: Infinity,
});
Infinity 能避免 \r 和 \n 因数据块边界或处理延迟而被识别成两个换行。这也是 Node.js 官方文件逐行读取示例中的常见配置。
八、prompt:设置默认提示符
prompt 是 rl.prompt() 使用的默认提示符:
js
const rl = createInterface({
input: process.stdin,
output: process.stdout,
prompt: 'command> ',
});
调用:
js
rl.prompt();
终端显示:
text
command>
默认提示符是 '> ',末尾包含一个空格。
创建接口后还可以修改提示符:
js
rl.setPrompt('next> ');
rl.prompt();
读取当前提示符:
js
console.log(rl.getPrompt());
prompt 配置只影响 rl.prompt()。它不会替换传给 rl.question() 的问询文字:
js
const answer = await rl.question('请输入项目名称:');
这里显示的是"请输入项目名称:",不是默认的 prompt。
九、history:提供初始输入历史
history 用于设置初始输入历史:
js
const rl = createInterface({
input: process.stdin,
output: process.stdout,
history: ['version', 'status', 'help'],
});
在终端模式中,用户可以使用上下方向键浏览这些记录。默认值是空数组:
js
[]
该配置主要在 terminal: true 时有意义。对于文件、管道和 socket,通常不需要历史记录。
运行期间可以通过 rl.history 查看当前记录:
js
console.log(rl.history);
新提交的输入通常会出现在数组前部。
historySize
historySize 限制最多保留多少条历史记录:
js
const rl = createInterface({
input: process.stdin,
output: process.stdout,
historySize: 100,
});
默认值是 30。设置为 0 可以禁用历史记录:
js
historySize: 0
removeHistoryDuplicates
如果不希望历史记录中重复保存同一条命令,可以设置:
js
const rl = createInterface({
input: process.stdin,
output: process.stdout,
removeHistoryDuplicates: true,
});
默认值是 false。
这三个配置的职责分别是:
| 配置 | 职责 |
|---|---|
history |
提供接口创建时的初始历史 |
historySize |
限制历史记录数量 |
removeHistoryDuplicates |
控制是否移除重复项 |
十、rl.question():读取一次回答
question() 适合"一问一答"流程:
text
显示问题
→ 等待用户输入一行
→ 把该行作为回答
1. Promise 版本
从 node:readline/promises 导入:
js
import { createInterface } from 'node:readline/promises';
import { stdin as input, stdout as output } from 'node:process';
const rl = createInterface({ input, output });
try {
const projectName = await rl.question('项目名称:');
const port = await rl.question('监听端口:');
console.log({
projectName,
port: Number(port),
});
} finally {
rl.close();
}
question() 的返回类型是 Promise<string>。返回的字符串不包含用户按下回车时产生的换行符。
question() 不会自动关闭接口。处理完成后应调用 rl.close(),并推荐使用 finally 保证异常情况下也能清理资源。
2. 回调版本
从 node:readline 导入:
js
import { createInterface } from 'node:readline';
import { stdin as input, stdout as output } from 'node:process';
const rl = createInterface({ input, output });
rl.question('项目名称:', (projectName) => {
console.log(`准备创建项目:${projectName}`);
rl.close();
});
传统版本的返回值是 undefined,回答通过回调参数传入。
3. 使用 AbortSignal 取消等待
Promise 版本可以通过 AbortSignal 取消等待:
js
import { createInterface } from 'node:readline/promises';
import { stdin as input, stdout as output } from 'node:process';
const rl = createInterface({ input, output });
const controller = new AbortController();
const timer = setTimeout(() => {
controller.abort();
}, 10_000);
try {
const answer = await rl.question('请在 10 秒内确认(y/n):', {
signal: controller.signal,
});
console.log(`输入结果:${answer}`);
} catch (error) {
if (error.name === 'AbortError') {
console.log('等待输入超时');
} else {
throw error;
}
} finally {
clearTimeout(timer);
rl.close();
}
取消等待会让 Promise 版本的 question() 拒绝,并产生 AbortError。它不会自动替程序决定是否继续使用或关闭整个 readline 接口。
4. 不要并发调用多个 question
下面的代码同时发起两个问题,但同一个终端无法清晰表达两个并发等待状态:
js
const [name, port] = await Promise.all([
rl.question('项目名称:'),
rl.question('监听端口:'),
]);
交互式问答应按顺序执行:
js
const name = await rl.question('项目名称:');
const port = await rl.question('监听端口:');
十一、rl.write():向 readline 注入输入
rl.write(data[, key]) 容易被误解为普通输出方法。它不是下面方法的替代品:
js
process.stdout.write(data);
rl.write() 会把字符串或按键交给 readline 处理,相当于以编程方式向当前输入行注入内容。
js
rl.prompt();
rl.write('status');
此时当前编辑行会变成:
text
> status
但 status 还没有提交。可以继续注入一个回车按键:
js
rl.write(null, {
name: 'return',
});
如果传入了 key,readline 按按键对象处理,data 不再是主要输入内容。常用按键字段包括:
ts
{
name?: string;
ctrl?: boolean;
meta?: boolean;
shift?: boolean;
}
例如模拟 Ctrl+U:
js
rl.write(null, {
ctrl: true,
name: 'u',
});
具体按键行为取决于终端模式和目标终端的能力。
非终端模式下的 write
在 terminal: false 时,rl.write() 会把内容交给普通行解析逻辑:
js
import { createInterface } from 'node:readline';
import { PassThrough } from 'node:stream';
const input = new PassThrough();
const rl = createInterface({
input,
terminal: false,
});
rl.on('line', (line) => {
console.log('读取到:', line);
});
rl.write('first\nsecond\n');
会依次处理:
text
first
second
只想打印文字时应该怎么做
如果只是向终端输出消息,应使用:
js
process.stdout.write('正在处理......');
也可以使用当前接口关联的输出流:
js
rl.output?.write('正在处理......');
这类输出不会被当作用户输入交给 readline 解析。
十二、rl.prompt():显示或重绘默认提示符
rl.prompt() 会输出或重绘通过 prompt 配置的默认提示符:
js
const rl = createInterface({
input: process.stdin,
output: process.stdout,
prompt: 'command> ',
});
rl.prompt();
如果 readline 当前处于暂停状态,prompt() 还会恢复输入。
需要注意,prompt():
- 不会等待并返回一行输入;
- 不返回用户输入字符串;
- 不等同于
question(); - 通常与
'line'事件组合使用。
典型流程如下:
js
rl.prompt();
rl.on('line', (line) => {
handleCommand(line);
rl.prompt();
});
每处理完一条命令,再次显示提示符。
十三、rl.close():关闭接口而不是销毁底层流
调用:
js
rl.close();
会关闭 readline.Interface,停止 readline 继续控制输入输出,并触发 'close' 事件。
它主要会:
- 暂停输入;
- 停止 readline 继续解析数据;
- 在终端模式下恢复相关终端状态;
- 释放 readline 对输入输出流的控制;
- 触发
'close'事件。
但它不会自动:
- 调用
input.destroy(); - 销毁网络 socket;
- 关闭文件描述符;
- 调用
output.end(); - 永久关闭
process.stdin。
因此,下面两种操作的职责不同:
js
rl.close(); // 关闭 readline 接口
socket.destroy(); // 销毁网络连接
如果读取文件时需要提前终止,可以同时关闭接口和文件流:
js
rl.close();
fileStream.destroy();
即使使用 node:readline/promises,rl.close() 仍然是同步方法,不需要写成 await rl.close()。
监听 close 事件
js
rl.on('close', () => {
console.log('readline 接口已关闭');
});
'close' 事件表示 readline 接口生命周期结束,不一定表示底层输入流已经被销毁。
十四、使用 line 事件持续读取
'line' 事件适合持续处理输入:
js
rl.on('line', (line) => {
console.log(line);
});
每识别到一个换行边界,readline 就会触发一次回调。传入的 line:
- 是字符串;
- 不包含结尾的
\n; - 不包含结尾的
\r\n; - 空行对应空字符串;
- 输入结束时,最后一行即使没有换行符,也会作为一行被处理。
例如输入:
text
alpha
beta
gamma
回调依次收到:
js
'alpha'
'beta'
''
'gamma'
逐行读取文件
js
import { createReadStream } from 'node:fs';
import { createInterface } from 'node:readline';
const input = createReadStream('./data.txt', {
encoding: 'utf8',
});
const rl = createInterface({
input,
crlfDelay: Infinity,
terminal: false,
});
let lineNumber = 0;
rl.on('line', (line) => {
lineNumber += 1;
console.log(`${lineNumber}: ${line}`);
});
rl.on('close', () => {
console.log(`读取完成,共 ${lineNumber} 行`);
});
rl.on('error', (error) => {
console.error('读取失败:', error);
});
当文件到达 EOF 时,readline 会结束读取并关闭接口。
line 事件适合什么场景
事件模式适合:
- 持续监听标准输入;
- 实现 REPL 或命令处理器;
- 逐行处理文件;
- 逐行消费子进程输出;
- 需要同时监听
'close'、'pause'等事件; - 对逐行处理开销比较敏感的场景。
如果每一行都需要等待异步任务完成,异步迭代器通常更容易控制执行顺序。
十五、使用异步迭代器逐行读取
readline.Interface 实现了异步迭代协议,因此可以直接使用:
js
for await (const line of rl) {
console.log(line);
}
1. 使用 for await...of
js
import { createReadStream } from 'node:fs';
import { createInterface } from 'node:readline';
const input = createReadStream('./data.txt', {
encoding: 'utf8',
});
const rl = createInterface({
input,
crlfDelay: Infinity,
terminal: false,
});
try {
for await (const line of rl) {
console.log('当前行:', line);
}
} finally {
rl.close();
}
每次循环得到一个不包含行尾换行符的字符串。
2. 顺序执行异步任务
异步迭代器适合每行都需要等待异步处理完成的场景:
js
for await (const line of rl) {
await saveToDatabase(line);
}
执行顺序是:
text
取得一行
→ 等待该行处理完成
→ 进入下一次循环
这比在 'line' 回调中直接使用异步函数更容易表达顺序关系。
下面的事件写法并不会等待上一行处理完:
js
rl.on('line', async (line) => {
await saveToDatabase(line);
});
如果输入到达速度快于数据库写入速度,多次回调可能同时处于等待状态。是否允许这种并发,应该由业务需求决定,不能仅凭语法判断。
3. 手动调用 next()
也可以显式取得异步迭代器:
js
const iterator = rl[Symbol.asyncIterator]();
逐次获取一行:
js
const first = await iterator.next();
if (!first.done) {
console.log('第一行:', first.value);
}
next() 返回 Promise<IteratorResult<string>>。读取到一行时:
js
{
value: '当前行内容',
done: false,
}
输入结束时:
js
{
value: undefined,
done: true,
}
手动调用 next() 适合只读取固定数量行,或者需要由其他状态控制读取时机的场景。
4. 提前退出时显式关闭接口
处理 process.stdin 时,如果循环提前 break,应使用 finally 关闭接口:
js
const rl = createInterface({
input: process.stdin,
});
try {
for await (const line of rl) {
if (line.trim() === 'exit') {
break;
}
console.log(`收到:${line}`);
}
} finally {
rl.close();
}
这能保证 readline 不再占用标准输入,并清理终端状态和监听关系。
5. 创建接口后应及时开始消费
createInterface() 创建接口后,readline 就会开始监听并恢复输入流。不要在创建接口和开始异步迭代之间执行耗时异步操作:
js
const rl = createInterface({
input: fastInputStream,
});
// 不推荐:输入流可能已经开始产生数据
await initializeApplication();
for await (const line of rl) {
// 处理输入
}
更稳妥的顺序是先完成初始化,再创建接口:
js
await initializeApplication();
const rl = createInterface({
input: fastInputStream,
});
for await (const line of rl) {
// 处理输入
}
十六、三种输入消费模式如何选择
readline 主要有三种消费方式:
| 模式 | 获取输入的方式 | 适合场景 |
|---|---|---|
question() |
获取一次回答 | 安装向导、确认流程、固定步骤问答 |
'line' 事件 |
每行触发回调 | REPL、持续命令输入、高频逐行处理 |
for await...of |
异步顺序迭代 | 文件处理、每行需要等待异步任务 |
固定数量的问题:使用 question
js
const name = await rl.question('姓名:');
const email = await rl.question('邮箱:');
它的优势是控制流直观,每一步都能等待上一项输入完成。
持续输入命令:使用 line 事件
js
rl.on('line', (line) => {
executeCommand(line);
rl.prompt();
});
它更符合事件驱动的 CLI 和 REPL 模型。
每行都需要异步处理:使用 for await
js
for await (const line of rl) {
await processLine(line);
}
它能够直接表达顺序等待。
不要随意混用消费模式
同一个 readline 接口最好确定一种主要的输入消费方式。
例如,question() 正在等待时,下一行输入会被它消费为回答,而不是按普通方式交给其他 'line' 监听逻辑。再叠加异步迭代器后,输入由谁消费会变得更难判断。
可以在不同生命周期阶段切换设计,但应明确状态边界,不应让多套消费者同时等待同一输入流。
十七、完整示例:实现一个交互式 CLI
下面使用 'line' 事件实现一个简单的命令行:
js
import { createInterface } from 'node:readline';
import { stdin as input, stdout as output } from 'node:process';
const rl = createInterface({
input,
output,
prompt: 'node-cli> ',
historySize: 50,
removeHistoryDuplicates: true,
});
function printHelp() {
console.log(`
可用命令:
help 显示帮助
version 显示版本
exit 退出程序
`.trim());
}
rl.prompt();
rl.on('line', (line) => {
const command = line.trim();
switch (command) {
case '':
break;
case 'help':
printHelp();
break;
case 'version':
console.log(process.version);
break;
case 'exit':
rl.close();
return;
default:
console.log(`未知命令:${command}`);
}
rl.prompt();
});
rl.on('close', () => {
console.log('命令行已退出');
});
执行效果中的 Node.js 版本取决于本地环境:
text
node-cli> help
可用命令:
help 显示帮助
version 显示版本
exit 退出程序
node-cli> version
v20.x.x
node-cli> exit
命令行已退出
这个示例中各部分职责明确:
prompt定义默认提示符;rl.prompt()显示提示符;'line'事件接收提交后的命令;- 每次命令处理完成后重新调用
prompt(); exit调用rl.close()结束接口;'close'事件执行退出提示。
十八、完整示例:逐行统计文件内容
下面使用异步迭代器统计文件中的总行数、空行数和非空白字符数:
js
import { createReadStream } from 'node:fs';
import { createInterface } from 'node:readline';
const input = createReadStream('./data.txt', {
encoding: 'utf8',
});
const rl = createInterface({
input,
crlfDelay: Infinity,
terminal: false,
});
let totalLines = 0;
let emptyLines = 0;
let nonWhitespaceCharacters = 0;
try {
for await (const line of rl) {
totalLines += 1;
if (line.trim() === '') {
emptyLines += 1;
}
nonWhitespaceCharacters += line.replaceAll(/\s/g, '').length;
}
console.log({
totalLines,
emptyLines,
nonWhitespaceCharacters,
});
} catch (error) {
console.error('文件处理失败:', error);
process.exitCode = 1;
} finally {
rl.close();
}
这个场景不需要:
output;- 提示符;
- 历史记录;
- 终端编辑能力。
因此配置中只保留输入流、换行处理和明确的非终端模式。
十九、常见误区
1. 认为所有 question 都返回 Promise
错误写法:
js
import { createInterface } from 'node:readline';
const answer = await rl.question('请输入:');
正确做法是从 Promise 模块导入:
js
import { createInterface } from 'node:readline/promises';
2. 使用 rl.write 打印普通消息
错误理解:
js
rl.write('处理完成');
这会把内容交给 readline,当作输入内容处理。普通输出应使用:
js
process.stdout.write('处理完成');
3. 认为 close 会销毁 input
js
rl.close();
这只会关闭 readline 接口。需要销毁 socket 或提前终止文件流时,应单独操作底层流。
4. 把 terminal 默认值写成 true
terminal 未指定时会根据 output.isTTY 判断。输出重定向、管道和文件环境下,结果可能是 false。
5. 对文件流启用终端模式
js
const rl = createInterface({
input: fileStream,
output: anotherFileStream,
terminal: true,
});
这可能把终端控制行为引入普通文件处理。文件和管道通常应设置:
js
terminal: false
6. 忘记关闭交互式接口
使用 process.stdin 的程序如果一直保留 readline 接口,Node.js 进程可能继续等待输入。
Promise 问答推荐使用:
js
try {
// 读取输入
} finally {
rl.close();
}
7. 在同一接口上同时注册多个消费者
同时使用 question()、'line' 事件和异步迭代器,会让下一行输入的归属难以判断。应为一个接口选择一种主要消费模式。
二十、API 速查表
createInterface 配置
| 字段 | 准确含义 | 推荐用法 |
|---|---|---|
input |
被监听的可读流 | 必需 |
output |
提示符和终端编辑输出流 | 交互式 CLI 使用 process.stdout |
terminal |
是否启用 TTY 行编辑 | CLI 自动判断;文件和管道显式设为 false |
crlfDelay |
合并 \r 与后续 \n 的时间范围 |
文件使用 Infinity |
prompt |
rl.prompt() 使用的提示符 |
默认 '> ' |
history |
初始历史记录 | 仅终端模式需要 |
historySize |
最大历史记录数 | 默认 30 |
removeHistoryDuplicates |
是否移除重复历史 | 默认 false |
Interface 常用 API
| API | 作用 |
|---|---|
rl.question(query, callback) |
回调方式读取一次回答 |
await rl.question(query) |
Promise 方式读取一次回答 |
rl.write(data[, key]) |
向 readline 注入文本或按键输入 |
rl.prompt() |
输出或重绘默认提示符 |
rl.setPrompt(value) |
修改默认提示符 |
rl.getPrompt() |
获取当前默认提示符 |
rl.close() |
关闭 readline 接口 |
rl.on('line', callback) |
每读取一行触发一次 |
rl.on('close', callback) |
接口关闭时触发 |
rl[Symbol.asyncIterator]() |
获取逐行读取的异步迭代器 |
总结
readline 的基础模型由输入流、Interface 和可选的输出流组成。input 提供连续数据,Interface 负责识别行边界,output 则为交互式终端提供提示符、回显和输入行重绘。
交互式 CLI 通常使用 process.stdin、process.stdout 和终端模式;文件、管道、socket 和子进程输出通常使用 terminal: false,读取文件时可以设置 crlfDelay: Infinity。
选择上层 API 时,可以依据输入流程判断:
- 固定步骤的一问一答使用 Promise 版
question(); - 持续命令输入使用
'line'事件; - 每行需要顺序等待异步处理时使用
for await...of。
最后需要区分两个容易混淆的行为:rl.write() 会向 readline 注入输入,而不是简单打印文本;rl.close() 只关闭 readline 接口,不负责销毁底层文件流、标准输入或网络连接。