核心流程

实现一个命令行List交互组件类,可以调用方式如下:

  1. const inquirer = require('inquirer')
  2. inquirer
  3. .prompt([
  4. {
  5. name:'name',
  6. message:'请输入你的名字',
  7. type:'list',
  8. choices:['佳燕','佳新','建秋']
  9. },{
  10. name:'address',
  11. message:'请输入你的地址',
  12. type:'input',
  13. }
  14. ])
  15. .then((answers) => {
  16. // Use user feedback for... whatever!!
  17. console.log('answers',answers)
  18. })
  19. .catch((error) => {
  20. if (error.isTtyError) {
  21. // Prompt couldn't be rendered in the current environment
  22. } else {
  23. // Something else went wrong
  24. }
  25. });

实现步骤

1、显示选项里面的列表。
2、监听用户的键盘事件,根据操作是上下选择还是回车决定是重新渲染列表还是清屏,回车的时候需要关闭关闭输入流,返回Promise。
image.png

  1. function Prompt(option) {
  2. return new Promise((resolve, reject) => {
  3. try {
  4. const list = new List(option);
  5. list.render();
  6. list.on('exit', function(answers) {
  7. resolve(answers);
  8. })
  9. } catch (e) {
  10. reject(e);
  11. }
  12. });
  13. }

具体细节

  • 封装一个List组件类,需要支持消息订阅,因为要通知流关闭。
  • render的实现,根据是否选择完毕以及选择的选项进行渲染,用到ansi转义码显示样式,用清屏
  • 创建一个readline流,监听用户输入,然后用rxjs对输入的键盘事件做监听,并根据键盘事件做相应的处理。
  • 输出流用进行包装,方便对一些输出做丢弃。

    1. class List extends EventEmitter {
    2. constructor (opts) {
    3. super()
    4. this.name = opts.name
    5. this.message = opts.message
    6. this.choices = opts.choices
    7. this.input = process.stdin
    8. const ms = new MuteStream();
    9. ms.pipe(process.stdout)
    10. this.output = ms
    11. this.height = 0
    12. this.rl = readline.createInterface({
    13. input:this.input ,
    14. output:this.output
    15. })
    16. this.finished = false
    17. this.choice = 0
    18. this.result = ''
    19. // fromEvent帮我们监听this.rl.input事件,事件回调是this.onKeypress
    20. fromEvent(this.rl.input,'keypress').forEach(this.onKeypress)
    21. }
    22. /**
    23. * 这里一定要用箭头函数,不然获取不到this
    24. * 处理键盘输入的事件
    25. * @param {*} keymap
    26. */
    27. onKeypress=(keymap)=>{
    28. const key = keymap[1];
    29. if (key.name === 'down') {
    30. this.choice++;
    31. if (this.choice> this.choices.length - 1) {
    32. this.choice = 0;
    33. }
    34. this.render();
    35. } else if (key.name === 'up') {
    36. this.choice--;
    37. if (this.choice < 0) {
    38. this.choice= this.choices.length - 1;
    39. }
    40. this.render();
    41. } else if (key.name === 'return') {
    42. this.haveSelected = true;
    43. this.render();
    44. this.close();
    45. this.emit('exit', this.choices[this.choice]);
    46. }
    47. }
    48. render=()=>{
    49. this.output.unmute();
    50. this.clean();
    51. const content = this.getContent()
    52. this.output.write(content);
    53. // this.output.mute();
    54. }
    55. getContent = ()=>{
    56. if (!this.haveSelected) {
    57. let title = '\x1B[32m?\x1B[39m \x1B[1m' + this.message + '\x1B[22m\x1B[0m \x1B[0m\x1B[2m(Use arrow keys)\x1B[22m\n';
    58. this.choices.forEach((choice, index) => {
    59. if (index === this.choice) {
    60. // 判断是否为最后一个元素,如果是,则不加\n
    61. if (index === this.choices.length - 1) {
    62. title += '\x1B[36m❯ ' + choice + '\x1B[39m ';
    63. } else {
    64. title += '\x1B[36m❯ ' + choice + '\x1B[39m \n';
    65. }
    66. } else {
    67. if (index === this.choices.length - 1) {
    68. title += ' ' + choice;
    69. } else {
    70. title += ' ' + choice + '\n';
    71. }
    72. }
    73. });
    74. this.height = this.choices.length + 1;
    75. return title;
    76. } else {
    77. // 输入结束后的逻辑
    78. const name = this.choices[this.choice];
    79. let title = '\x1B[32m?\x1B[39m \x1B[1m' + this.message + '\x1B[22m\x1B[0m \x1B[36m' + name + '\x1B[39m\x1B[0m \n';
    80. return title;
    81. }
    82. }
    83. clean =()=>{
    84. const emptyLines = ansiEscapes.eraseLines(this.height);
    85. this.output.write(emptyLines);
    86. }
    87. close=()=>{
    88. this.height = 0;
    89. this.output.unmute();
    90. this.rl.output.end();
    91. this.rl.pause()
    92. this.rl.close();
    93. }
    94. }

    架构图

截屏2022-09-10 17.38.45.png
截屏2022-09-10 17.44.38.png
截屏2022-09-10 17.45.35.png
截屏2022-09-10 17.46.18.png

相关库

:操作终端的ansi转义码,这里用于清屏,从当前光标位置向上擦除指定数量的行。
ansiEscapes.eraseLines(this.height);

相关知识拓展

ansi转义码

ANSI转义序列(ANSI escape sequences)是一种带内信号的转义序列标准,用于控制视频文本终端上的光标位置、颜色和其他选项。 在文本中嵌入确定的字节序列,大部分以 ESC 转义字符和”[“字符开始,终端会把这些字节序列解释为相应的指令,而不是普通的字符编码。

  1. // 背景颜色变成红色,然后带了下划线
  2. console.log('\x1B[41m\x1B[4m%s\x1B[0m', 'your name:');
  3. console.log('\x1B[2B%s', 'your name2:');

终端中的坐标

x,y坐标代表行列数

class方法中获取不到this的问题

用箭头函数解决

  1. class List {
  2. constructor(){
  3. fromEvent(this.rl.input,'keypress').forEach(this.onKeypress)
  4. }
  5. onKeypress=(keymap)=>{
  6. //
  7. this.clean()
  8. console.log(keymap)
  9. }
  10. }