导航菜单

  • 1.claudecode
  • 2.claudecode
  • readline
  • iconv-lite
  • 1. readline 是做什么的
  • 2. 创建接口 createInterface
  • 3. 单次提问 rl.question
  • 4. 接口关闭与 close 事件
  • 5. 连续多问
  • 6. 命令行循环
  • 7. 将 question 封装成 Promise
  • 8. 从文件按行读取
  • 9. for await...of 逐行读取
  • 10. SIGINT 事件
  • 11. 常见陷阱
    • 11.1 问完不关:question 后忘记 rl.close()
    • 11.2 不要把 question 和 on('line') 混在同一流程里
    • 11.3 读文件时请设 terminal: false
  • 12. 和第三方库怎么选
  • 13. 知识点速查

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)
← 上一节 iconv-lite
下一节 没有下一节 →

访问验证

请输入访问令牌

Token不正确,请重新输入