浅析node命令行交互原理

 更新时间:2023年05月24日 09:31:13   作者:PHM  
当我们使用脚手架去创建一个项目的时候,通常会通过命令行交互来获取一些信息,比如填项目名称,选择项目模板,选择版本,我们虽然经常用到,但是想必对于其中的原理还是不太了解,本文将待大家详细介绍一下node命令行的交互原理,需要的朋友可以参考下

什么是命令行交互

当我们使用脚手架去创建一个项目的时候,通常会通过命令行交互来获取一些信息:比如填项目名称;选择项目模板;选择版本;需要安装哪些额外的工具等等。这些我们虽然经常用到,但是想必对于其中的原理还是不太了解。

常用的命令行交互库

在开发脚手架项目时,如果需要命令行交互去获取一些信息,最常用的库是inquirer,其使用也非常简单:下面给出简单的示例:

var inquirer = require('inquirer');
inquirer.prompt([
  {
    type: 'list',
    name: 'projectType',
    message: '请选择初始化类型',
    default: TYPE_PROJECT,
    choices: [
      {
        name: '项目',
        value: TYPE_PROJECT,
      },
      {
        name: '组件',
        value: TYPE_COMPONENT,
      },
    ],
  },
]);
// 询问项目的基本信息
inquirer.prompt([
  {
    type: 'input',
    name: 'projectName',
    message: '请确认项目名称',
    default: this.projectName,
    validate: function(v) {
      const done = this.async();
      setTimeout(function() {
        // 校验规则:
        // 1. 首字母必须为英文字符
        // 2. 尾字母必须为英文或数字,不能为字符
        // 3. 字符仅允许"-_"
        // 4. 长度必须大于等于1
        if (!/^[a-zA-Z][\w-]{0,}[a-zA-Z0-9]$/.test(v)) {
          // Pass the return value in the done callback
          done('请输入合法的项目名称');
          return;
        }
        // Pass the return value in the done callback
        done(null, true);
      }, 0);
    }
  },
  {
    type: 'input',
    name: 'projectVersion',
    message: '请输入项目版本号',
    default: '1.0.0',
    validate: function(v) {
      const done = this.async();
      setTimeout(() => {
        if (!semver.valid(v)) {
          done('请输入合法的版本号');
          return;
        }
        done(null, true);
      }, 0)
    },
    filter: function(v) {
      if (!!semver.valid(v)) {
        return semver.valid(v)
      } else {
        return v
      }
    }
  }
])

开发命令行工具常用库或API

readline

readline是node提供的原生API,它可以帮助我们实现命令行交互,如下是一个简单的案例:

const readline = require('node:readline');
const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout
});
rl.question('What do you think of Node.js? ', (answer) => {
  // TODO: Log the answer in a database
  console.log(`Thank you for your valuable feedback: ${answer}`);
  rl.close();
});

运行这个代码就会出现命令行输入的效果,输入完成回车会在回调里拿到输入的内容,调用rl.close()会结束交互。

ansi-escapes

ANSI转义序列是用于在视频文本终端和终端模拟器上控制光标位置、颜色、字体样式和其他选项的带内信令的标准。某些字节序列(大多数以ASCII转义字符和方括号字符开始)嵌入到文本中。终端将这些序列解释为命令,而不是逐字显示的文本。
当我们想改变命令行的文字样式、位置等就可以使用这个标准,比如我想打印红色的文字,那么我会这样写:

console.log('\x1B[31m%s\x1B[0m', 'I am red');

