命令行交互原理
重点知识
- 掌握:readline / events / stream / ansi-escapes / rxjs
- 掌握命令行交互实现原理,并实现一个可交互的列表
- 分析 inquirer 源码掌握其中的关键实现
ANSI-escape-code查阅文档:https://handwiki.org/wiki/ANSI_escape_code
readline 源码分析
- 强制将函数转换为构建函数
js
if (!(this instanceof Interface)) {
return new Interface(input, output, completer, terminal);
}
- 获得事件驱动能力
js
EventEmitter.call(this);
- 监听键盘事件
js
emitKeypressEvents(input, this);
// `input` usually refers to stdin
input.on('keypress', onkeypress);
input.on('end', ontermend);
readline 核心实现原理:
注:readline利用了Generator函数的特性,查看:https://es6.ruanyifeng.com/#docs/generator
代码演示:
js
import * as readline from 'node:readline'; // https://nodejs.org/api/readline.html#readline
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
rl.question('your name:', (answer) => {
console.log(answer)
rl.close()
})
js
function* g() {
console.log('read');
let ch = yield;
console.log(ch)
let s = yield;
console.log(s)
}
const f = g();
console.log(f)
f.next()
f.next('a')
f.next('b')
js
function stepRead(callback) {
function onkeypress(s) {
output.write(s);
line += s;
switch (s) {
case '\r':
input.pause();
callback(line);
break;
}
}
const input = process.stdin;
const output = process.stdout;
let line = '';
emitKeypressEvents(input);
input.on('keypress', onkeypress);
input.setRawMode(true);
input.resume();
}
function emitKeypressEvents(stream) {
function onData(chunk) {
g.next(chunk.toString());
}
const g = emitKeys(stream);
g.next()
stream.on('data', onData)
}
function* emitKeys(stream) {
while (true) {
let ch = yield;
stream.emit('keypress', ch)
}
}
stepRead(function (s) {
console.log('answer:', s)
})
如何开发命令行交互列表
实现原理
获取字符串的核心实现:
js
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.selected) {
if (index === this.choices.length - 1) {
title += '\x1B[36m❯ ' + choice.name + '\x1B[39m ';
} else {
title += '\x1B[36m❯ ' + choice.name + '\x1B[39m \n';
}
} else {
if (index === this.choices.length - 1) {
title += ` ${choice.name} `;
} else {
title += ` ${choice.name} \n`;
}
}
});
this.height = this.choices.length + 1;
return title;
} else {
// 输入结束后的逻辑
const name = this.choices[this.selected].name;
let title = '\x1B[32m?\x1B[39m \x1B[1m' + this.message + '\x1B[22m\x1B[0m \x1B[36m' + name + '\x1B[39m\x1B[0m \n';
return title;
}
};
架构图
代码演示:
js
import { range, filter, map } from 'rxjs'; // https://www.npmjs.com/package/rxjs
range(1, 200)
.pipe(
filter(x => x % 2 === 1),
map(x => x + x),
filter(x => x % 3 === 0),
filter(x => x % 6 === 0),
filter(x => x % 9 === 0),
)
.subscribe(x => console.log(x));
js
import inquirer from 'inquirer'; // https://www.npmjs.com/package/inquirer
inquirer
.prompt([
{
type: 'input',
name: 'yourName',
message: 'your name',
default: 'noname',
validate: function (v) {
return typeof v === 'string'
},
transformer: function (v) {
return 'name:' + v
},
filter: function (v) {
return 'name[' + v + ']'
}
},
{
type: 'number',
name: 'num',
message: 'your number'
},
{
type: 'confirm',
name: 'choice',
message: 'your choice',
default: true
},
{
type: 'list',
name: 'list',
message: 'your list',
default: 0,
choices: [
{ value: 1, name: 'a'},
{ value: 2, name: 'b'},
{ value: 3, name: 'c'},
]
},
{
type: 'rawlist',
name: 'rawlist',
message: 'your rawlist',
default: 0,
choices: [
{ value: 1, name: 'a'},
{ value: 2, name: 'b'},
{ value: 3, name: 'c'},
]
},
{
type: 'expand',
name: 'expand',
message: 'your expand',
default: 'red',
choices: [
{ key: 'R', value: 'red'},
{ key: 'G', value: 'green'},
{ key: 'B', value: 'grey'},
]
},
{
type: 'checkbox',
name: 'checkbox',
message: 'your checkbox',
default: 0,
choices: [
{ value: 1, name: 'a'},
{ value: 2, name: 'b'},
{ value: 3, name: 'c'},
]
},
{
type: 'password',
name: 'password',
message: 'your password',
},
{
type: 'editor',
name: 'editor',
message: 'your editor',
},
])
.then((answers) => {
console.log(answers)
})
.catch((error) => {
if (error.isTtyError) {
// Prompt couldn't be rendered in the current environment
} else {
// Something else went wrong
}
});
js
import { EventEmitter } from 'events';
import MuteStream from 'mute-stream'; // https://www.npmjs.com/package/mute-stream
import * as readline from 'node:readline'; // https://nodejs.org/api/readline.html#readline
import { fromEvent } from "rxjs";
import ansiEscapes from 'ansi-escapes'; // https://www.npmjs.com/package/ansi-escapes
const option = {
type: 'list',
name: 'name',
message: 'select your name',
choices: [
{
name: 'zhangSan', value: 'zhangSan'
},
{
name: 'liSi', value: 'liSi'
}
]
}
function Prompt(option) {
return new Promise((resolve, reject) => {
try {
const list = new List(option);
list.render();
list.on('exit', function (answer){
resolve(answer)
})
} catch (e) {
reject(e)
}
})
}
class List extends EventEmitter {
constructor(option) {
super();
this.name = option.name;
this.message = option.message;
this.choices = option.choices;
this.input = process.stdin;
const ms = new MuteStream();
ms.pipe(process.stdout);
this.output = ms;
this.rl = readline.createInterface({
input: this.input,
output: this.output
});
this.selected = 0;
this.height = 0;
this.keypress = fromEvent(this.rl.input, 'keypress').forEach(this.onkeypress);
this.haveSelected = false; // 是否已经选择完毕
}
onkeypress = (keymap) => {
const key = keymap[1];
if (key.name === 'down') {
this.selected ++;
if (this.selected > this.choices.length - 1) {
this.selected = 0;
}
this.render()
} else if (key.name === 'up') {
this.selected --;
if (this.selected < 0) {
this.selected = this.choices.length - 1;
}
this.render()
} else if (key.name === 'return') {
this.haveSelected = true;
this.render();
this.close();
this.emit('exit', this.choices[this.selected])
}
};
render() {
this.output.unmute();
this.clean();
this.output.write(this.getContent());
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.selected) {
if (index === this.choices.length - 1) {
title += '\x1B[36m❯ ' + choice.name + '\x1B[39m ';
} else {
title += '\x1B[36m❯ ' + choice.name + '\x1B[39m \n';
}
} else {
if (index === this.choices.length - 1) {
title += ` ${choice.name} `;
} else {
title += ` ${choice.name} \n`;
}
}
});
this.height = this.choices.length + 1;
return title;
} else {
// 输入结束后的逻辑
const name = this.choices[this.selected].name;
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.output.unmute();
this.rl.output.end();
this.rl.pause();
this.rl.close();
}
}
Prompt(option).then(answers => {
console.log(answers)
})