40行代码实现mini-readline🦄

大家好,我是哈默。今天我们来实现一个简单的 readline,希望能帮助大家理解 readline 的核心原理。

readline 的使用方法

readline 是 node 的一个内置模块。

它的使用方法是:

js 复制代码
const readline = require("readline");

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

rl.question("如何鼓励一下哈默:", (answer) => {
  console.log(`你的选择是:${answer}`);

  rl.close();
});

效果:

接下来,我们就来自己用 40 行代码实现一个 mini-readline 吧。

功能

监听我们在命令行中输入的内容,将输入的内容获取到,并打印出来。

定义核心方法 emitKeypressEvents

首先,我们定义一个 read方法,这个方法接收一个回调函数,在回调函数里,我们可以接收到一个 answer 参数,这个 answer 就是我们 在命令行里输入的内容

js 复制代码
function read() {}

read((answer) => {
  console.log("你输入了:" + answer);
});

readline 的核心逻辑里,会定义一个 emitKeypressEvents 方法,所以在我们的代码里,我们也定义一下这个方法,并执行它:

js 复制代码
function read() {
  emitKeypressEvents();
}

function emitKeypressEvents() {}

同时,我们在 read 方法中,将 输入流输出流保存结果的变量,都定义出来,并将输入流作为参数传入 emitKeypressEvents

js 复制代码
function read() {
  // 输入流
  const input = process.stdin;
  // 输出流
  const output = process.stdout;
  // 保存结果的变量
  let line = "";

  emitKeypressEvents(input);
}

function emitKeypressEvents(stream) {}

emitKeypressEvents 里,我们要做 2 件事情:

  1. 监听用户的输入。
  2. 处理用户的输入。

监听用户输入

首先,我们先来监听用户的输入:

js 复制代码
function emitKeypressEvents(stream) {
  function onData(chunk) {
    console.log("输入的内容:", chunk.toString());
  }
  stream.on("data", onData);
}

这里的 stream,是我们传入的输入流,我们通过在输入流上监听 data 事件,便可以让进程 处于等待用户输入的状态

并且,我们可以打印出,我们输入的内容。

处理用户的输入

能够获取到用户输入的内容之后,我们还需要处理用户的输入。

首先,我们需要定义一个方法,能够不断处理用户输入的内容,我们将它取名为 emitKeys

这个函数比较特殊,是一个 generator function:

js 复制代码
function emitKeypressEvents(stream) {
  function onData(chunk) {
    console.log("输入的内容:", chunk.toString());
  }
  stream.on("data", onData);

  const g = emitKeys(stream);
  g.next();
}

function* emitKeys(stream) {
  // 开启一个死循环,不断处理用户输入的内容
  while (true) {
    // 输入的字符
    let character = yield;
    // 广播一个 keypress 事件,参数为输入的字符
    stream.emit("keypress", character);
  }
}

当我们执行 g.next() 的时候,emitKeys 函数会执行起来,并且只会执行到 let character = yield; 这一行,并停止执行。

直到下一次我们再调用 g.next() 的时候,才会 继续执行 let character = yield; 后面的代码。这个后续代码的执行时机,大家在心里记一下,后面会涉及。

广播 keypress 事件

现在我们能够获取到用户的输入内容,并且也定义了不断处理用户输入的函数,那最后一步就是我们要在用户输入回车的时候,将输入的内容传入到我们使用时候的 read 函数的参数中。

那么,首先,我们就可以在 onData,也就是获取到用户输入的地方,从原来的 console.log 变成 g.next(chunk.toString())。

我们给 g.next() 传入的参数(即 chunk.toString())是我们输入的内容,而这个参数会通过 emitKeys 里的 yield 关键字返回,保存到 character 变量中:

js 复制代码
let character = yield;

然后通过输入流广播一个 keypress 事件,参数为输入的内容。

那么此时,我们就需要有一个地方来监听这里广播的 keypress 事件。

监听 keypress 事件

我们可以将这个监听 keypress 事件的函数定义在 read 方法中。

js 复制代码
function read(callback) {
  // keypress 事件的回调
  function onkeypress(character) {}

  ...

  emitKeypressEvents(input);

  input.on("keypress", onkeypress); // 监听 keypress 事件
}

在 onkeypress 中,我们将会处理 keypress 事件,其中就包括:在用户输入 回车键 的时候,调用用户传入的回调函数,并结束整个进程。

这里我们要处理回车的事件,所以我们需要开启输入流的原始模式。

