核心流程
实现一个命令行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.name
this.message = opts.message
this.choices = opts.choices
this.input = process.stdin
const ms = new MuteStream();
ms.pipe(process.stdout)
this.output = ms
this.height = 0
this.rl = readline.createInterface({
input:this.input ,
output:this.output
})
this.finished = false
this.choice = 0
this.result = ''
// fromEvent帮我们监听this.rl.input事件,事件回调是this.onKeypress
fromEvent(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) {
// 判断是否为最后一个元素,如果是,则不加\n
if (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)
}
}