核心流程
实现一个命令行List交互组件类,可以调用方式如下:
const inquirer = require('inquirer')inquirer.prompt([{name:'name',message:'请输入你的名字',type:'list',choices:['佳燕','佳新','建秋']},{name:'address',message:'请输入你的地址',type:'input',}]).then((answers) => {// Use user feedback for... whatever!!console.log('answers',answers)}).catch((error) => {if (error.isTtyError) {// Prompt couldn't be rendered in the current environment} else {// Something else went wrong}});
实现步骤
1、显示选项里面的列表。
2、监听用户的键盘事件,根据操作是上下选择还是回车决定是重新渲染列表还是清屏,回车的时候需要关闭关闭输入流,返回Promise。
function Prompt(option) {return new Promise((resolve, reject) => {try {const list = new List(option);list.render();list.on('exit', function(answers) {resolve(answers);})} catch (e) {reject(e);}});}
具体细节
- 封装一个List组件类,需要支持消息订阅,因为要通知流关闭。
render的实现,根据是否选择完毕以及选择的选项进行渲染,用到ansi转义码显示样式,用清屏- 创建一个
readline流,监听用户输入,然后用rxjs对输入的键盘事件做监听,并根据键盘事件做相应的处理。 输出流用进行包装,方便对一些输出做丢弃。
class List extends EventEmitter {constructor (opts) {super()this.name = opts.namethis.message = opts.messagethis.choices = opts.choicesthis.input = process.stdinconst ms = new MuteStream();ms.pipe(process.stdout)this.output = msthis.height = 0this.rl = readline.createInterface({input:this.input ,output:this.output})this.finished = falsethis.choice = 0this.result = ''// fromEvent帮我们监听this.rl.input事件,事件回调是this.onKeypressfromEvent(this.rl.input,'keypress').forEach(this.onKeypress)}/*** 这里一定要用箭头函数,不然获取不到this* 处理键盘输入的事件* @param {*} keymap*/onKeypress=(keymap)=>{const key = keymap[1];if (key.name === 'down') {this.choice++;if (this.choice> this.choices.length - 1) {this.choice = 0;}this.render();} else if (key.name === 'up') {this.choice--;if (this.choice < 0) {this.choice= this.choices.length - 1;}this.render();} else if (key.name === 'return') {this.haveSelected = true;this.render();this.close();this.emit('exit', this.choices[this.choice]);}}render=()=>{this.output.unmute();this.clean();const content = this.getContent()this.output.write(content);// this.output.mute();}getContent = ()=>{if (!this.haveSelected) {let title = '\x1B[32m?\x1B[39m \x1B[1m' + this.message + '\x1B[22m\x1B[0m \x1B[0m\x1B[2m(Use arrow keys)\x1B[22m\n';this.choices.forEach((choice, index) => {if (index === this.choice) {// 判断是否为最后一个元素,如果是,则不加\nif (index === this.choices.length - 1) {title += '\x1B[36m❯ ' + choice + '\x1B[39m ';} else {title += '\x1B[36m❯ ' + choice + '\x1B[39m \n';}} else {if (index === this.choices.length - 1) {title += ' ' + choice;} else {title += ' ' + choice + '\n';}}});this.height = this.choices.length + 1;return title;} else {// 输入结束后的逻辑const name = this.choices[this.choice];let title = '\x1B[32m?\x1B[39m \x1B[1m' + this.message + '\x1B[22m\x1B[0m \x1B[36m' + name + '\x1B[39m\x1B[0m \n';return title;}}clean =()=>{const emptyLines = ansiEscapes.eraseLines(this.height);this.output.write(emptyLines);}close=()=>{this.height = 0;this.output.unmute();this.rl.output.end();this.rl.pause()this.rl.close();}}
架构图




相关库
:操作终端的ansi转义码,这里用于清屏,从当前光标位置向上擦除指定数量的行。ansiEscapes.eraseLines(this.height);
相关知识拓展
ansi转义码
ANSI转义序列(ANSI escape sequences)是一种带内信号的转义序列标准,用于控制视频文本终端上的光标位置、颜色和其他选项。 在文本中嵌入确定的字节序列,大部分以 ESC 转义字符和”[“字符开始,终端会把这些字节序列解释为相应的指令,而不是普通的字符编码。
// 背景颜色变成红色,然后带了下划线console.log('\x1B[41m\x1B[4m%s\x1B[0m', 'your name:');console.log('\x1B[2B%s', 'your name2:');
终端中的坐标
class方法中获取不到this的问题
用箭头函数解决
class List {constructor(){fromEvent(this.rl.input,'keypress').forEach(this.onKeypress)}onKeypress=(keymap)=>{//this.clean()console.log(keymap)}}