开启后,将终端对字符的所有特殊处理都将被禁用,比如 回车、ctrl + c 结束进程、甚至包括我们输入字符时,字符的回显

这些操作的行为,将完全让我们开发者自己来定义,输入将会逐个字符进行监听。

开启的方式也是比较简单的,我们只需要调用输入流的 setRawMode API 即可。

js 复制代码
function read(callback) {
  // keypress 事件的回调
  function onkeypress(character) {}

  ...

  emitKeypressEvents(input);

  input.on("keypress", onkeypress); // 监听 keypress 事件
  input.setRawMode(true);
}

此时,我们会发现,我们不能输入任何字符了!

所以,我们第一步,就先将输入的内容回显出来。

js 复制代码
function read(callback) {
  function onkeypress(character) {
    // 通过 line 变量,保存输入的内容
    line += character;
    // 向输出流写入我们输入的内容,让我们输入的内容能够回显出来
    output.write(character);
  }

  ...

  emitKeypressEvents(input);

  input.on("keypress", onkeypress); // 监听 keypress 事件
  input.setRawMode(true);
}

此时,我们又能输入字符了,但会发现回车键,ctrl + c 依旧无法使用。

那么,我们就继续对于回车键进行处理,即 character 为 \r 的时候,将用户输入的所有内容传入到用户给的回调函数中:

js 复制代码
function read(callback) {
  function onkeypress(character) {
    // 通过 line 变量,保存输入的内容
    line += character;
    // 向输出流写入我们输入的内容,让我们输入的内容能够回显出来
    output.write(character);

    switch (character) {
      // 当输入回车键的时候,我们调用用户传入的回调函数 callback
      case "\r":
        // 将输入的内容,传入到 callback 中
        callback(line);
        break;
    }
  }

  ...

  emitKeypressEvents(input);

  input.on("keypress", onkeypress);
  input.setRawMode(true);
}

此时,我们会发现,我们接近成功了!

我们可以看到我们的回调函数执行了,并得到了结果:

js 复制代码
// 我们给 read 函数的回调函数执行了,并得到了 answer 的结果
read((answer) => {
  console.log("你输入了:" + answer);
});

最后一个问题!我们的进程没有结束,所以最后我们需要将输入流中断下来,我们可以通过输入流的 pause API:

js 复制代码
function read(callback) {
  function onkeypress(character) {
    // 通过 line 变量,保存输入的内容
    line += character;
    // 向输出流写入我们输入的内容,让我们输入的内容能够回显出来
    output.write(character);

    switch (character) {
      // 当输入回车键的时候,我们调用用户传入的回调函数 callback
      case "\r":
        // 将输入的内容,传入到 callback 中
        callback(line);
        // 将输入流中断下来
        input.pause();
        break;
    }
  }

  ...

  emitKeypressEvents(input);

  input.on("keypress", onkeypress);
  input.setRawMode(true);
}

我们再来试一下:

可以看到,输入流中断后,进程也成功终止了。

至此,我们的 mini-readline 也就实现完了。

总结

我们实现了一个简单的 readline,mini-readline。

原理是比较简单的,就是:监听用户的输入,并处理用户的输入,最终调用用户传入的回调。

但其中的过程,如果初次接触的话,可能需要多看几遍,并自己亲自尝试一下,才能够 get 到其中的原理。

相关推荐
红尘散仙14 分钟前
一个 `#[uniffi::export]`,把 Rust 接进 React Native
前端·后端·rust
moshuying15 分钟前
AI Coding 最大的 token 黑洞,可能根本不是 prompt
前端
红尘散仙16 分钟前
一行 `#[specta::specta]`,让 Tauri IPC 有类型
前端·后端·rust
lichenyang45338 分钟前
HarmonyOS HMRouter 接入记录:从普通 Tab Demo 到路由跳转
前端
Aphasia3111 小时前
CORS、CSRF和XSS
面试
木斯佳1 小时前
前端八股文面经大全:腾讯WXG暑期前端一面(2026-05-15)·面经深度解析
前端·面试·笔试
canonical_entropy2 小时前
NOP Chaos Flux 架构演变史:从 AMIS 重写到现代低代码运行时
前端·aigc·ai编程
张元清2 小时前
useEffect 之外:专门处理异步、深比较和 SSR 的 Effect Hook
前端·javascript·面试
小小小小宇2 小时前
前端双Token机制无感刷新(二)
前端
XinZong3 小时前
OpenClaw 中最经典的 6 款skill,真正能进工作流的 skills
javascript·后端