目录
加餐:命令行交互原理
学习路径
readline 源码分析
如何开发命令行交互列表
实现原理
架构图
本章学习路径和学习目标
readline 的使用方法和实现原理
高能:深入讲解 readline 键盘输入监听实现原理
秀操作:手写 readline 核心实现
命令行样式修改的核心原理:ansi 转移序列讲解
响应式库 rxjs 快速入门
放大招:手写命令行交互式列表组件(上)(下)
inquirer 源码执行流程分析
加餐:命令行交互原理
学习路径
·掌握:readline / events / stream / ansi-escapes(实现命令行中特殊显示) / rxjs(响应式模型库)
·掌握命令行交互的实现原理,并实现一个可交互的列表
·分析 inquirer 源码掌握其中的关键实现
ANSI-escape-code 查阅文档:https://handwiki.org/wiki/ANSI_escape_code
readline 源码分析
·强制将函数转换为构建函数
if (!(this instance Interface)) {
return new Interface(input, output, completer, terminal)
}
·继承 EventEmitter
EventEmitter.call(this)
·监听键盘事件
emitKeypressEvents(input, this)
// `input` usually refers to stdin
input.on('keypress', onkeypress)
input.on('end', ontermend)
readline 核心实现原理:
注:readline 利用了 Generator 函数的特性,还不熟悉 Generator 函数的同学可以查看 https://es6.ruanyifeng.com/#docs/generator
如何开发命令行交互列表
实现原理
架构图
本章学习路径和学习目标
......
readline 的使用方法和实现原理
nodejs 内置库,主要帮助我们管理输入流的。命令当中要交互的方式一定是需要用户提供一些输入的。readline 就可以很好的帮我们去一次一次地读取输入流。这里要注意的是,这个输入不只是我们输入一些字符,还包括输入我们键盘上的内容,如 上下左右 esc 回车 空格,这些都在输入流的监听范围内。
// Usage:
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
rl.question('your name: ', (answer => {
console.log(answer);
rl.close(); // 需要手动关闭
}));
高能:深入讲解 readline 键盘输入监听实现原理
// generator 基础知识
function* g() {
console.log('read');
let ch = yield;
console.log(ch);
let s = yield;
console.log(s);
}
const f = g();
f.next();
f.next('a');
f.next('b');
// 输出 read a b
秀操作:手写 readline 核心实现
function stepRead(callback) {
// readline 源码为什么那么长,在处理各种用户输入的场景,如 回车 ctrl+C。场景是特别复杂的。
// 每输入一次就会监听。所有模式全部由开发者控制。原生模式,所有内容都不会帮你实现了。
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()); // toString 否则是个buffer。
}
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);
});
命令行样式修改的核心原理:ansi 转移序列讲解
简单来说,定义一个规范,这个规范可以让我们在终端当中通过转义字符实现一些特殊操作。如:将光标上移或者下移 换行等,输入信息擦除,字体加粗倾斜变色等。
console.log('\x1B[41m\x1B[4m%s\x1B[0m', 'your name:');
// \x1B[4m 文字加下划线
// \x 十六进制,1B 是固定的,%s 占位符。
// 41m 红色,0m 无色。
// m 表示渲染的参数
// B 表示光标下移
// G 表示水平移动
console.log('\x1B[2B%s', 'your name2:')
console.log('\x1B[2G%s', 'your name3:')
概念知与不知道,有天壤之别。
响应式库 rxjs 快速入门
异步库,和 promise 相似。inquirer 源码当中,大量使用 rxjs 去做回调、事件绑定监听。
const { range } = require('rxjs');
const { map, filter } = require('rxjs/operators');
const pipe = 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),
);
pipe.subscribe(x => console.log(x));
// 会打印符合条件的数字
放大招:手写命令行交互式列表组件(上)(下)
npm install -S mute-stream
npm install -S rxjs
npm install -S ansi-escapes # 清屏
const EventEmitter = require('events');
const readline = require('readline');
const MuteStream = require('mute-stream');
const { fromEvent } = require('rxjs');
const ansiEscapes = require('ansi-escapes');
const option = {
type: 'list',
name: 'name',
message: 'select your name:',
choices: [{
name: 'sam', value: 'sam',
}, {
name: 'shuangyue', value: 'sy',
}, {
name: 'zhangxuan', value: 'zx',
}],
};
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);
}
});
}
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); // 对 output 封装
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) {
// 判断是否为最后一个元素,如果是,则不加\n
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:', answers);
});
inquirer 源码执行流程分析
......