\x1B[是固定写法,31表示红色字体背景,m表示设置代码后面字符的颜色和样式,%s是log方法第二个参数的占位符,0m表示打印完后还原正常样式。执行的效果如下

除了颜色当然还可以设置其他样式,例如下划线等,替换code即可,具体可以参考:ANSI escape code - HandWiki 里的code表。

// 打印红色文字
console.log('\x1B[31m%s\x1B[0m', 'I am red');
// 打印文字添加下划线
console.log('\x1B[4m%s\x1B[0m', 'underline');
// 红色文字下划线
console.log('\x1B[31m\x1B[4m%s\x1B[0m', 'I am red and underline');

除了样式还可以处理光标位置,比如想打印完一行后隔两行再继续打印,则可以这样写:

// 红色文字下划线
console.log('\x1B[31m\x1B[4m%s\x1B[0m', 'I am red and underline');
// cursor 移动
console.log('\x1B[2B%s\x1B[0m', 'I am red and underline');

将m换成B,B前面的数字表示要空几行,效果如下:

还有更多的可能你想要的效果,可以查看 ANSI escape code - HandWiki

rxjs

RxJS 是一个用于处理异步编程的 JavaScript 库,目标是使编写异步和基于回调的代码更容易。

自己开发一个命令行列表选择功能

流程图

代码实现

首先模仿inquirer的使用方式搭一个简单的架子:

const option = {
  type: 'list',
  name: 'name',
  message: 'select a name',
  choices: [
    { name: 'sam', value: 'sam' },
    { name: 'tom', value: 'tom' },
    { name: 'jerry', value: 'jerry' }
  ]
}
function prompt(option) {
  return new Promise((resolve, reject) => {})
}
prompt(option).then((answer) => {
  console.log(answer)
})

然后实现一个List类,由它来做具体的实现:

const { EventEmitter } = require('events');
const rl = require('readline');
const MuteStream = require('mute-stream');
const { fromEvent } = require('rxjs');
const ansi = require('ansi-escapes');
class List extends EventEmitter {
  constructor(option) {
    super();
    this.name = option.name;
    this.message = option.message;
    this.choices = option.choices;
    this.input = process.stdin;
    // 通过mute-stream来实现控制台的输入输出
    const ms = new MuteStream();
    ms.pipe(process.stdout);
    this.output = ms;
    // 创建readline接口
    this.rl = rl.createInterface({
      input: this.input,
      output: this.output,
    });
    // 默认选中
    this.selected = option.default || 0;
    this.height = this.choices.length + 1;
    // 监听keypress事件
    this.keypress = fromEvent(this.rl.input, 'keypress').subscribe(
      this.onKeypress
    );
    // 是否选择完毕
    this.done = false;
  }
  // 键盘事件
  onKeypress = (keyMap) => {
    // 获取key
    const key = keyMap[1];
    if (key.name === 'up') { // 上键点击
      if (this.selected > 0) {
        this.selected--;
      }
    } else if (key.name === 'down') { // 下键点击
      if (this.selected < this.choices.length - 1) {
        this.selected++;
      }
    } else if (key.name === 'return') { // 回车点击
      this.done = true;
    }
    this.render();
    // 完成选择后退出
    if (this.done) {
      this.close()
      this.emit('exit', this.choices[this.selected])
    }
  }
  render() {
    // 解除mute状态
    this.output.unmute();
    // 清除控制台
    this.clean();
    // 写入list内容
    this.output.write(this.getContent());
    // 开启mute状态 限制用户不可输入
    this.output.mute();
  }
  getContent() {
    if (this.done) {
      return `\x1B[1m${this.message} \x1B[22m\x1B[36m${this.choices[this.selected].name}\x1B[39m\n`;
    } else {
      const title = `\x1B[32m?\x1B[39m \x1B[1m${this.message}(use arrow keys)\x1B[22m\n`;
      const list = this.choices.map((item, index) => {
        // 选中的选项前面加上❯
        if (index === this.selected) {
          return `\x1B[36m❯ ${item.name}\x1B[39m`;
        }
        return `  ${item.name}`;
      });
      return title + list.join('\n');
    }
  }
  // 清除控制台
  clean() {
    const emptyLines = ansi.eraseLines(this.height);
    this.output.write(emptyLines);
  }
  close() {
    this.output.unmute();
    this.rl.output.end();
    this.rl.pause();
    this.rl.close();
    this.keypress.unsubscribe();
  }
}

然后使用List

function prompt(option) {
  return new Promise((resolve, reject) => {
    try {
      const list = new List(option);
      // 渲染列表
      list.render();
      list.on('exit', (answer) => {
        resolve(answer);
      });
    } catch (error) {
      reject(error);
    }
  });
}
prompt(option).then((answer) => {
  console.log('answer', answer);
});

运行效果就是这样:

到此这篇关于浅析node命令行交互原理的文章就介绍到这了,更多相关node 命令行交互内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Node.js、Socket.IO和GPT-4构建AI聊天机器人的项目实践

    Node.js、Socket.IO和GPT-4构建AI聊天机器人的项目实践

    本文主要介绍了Node.js、Socket.IO和GPT-4构建AI聊天机器人的项目实践,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-05-05
  • node.js中实现GET和POST请求的代码示例

    node.js中实现GET和POST请求的代码示例

    在很多场景中,我们的服务器都需要跟用户的浏览器打交道,如发送验证码、登录表单提交,请求服务器数据一般都使用GET请求,表单提交到服务器一般都使用POST请求,本文详细介绍了在Node.js中如何处理GET和POST请求,需要的朋友可以参考下
    2024-12-12
  • webpack打包、编译、热更新Node内存不足问题解决

    webpack打包、编译、热更新Node内存不足问题解决

    Webpack是现在主流的功能强大的模块化打包工具,在使用Webpack时,如果不注意性能优化,有非常大的可能会产生性能问题,下面这篇文章主要给大家介绍了关于webpack打包、编译、热更新Node内存不足问题解决的相关资料,需要的朋友可以参考下
    2023-03-03
  • node版本与node-sass版本不兼容时的问题及解决

    node版本与node-sass版本不兼容时的问题及解决

    这篇文章主要介绍了node版本与node-sass版本不兼容时的问题及解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-04-04
  • 前端常见面试题之async/await和promise的区别

    前端常见面试题之async/await和promise的区别

    async/await是异步代码的新方式,以前的方法有回调函数和Promise,下面这篇文章主要给大家介绍了关于前端常见面试题之async/await和promise区别的相关资料,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
    2022-07-07
  • node.js中的console.dir方法使用说明

    node.js中的console.dir方法使用说明

    这篇文章主要介绍了node.js中的console.dir方法使用说明,本文介绍了console.dir的方法说明、语法、接收参数、使用实例和实现源码,需要的朋友可以参考下
    2014-12-12
  • 测试驱动ChatGPT编程示例详解

    测试驱动ChatGPT编程示例详解

    这篇文章主要为大家介绍了测试驱动ChatGPT编程示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-03-03
  • 使用express+multer实现node中的图片上传功能

    使用express+multer实现node中的图片上传功能

    这篇文章主要介绍了使用express+multer实现node中的图片上传功能,需要的朋友可以参考下
    2018-02-02
  • NodeJS自定义模块写法(详解)

    NodeJS自定义模块写法(详解)

    下面小编就为大家带来一篇NodeJS自定义模块写法(详解)。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-06-06
  • 零基础搭建Node.js、Express、Ejs、Mongodb服务器及应用开发入门

    零基础搭建Node.js、Express、Ejs、Mongodb服务器及应用开发入门

    这篇文章主要介绍了零基础搭建Node.js、Express、Ejs、Mongodb服务器及应用开发入门,本文在windows8系统下完成本教程,其它系统也可参考,需要的朋友可以参考下
    2014-12-12

最新评论