淺析node命令行交互原理
什么是命令行交互
當(dāng)我們使用腳手架去創(chuàng)建一個(gè)項(xiàng)目的時(shí)候,通常會(huì)通過(guò)命令行交互來(lái)獲取一些信息:比如填項(xiàng)目名稱(chēng);選擇項(xiàng)目模板;選擇版本;需要安裝哪些額外的工具等等。這些我們雖然經(jīng)常用到,但是想必對(duì)于其中的原理還是不太了解。
常用的命令行交互庫(kù)
在開(kāi)發(fā)腳手架項(xiàng)目時(shí),如果需要命令行交互去獲取一些信息,最常用的庫(kù)是inquirer,其使用也非常簡(jiǎn)單:下面給出簡(jiǎn)單的示例:
var inquirer = require('inquirer');
inquirer.prompt([
{
type: 'list',
name: 'projectType',
message: '請(qǐng)選擇初始化類(lèi)型',
default: TYPE_PROJECT,
choices: [
{
name: '項(xiàng)目',
value: TYPE_PROJECT,
},
{
name: '組件',
value: TYPE_COMPONENT,
},
],
},
]);
// 詢(xún)問(wèn)項(xiàng)目的基本信息
inquirer.prompt([
{
type: 'input',
name: 'projectName',
message: '請(qǐng)確認(rèn)項(xiàng)目名稱(chēng)',
default: this.projectName,
validate: function(v) {
const done = this.async();
setTimeout(function() {
// 校驗(yàn)規(guī)則:
// 1. 首字母必須為英文字符
// 2. 尾字母必須為英文或數(shù)字,不能為字符
// 3. 字符僅允許"-_"
// 4. 長(zhǎng)度必須大于等于1
if (!/^[a-zA-Z][\w-]{0,}[a-zA-Z0-9]$/.test(v)) {
// Pass the return value in the done callback
done('請(qǐng)輸入合法的項(xiàng)目名稱(chēng)');
return;
}
// Pass the return value in the done callback
done(null, true);
}, 0);
}
},
{
type: 'input',
name: 'projectVersion',
message: '請(qǐng)輸入項(xiàng)目版本號(hào)',
default: '1.0.0',
validate: function(v) {
const done = this.async();
setTimeout(() => {
if (!semver.valid(v)) {
done('請(qǐng)輸入合法的版本號(hào)');
return;
}
done(null, true);
}, 0)
},
filter: function(v) {
if (!!semver.valid(v)) {
return semver.valid(v)
} else {
return v
}
}
}
])開(kāi)發(fā)命令行工具常用庫(kù)或API
readline
readline是node提供的原生API,它可以幫助我們實(shí)現(xiàn)命令行交互,如下是一個(gè)簡(jiǎn)單的案例:
const readline = require('node:readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
rl.question('What do you think of Node.js? ', (answer) => {
// TODO: Log the answer in a database
console.log(`Thank you for your valuable feedback: ${answer}`);
rl.close();
});運(yùn)行這個(gè)代碼就會(huì)出現(xiàn)命令行輸入的效果,輸入完成回車(chē)會(huì)在回調(diào)里拿到輸入的內(nèi)容,調(diào)用rl.close()會(huì)結(jié)束交互。
ansi-escapes
ANSI轉(zhuǎn)義序列是用于在視頻文本終端和終端模擬器上控制光標(biāo)位置、顏色、字體樣式和其他選項(xiàng)的帶內(nèi)信令的標(biāo)準(zhǔn)。某些字節(jié)序列(大多數(shù)以ASCII轉(zhuǎn)義字符和方括號(hào)字符開(kāi)始)嵌入到文本中。終端將這些序列解釋為命令,而不是逐字顯示的文本。
當(dāng)我們想改變命令行的文字樣式、位置等就可以使用這個(gè)標(biāo)準(zhǔn),比如我想打印紅色的文字,那么我會(huì)這樣寫(xiě):
console.log('\x1B[31m%s\x1B[0m', 'I am red');\x1B[是固定寫(xiě)法,31表示紅色字體背景,m表示設(shè)置代碼后面字符的顏色和樣式,%s是log方法第二個(gè)參數(shù)的占位符,0m表示打印完后還原正常樣式。執(zhí)行的效果如下

除了顏色當(dāng)然還可以設(shè)置其他樣式,例如下劃線(xiàn)等,替換code即可,具體可以參考:ANSI escape code - HandWiki 里的code表。
// 打印紅色文字
console.log('\x1B[31m%s\x1B[0m', 'I am red');
// 打印文字添加下劃線(xiàn)
console.log('\x1B[4m%s\x1B[0m', 'underline');
// 紅色文字下劃線(xiàn)
console.log('\x1B[31m\x1B[4m%s\x1B[0m', 'I am red and underline');除了樣式還可以處理光標(biāo)位置,比如想打印完一行后隔兩行再繼續(xù)打印,則可以這樣寫(xiě):
// 紅色文字下劃線(xiàn)
console.log('\x1B[31m\x1B[4m%s\x1B[0m', 'I am red and underline');
// cursor 移動(dòng)
console.log('\x1B[2B%s\x1B[0m', 'I am red and underline');將m換成B,B前面的數(shù)字表示要空幾行,效果如下:

還有更多的可能你想要的效果,可以查看 ANSI escape code - HandWiki
rxjs
RxJS 是一個(gè)用于處理異步編程的 JavaScript 庫(kù),目標(biāo)是使編寫(xiě)異步和基于回調(diào)的代碼更容易。
自己開(kāi)發(fā)一個(gè)命令行列表選擇功能
流程圖

代碼實(shí)現(xiàn)
首先模仿inquirer的使用方式搭一個(gè)簡(jiǎn)單的架子:
const option = {
type: 'list',
name: 'name',
message: 'select a name',
choices: [
{ name: 'sam', value: 'sam' },
{ name: 'tom', value: 'tom' },
{ name: 'jerry', value: 'jerry' }
]
}
function prompt(option) {
return new Promise((resolve, reject) => {})
}
prompt(option).then((answer) => {
console.log(answer)
})然后實(shí)現(xiàn)一個(gè)List類(lèi),由它來(lái)做具體的實(shí)現(xiàn):
const { EventEmitter } = require('events');
const rl = require('readline');
const MuteStream = require('mute-stream');
const { fromEvent } = require('rxjs');
const ansi = require('ansi-escapes');
class List extends EventEmitter {
constructor(option) {
super();
this.name = option.name;
this.message = option.message;
this.choices = option.choices;
this.input = process.stdin;
// 通過(guò)mute-stream來(lái)實(shí)現(xiàn)控制臺(tái)的輸入輸出
const ms = new MuteStream();
ms.pipe(process.stdout);
this.output = ms;
// 創(chuàng)建readline接口
this.rl = rl.createInterface({
input: this.input,
output: this.output,
});
// 默認(rèn)選中
this.selected = option.default || 0;
this.height = this.choices.length + 1;
// 監(jiān)聽(tīng)keypress事件
this.keypress = fromEvent(this.rl.input, 'keypress').subscribe(
this.onKeypress
);
// 是否選擇完畢
this.done = false;
}
// 鍵盤(pán)事件
onKeypress = (keyMap) => {
// 獲取key
const key = keyMap[1];
if (key.name === 'up') { // 上鍵點(diǎn)擊
if (this.selected > 0) {
this.selected--;
}
} else if (key.name === 'down') { // 下鍵點(diǎn)擊
if (this.selected < this.choices.length - 1) {
this.selected++;
}
} else if (key.name === 'return') { // 回車(chē)點(diǎn)擊
this.done = true;
}
this.render();
// 完成選擇后退出
if (this.done) {
this.close()
this.emit('exit', this.choices[this.selected])
}
}
render() {
// 解除mute狀態(tài)
this.output.unmute();
// 清除控制臺(tái)
this.clean();
// 寫(xiě)入list內(nèi)容
this.output.write(this.getContent());
// 開(kāi)啟mute狀態(tài) 限制用戶(hù)不可輸入
this.output.mute();
}
getContent() {
if (this.done) {
return `\x1B[1m${this.message} \x1B[22m\x1B[36m${this.choices[this.selected].name}\x1B[39m\n`;
} else {
const title = `\x1B[32m?\x1B[39m \x1B[1m${this.message}(use arrow keys)\x1B[22m\n`;
const list = this.choices.map((item, index) => {
// 選中的選項(xiàng)前面加上?
if (index === this.selected) {
return `\x1B[36m? ${item.name}\x1B[39m`;
}
return ` ${item.name}`;
});
return title + list.join('\n');
}
}
// 清除控制臺(tái)
clean() {
const emptyLines = ansi.eraseLines(this.height);
this.output.write(emptyLines);
}
close() {
this.output.unmute();
this.rl.output.end();
this.rl.pause();
this.rl.close();
this.keypress.unsubscribe();
}
}然后使用List
function prompt(option) {
return new Promise((resolve, reject) => {
try {
const list = new List(option);
// 渲染列表
list.render();
list.on('exit', (answer) => {
resolve(answer);
});
} catch (error) {
reject(error);
}
});
}
prompt(option).then((answer) => {
console.log('answer', answer);
});運(yùn)行效果就是這樣:

到此這篇關(guān)于淺析node命令行交互原理的文章就介紹到這了,更多相關(guān)node 命令行交互內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
nodejs連接mongodb數(shù)據(jù)庫(kù)實(shí)現(xiàn)增刪改查
本篇文章主要結(jié)合了nodejs操作mongodb數(shù)據(jù)庫(kù)實(shí)現(xiàn)增刪改查,包括對(duì)數(shù)據(jù)庫(kù)的增加,刪除,查找和更新,有興趣的可以了解一下。2016-12-12
NodeJS設(shè)計(jì)模式總結(jié)【單例模式,適配器模式,裝飾模式,觀察者模式】
這篇文章主要介紹了NodeJS設(shè)計(jì)模式,結(jié)合實(shí)例形式總結(jié)分析了nodejs單例模式,適配器模式,裝飾模式,觀察者模式的概念、原理與具體實(shí)現(xiàn)技巧,需要的朋友可以參考下2017-09-09
nodejs實(shí)現(xiàn)bigpipe異步加載頁(yè)面方案
本文給大家分享的是使用nodejs結(jié)合bigpipe實(shí)現(xiàn)異步加載頁(yè)面的方案,非常的實(shí)用,也是以后前端性能優(yōu)化的一個(gè)方向,希望大家能夠喜歡。2016-01-01
詳解IWinter 一個(gè)路由轉(zhuǎn)控制器的 Nodejs 庫(kù)
本篇文章主要介紹了詳解IWinter 一個(gè)路由轉(zhuǎn)控制器的 Nodejs 庫(kù),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-11-11
NodeJS感知和控制自身進(jìn)程的運(yùn)行環(huán)境和狀態(tài)
NodeJS可以感知和控制自身進(jìn)程的運(yùn)行環(huán)境和狀態(tài),也可以創(chuàng)建子進(jìn)程并與其協(xié)同工作,這使得NodeJS可以把多個(gè)程序組合在一起共同完成某項(xiàng)工作,并在其中充當(dāng)膠水和調(diào)度器的作用,和進(jìn)程管理相關(guān)的API單獨(dú)介紹起來(lái)比較枯燥,這里從一些典型的應(yīng)用場(chǎng)景出發(fā)2024-01-01

