Node.js:readline(逐行读取)
readline 用来从可读流里 一次读一行。交互式问句走 process.stdin,读文件则把 fs.createReadStream 接到 input。引入:require('readline') 或 require('node:readline')。
现行还有 node:readline/promises:question 返回 Promise,方便 async、await。下面先按回调版把接口讲完,文末补 Promise 与 for await...of。
简单示例
// readlineDemo.js
const readline = require('readline');
const readlineInst = readline.createInterface({
input: process.stdin,
output: process.stdout
});
readlineInst.question('请输入些什么吧?', (answer) => {
console.log(`输入:${answer}`);
readlineInst.close();
});
运行 node ./readlineDemo.js。创建接口后进程不会自己退出,它在 input 上等数据;处理完后必须 close()(或让 input 走到 EOF)。若 input 是 stdin 又不想一直等用户,可 process.stdin.unref()。
逐行地读取文件流
const readline = require('readline');
const fs = require('fs');
const rl = readline.createInterface({
input: fs.createReadStream('test.txt'),
crlfDelay: Infinity
});
rl.on('line', (line) => {
console.log(`单行:${line}`);
});
crlfDelay: Infinity 表示 \r\n 始终当成 一个 换行,适合读 Windows 文本。
Interface 类
实例由 readline.createInterface() 构造,不要自己 new。每个实例绑一条 可读 input 和一条 可写 output。output 用来打提示符;用户输入从 input 读。
close 事件
监听关闭,回调无参数。下列任一情况会触发:
- 调用了
close(),实例放弃对input、output的控制 input收到endinput收到Ctrl+D,表示传输结束(EOT)input收到Ctrl+C(SIGINT),且实例上 没有 注册SIGINT监听器
readlineInst.on('close', () => {
console.log('关闭');
});
一旦触发,这个实例就结束了。
error 事件
input 出错时触发,参数是 Error。
line 事件
input 收到行尾(\n、\r 或 \r\n)时触发,通常是用户按了 Enter。流在没有最后换行的情况下结束,也会再发一次 line,把最后半行交出来。
监听器拿到这一行字符串(不含换行符):
readlineInst.on('line', (input) => {
console.log(`接收到:${input}`);
});
history 事件
历史数组变化时触发(增删都算,受 historySize、removeHistoryDuplicates 影响)。回调参数是当前历史数组,可拿去持久化,也可在回调里改掉数组(例如不要把密码写进历史)。
readlineInst.on('history', (history) => {
console.log(history);
});
pause 事件
下列情况触发,回调无参数:
input被暂停input并未暂停,但收到了SIGCONT(见下面的SIGTSTP、SIGCONT)
没有注册 SIGINT 监听器时,Ctrl+C 会先走到 pause,这一点写在 SIGINT 事件里。
readlineInst.on('pause', () => {
console.log('暂停');
});
resume 事件
input 恢复时触发,回调无参数。
SIGINT 事件
input 收到 Ctrl+C(SIGINT)时触发,回调无参数。若此时没有注册 SIGINT 监听器,则改为触发 pause。
readlineInst.on('SIGINT', () => {
console.log('按键:Ctrl + C');
});
SIGCONT 事件
进程曾被 Ctrl+Z(SIGTSTP)送到后台,再用 fg(1p) 拉回前台时触发。若在 SIGTSTP 之前 input 已经暂停,则不会触发。回调无参数。收到后可再调 prompt(),它会自动 resume。
Windows 上不支持 SIGCONT。
readlineInst.on('SIGCONT', () => {
readlineInst.prompt();
});
SIGTSTP 事件
input 收到 Ctrl+Z(SIGTSTP)时触发。若没有注册 SIGTSTP 监听器,Node 进程会被送到后台。用 fg(1p) 恢复后会触发 pause 和 SIGCONT,可在那里恢复 input。若送进后台之前 input 已经暂停,则不会触发这两项。回调无参数。
readlineInst.on('SIGTSTP', () => {
console.log('按键:Ctrl + Z');
});
Windows 上不支持 SIGTSTP。
close()
关闭实例,撤回对 input、output 的控制,并触发 close 事件。
pause()
暂停 input,之后可用 resume() 恢复。
注意: pause() 不会立刻停掉实例上已经排队的其他事件(包括 line)。
prompt([preserveCursor])
在 output 新的一行写入当前配置的提示符,给用户一个输入位置。若 input 已暂停会先恢复;创建时 output 为 null 或 undefined 则不写提示。preserveCursor 为 true 时,不把光标重置到 0。
setPrompt(prompt)
设置之后 prompt() 要写到 output 的提示文案。prompt 是字符串。
getPrompt()
返回当前提示字符串。
question(query[, options], callback)
把 query 写到 output,等用户在 input 里回车,再把这一行交给 callback。input 若已暂停会先恢复;output 为空则不写提示。
query:提示前要写出的问句options.signal:AbortSignal,中止这次提问(回调不再调用)callback:只接收用户输入这一个参数
readlineInst.question('请输入些什么吧?', (answer) => {
console.log(`输入:${answer}`);
});
注意: 这个 callback 不是 Node 常见的 (err, result) 形式,没有第一个 Error 参数。
带超时中止:
const ac = new AbortController();
const { signal } = ac;
readlineInst.question('请输入些什么吧?', { signal }, (answer) => {
console.log(`输入:${answer}`);
});
setTimeout(() => ac.abort(), 10000);
resume()
若 input 已暂停,则恢复。
write(data[, key])
把 data,或 key 指定的按键序列,写到 output。key 只在 output 是 TTY 时有效。input 已暂停会先恢复;output 为空则什么也不写。指定了 key 就忽略 data。
data:字符串key.ctrl、key.meta、key.shift:对应修饰键key.name:按键名
readlineInst.write('输出!');
readlineInst.write(null, { ctrl: true, name: 'u' }); // 模拟 Ctrl+U,删到行首
注意: write() 写进的是接口的 input,效果等同用户亲手键入。
readline 方法
clearLine(stream, dir[, callback])
按 dir 清除给定 TTY 流的当前行。stream 是 可写 流。dir:-1 光标左边、1 光标右边、0 整行。可选 callback 在写完后调用。返回值与 stream.write 相同:需要等 drain 时返回 false。
clearScreenDown(stream[, callback])
从光标当前位置向下清屏。stream 是可写流。
createInterface(options)
创建 Interface 实例。
input:可读 流,必填output:可写 流,用来写提示和回显completer:Tab 补全函数,见下文terminal:是否把input、output当 TTY 并写 ANSI、VT100。默认在实例化时看output.isTTYhistory:初始历史行数组,默认[]。只在terminal为true(或内部根据output判断为 TTY)时才会启用历史historySize:最多保留多少行历史。0关闭历史。默认30。同样只在 TTY 时有意义prompt:提示字符串,默认'> '(带一个空格)crlfDelay:\r与\n间隔超过该毫秒数,则两者都当换行。会被收成不小于100的数。设为Infinity则\r\n始终是一个换行。默认100removeHistoryDuplicates:新行与旧行重复时删掉旧的。默认falseescapeCodeTimeout:读歧义按键序列时等待后续字符的毫秒数,默认500tabSize:一个 Tab 等于几个空格,最小1,默认8signal:AbortSignal,中止时内部调用close()
const readlineInst = readline.createInterface({
input: process.stdin,
output: process.stdout,
completer: (lines) => {
return [['1111', '2222', '3333'], lines];
},
terminal: true,
historySize: 30,
prompt: '自定义提示符:',
crlfDelay: 100,
removeHistoryDuplicates: true
});
readlineInst.prompt();
readlineInst.on('line', (line) => {
console.log(`接收到:${line}`);
});
completer 收到当前行,返回长度为 2 的数组:[匹配列表, 用来匹配的子串]。函数也可以收两个参数,第二个是回调,以便异步补全:
function completer(linePartial, callback) {
callback(null, [['123'], linePartial]);
}
terminal 为 true 时,output 最好提供 columns,并在宽度变化时发 resize(process.stdout 在 TTY 下会自己做)。
cursorTo(stream, x[, y][, callback])
把光标移到给定 TTY 可写流的绝对坐标 (x, y)。
moveCursor(stream, dx, dy[, callback])
相对当前位置移动光标。
emitKeypressEvents(stream[, interface])
让给定 可读 流按收到的输入触发 keypress。stream 若是 TTY,必须处于 raw 模式。可选传入一个 Interface:检测到粘贴时会暂时关掉自动补全。
readline 实例若发现 input 是终端,会自动调用本方法。关掉 Interface 不会 停止 input 继续发 keypress。
readline.emitKeypressEvents(process.stdin);
if (process.stdin.isTTY) {
process.stdin.setRawMode(true);
}
Promise 与异步迭代
const readline = require('readline/promises');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
async function main() {
const answer = await rl.question('请输入些什么吧?');
console.log(`输入:${answer}`);
rl.close();
}
main();
Interface 实现了异步迭代器,适合读文件,但吞吐不如直接听 line。循环被 break、throw、return 打断时会 close()。创建接口后就会开始消费 input,中间若先 await 别的事,前面的行会丢掉。
const readline = require('readline');
const fs = require('fs');
async function processLineByLine() {
const rl = readline.createInterface({
input: fs.createReadStream('test.txt'),
crlfDelay: Infinity
});
for await (const line of rl) {
console.log(`单行:${line}`);
}
}
processLineByLine();