浅析node命令行交互原理

作者:PHM 时间:2024-05-11 10:14:36 

什么是命令行交互

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

常用的命令行交互库

在开发脚手架项目时,如果需要命令行交互去获取一些信息,最常用的库是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表示打印完后还原正常样式。执行的效果如下

浅析node命令行交互原理

除了颜色当然还可以设置其他样式,例如下划线等,替换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前面的数字表示要空几行,效果如下:

浅析node命令行交互原理

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

rxjs

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

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

流程图

浅析node命令行交互原理

代码实现

首先模仿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命令行交互原理

来源:https://juejin.cn/post/7236281103221030969

标签:node,命令行,交互
0
投稿

猜你喜欢

  • 通过python模糊匹配算法对两个excel表格内容归类

    2023-02-14 20:57:49
  • 如何恢复SQL Server 2000损坏的数据库文件

    2024-01-16 21:02:54
  • XMLHttp ASP远程获取网页内容代码

    2011-04-10 10:41:00
  • 在Python中使用第三方模块的教程

    2022-09-06 12:20:27
  • ASP应用之教你使用模板

    2008-10-15 13:09:00
  • 解决python执行较大excel文件openpyxl慢问题

    2021-06-25 15:58:04
  • 微软建议的ASP性能优化28条守则(2)

    2008-02-22 17:02:00
  • Facebook的特别之处是什么?

    2008-08-04 12:57:00
  • js读取配置文件自写

    2024-04-17 09:51:37
  • MySQL中数据导入恢复的简单教程

    2024-01-20 15:28:04
  • Spark在Windows下的环境搭建方法

    2023-07-16 11:35:45
  • php header功能的使用

    2023-11-15 09:25:26
  • Python3实现从文件中读取指定行的方法

    2021-01-06 04:18:17
  • python定时任务timeloop库用法实例详解

    2023-12-21 19:34:53
  • django自定义非主键自增字段类型详解(auto increment field)

    2021-08-22 02:11:42
  • 使用python写一个自动浏览文章的脚本实例

    2023-11-10 10:52:52
  • Django1.11配合uni-app发起微信支付的实现

    2023-12-18 13:22:22
  • Python 实现图像合成微缩效果

    2023-08-19 22:15:16
  • 在Linux下安装Oracle

    2010-07-30 12:46:00
  • Python登录系统界面实现详解

    2021-02-11 19:24:04
  • asp之家 网络编程 m.aspxhome.com