在日常的前端開發工作中,我們經常依賴各種命令行工具來提高效率和代碼質量。例如,create-react-app 和 eslint 等工具不僅簡化了項目的初始化過程,還能自動執行代碼檢查和格式化任務。當我們使用這些工具時,它們通常會通過一系列互動式的問答來收集必要的信息,從而根據我們的選擇進行相應的配置和安裝。
以 eslint 工具為例(如下圖所示),當你首次運行 eslint --init 命令時,它會引導你完成一系列選擇題,包括你使用的框架(如 React、Vue.js 或其他),以及其他配置選項。通過這種方式,eslint 能夠為你生成一個最適合項目需求的配置文件。
本篇文章將介紹在開發命令行工具過程中常用的第三方庫。這些庫主要分為三類:
- 腳手架框架:用於解析命令行參數,例如
eslint --init中的 --init。常用的腳手架框架有yargs和command。 - 命令行輸出美化庫:基於
ANSI Escape規範,用於對命令行輸出進行顏色和樣式美化。常用的美化庫有chalk和ora。 - 交互式命令行庫:用於創建交互式的命令行界面,例如
eslint初始化過程中會提出的問答,如 "Which framework does your project use?"。常用的交互式命令行庫有inquirer。
腳手架框架
首先,讓我們簡單回顧一下 Node.js 腳手架的開發流程:
- 創建 npm 項目:使用
npm init命令創建一個新的 npm 項目,並填寫相關項目信息。 - 創建腳手架入口文件:在項目根目錄下創建一個入口文件,例如 index.js,並在文件頂部添加
#!/usr/bin/env node以便將其識別為可執行文件。 - 配置 package.json:在
package.json文件中添加bin屬性,指定腳手架的入口文件路徑。 - 添加 npm link:使用
npm link命令將項目鏈接到全局環境中,這樣就可以在本地通過短指令訪問腳手架。
詳細的創建及實現功能邏輯可以參考文章《Node.js 構建命令行工具:實現 ls 命令的 -a 和 -l 選項》
例如,假設我們創建了一個名為 ice-cli 的項目,並在其中添加了 --init 指令的執行邏輯。那麼,我們如何知道用户輸入了這項指令呢?這就需要我們在項目中解析命令行參數。
自行解析參數
在 Node.js 中,我們可以利用內置的 process 對象來解析命令行參數。具體來説,可以在入口文件 index.js 中執行以下代碼:
const argv = require("process").argv;
console.log("argv", argv);
當用户在命令行中輸入 ice-cli create project --help 這一長串指令後,通過 process.argv 獲取到的是一個數組。數組的第一個元素代表 Node.js 的執行路徑,第二個元素代表當前指令文件的路徑,從第三個元素開始則是用户輸入的內容。
例如,對於命令 ice-cli create project --help,process.argv 的輸出可能如下所示:
[
'/usr/local/bin/node', // Node.js 執行路徑
'/usr/local/lib/node_modules/ice-cli/index.js', // 當前指令文件路徑
'create', // 用户輸入的第一個參數
'project', // 用户輸入的第二個參數
'--help' // 用户輸入的第三個參數
]
拿到用户輸入的內容後,我們需要對其進行進一步的拆分和處理。用户輸入的內容通常包括 命令(command) 和 選項(options)。例如,在命令 webpack config ./webpack.config.js 中,config 是命令,./webpack.config.js 是命令後面的參數;而在命令 webpack --help 中,--help 是選項。
通過解析 process.argv 數組,我們可以提取出命令和選項,並根據它們執行相應的邏輯。例如:
const command = argv[2]; // 獲取命令
const args = argv.slice(3); // 獲取命令後面的參數
if (command === 'create') {
if (args.includes('--help')) {
console.log('Usage: ice-cli create <project-name>');
} else {
const projectName = args[0];
console.log(`Creating project ${projectName}...`);
}
}
在日常開發中,我們通常不會自己去解析命令行參數,因為這涉及到大量的邊界情況和錯誤處理。使用社區廣泛認可的第三方庫可以更加高效和嚴謹。其中,yargs 和 commander 是兩個非常優秀的推薦庫。
yargs
yargs 是一個功能強大且易於使用的命令行參數解析庫。它提供了豐富的 API,可以幫助你輕鬆地解析命令和選項,並生成詳細的幫助信息。
安裝 yargs
首先,通過 npm 安裝 yargs:
npm install yargs
實現 --help 和 --version 功能
通過簡單的代碼就可以實現 --help 和 --version 功能:
const yargs = require("yargs/yargs");
const { hideBin } = require("yargs/helpers");
const arg = hideBin(process.argv);
yargs(arg).argv;
運行 ice-cli --help ,結果如下圖所示:
常用屬性
yargs 採用鏈式調用的方式為命令設置屬性。以下是一些常用的屬性:
- usage() :在輸入 --help 時會顯示的提示信息。
- demandCommand() :最少要輸入的命令數量,以及當沒有輸入命令時的提示。
- recommendCommands() :如果輸入的指令不完整,會給出最近似命令的提示,例如:“Did you mean xx?”
- strict() :嚴格模式,輸入錯誤命令時會給出提示。
- alias() :為指令取別名。
- options() :定義多個全局選項,在任何場景都可以訪問到。
- option() :定義單個全局選項,在任何場景都可以訪問到。
- group() :將一些命令聚合到一個分類中。
- command() :定義指令。
- epilogue() :定義結尾信息。
示例代碼
將上述命令組合起來,示例如下:
const yargs = require("yargs/yargs");
const { hideBin } = require("yargs/helpers");
const arg = hideBin(process.argv);
const cli = yargs(arg);
cli
.usage("Usage: ice-ls [command] <options>")
.demandCommand(
1,
"A command is required. Pass --help to see all avaiable commands and options."
)
.recommendCommands()
.strict()
.alias("h", "help")
.options({
debug: {
type: "boolean",
describe: "Bootstarap debug mode",
alias: "d",
},
})
.group(["debug"], "Dev options:")
.command({
command: "list",
aliases: ["ls", "la", "ll"],
describe: "List total packages",
builder: (argv) => {
console.log("builder", argv);
},
handler: (argv) => {
console.log("handler", argv);
},
})
.epilogue("You own footer description").argv;
當執行 ice-ls 命令時,輸出如下:
- 第一行出現
usage函數配置的提示:Usage: ice-ls [command] <options>。 - 接着是
command函數配置的指令 list,以及它的別名 ls, la, ll。 - 然後是通過
group函數分組的 Dev options。 - 下面是
yargs默認提供的選項 --version 和 --help。 - 接着是
epilogue函數配置的尾部描述。 - 最後一行是
demandCommand提示: A command is required. Pass --help to see all avaiable commands and options。因為執行命令 ice-ls 的時候沒有提供具體指令。
執行 ice-cli list 和 ice-ls list --debug,此時程序進入 command 函數中,執行 builder 函數 和 handler 函數,可以在這裏編寫實際的功能邏輯。
commander
commander 也是一個功能強大的命令行參數解析庫,但它在使用方式和 API 設計上和 yargs 有一些差異。
安裝 commander
首先,通過 npm 安裝 commander:
npm install commander
實現 --help 和 --version 功能
commander 通過簡單的配置就可以生成 usage 提示以及 --help 和 --version 功能。
const commander = require("commander");
const pkg = require("../package.json");
const program = new commander.Command();
program
.name(Object.keys(pkg.bin)[0])
.usage("<command> [options]")
.version(pkg.version);
program.parse(process.argv);
執行上述代碼後,運行 ice-cli --help 的輸出如下所示:
Usage: ice-cli <command> [options]
Options:
-V, --version output the version number
-h, --help display help for command
註冊指令
commander 和 yargs 在註冊指令的語法上有一些區別。yargs 使用鏈式調用,而 commander 註冊指令後返回值並不是自身,因此不能通過鏈式調用來註冊多個指令。
// 註冊 clone 命令
program
.command("clone <source> [destination]")
.description("clone a repository")
.option("-f --force", "是否強制克隆")
.usage("[options]")
.action((source, destination, cmdObj) => {
console.log("do clone", source, destination);
});
// 劫持所有未定義的指令
program
.arguments("<cmd> [options]")
.description("test command", {
cmd: "command to run",
options: "options for command",
})
.action((cmd, options) => {
console.log(cmd, options);
});
當執行 ice-cli clone a b 時,輸出為 "do clone a b"。當執行 ice-cli create c 時,輸出為 "create c"。
註冊子命令
commander 註冊子命令的方式也非常簡單。以下是一個示例:
const service = new commander.Command("service");
service
.command("start [port]")
.description("start service at some port")
.action((port) => {
console.log('>>>service start', port)
});
service
.command("stop")
.description("stop service")
.action(() => {
console.log('>>>service stop')
});
當執行 ice-cli service start 8000 時,會輸出 ">>>service start 8000"。當執行 ice-cli service stop 時,會輸出 ">>>service stop"。
yargs 和 commander 解析命令行參數,生成幫助信息,並註冊命令。它們提供了強大的命令行接口構建能力,使得命令行工具更加靈活和易用。
命令行輸出美化庫
在進行命令行交互時,經常需要對某些內容加粗、加字體顏色,以區分用户選中的內容和需要重點關注的問題。為了實現這些效果,存在一個命令行渲染標準,稱為 ANSI escape code。此外,還有一些成熟的第三方庫,如 chalk 和 ora,可以幫助我們更方便地實現這些功能。
ANSI escape code
ANSI escape code 是一種用於控制終端輸出的標準。通過特定的編碼序列,可以在命令行中實現顏色、加粗等效果。
示例
在 bin 文件夾下創建 ansi.js 文件,文件中定義如下代碼:
console.log("\x1B[31mThis text is red\x1B[0m");
通過 node 執行該 JS 文件,顯示的是紅色文本,內容為 "This text is red"。如圖所示:
編碼解析
即使我們沒有藉助任何第三方庫,僅通過一行文本就能實現命令行中的顏色和樣式變化。這一行看似“亂碼”的文本實際上是由 ANSI Escape Codes 組成的。下面是對這些字符的詳細拆解:
- \x1B:這是轉義字符,表示 ASCII 值為 27 的字符,也常表示為 ESC。它是所有 ANSI Escape Codes 的前綴。
- [:這是一個分隔符,表示接下來是一個控制序列。
- 31:這是一個數字代碼,表示設置前景色為紅色。
- m:這是一個終止符,表示控制序列的結束。
- \x1B[0m:重置所有文本屬性,包括顏色和樣式。
查找期望的樣式
要找到期望的樣式,可以在 ansi escape code 官網 查找。例如,31 代表紅色前景色,41 代表紅色背景色。
雖然可以直接使用 ANSI Escape Codes 來實現顏色和樣式變化,但在實際開發中這樣做會非常繁瑣。你需要手動定義轉義字符、分隔符,還要查找每個顏色對應的編碼。幸運的是,已經有成熟的第三方庫可以幫助我們解決這些問題,例如 chalk 和 ora。
chalk
chalk 是一個用於顏色渲染的庫,其語法非常簡單,通過方法名就能知道其用途。常見的方法包括:
rgb(r, g, b):定義自定義顏色。blue:設置藍色字體。bold:設置字體加粗。green:設置綠色字體。underline:設置下劃線。
這些方法名與 CSS 中的樣式名稱相似,使得使用起來非常直觀。chalk 支持多種使用形式,包括直接使用、拼接、鏈式調用、傳入多個參數和嵌套調用。
安裝
首先通過 npm 安裝 chalk
npm install chalk
基本使用
chalk 是以 ES module 方式實現的,需要通過 import 方式引入。如果希望在 Node.js 環境中運行,可以將文件後綴名定義為 .mjs。
例如以下代碼:
import chalk from "chalk";
// 直接使用
console.log("hello chalk");
// 定義自定義顏色
console.log(chalk.rgb(255, 0, 0)("hello nodejs"));
// 拼接不同樣式
console.log(chalk.blue.bold("hello ") + chalk.green("world"));
// 使用十六進制顏色
console.log(chalk.hex("#ff0000")("it is a nice day"));
// 鏈式調用和嵌套調用
console.log(
chalk.green(
"I am a green line " +
chalk.blue.underline.bold("with a blue substring") +
" that becomes green again!"
)
);
執行上述代碼後,命令行中的輸出效果如下所示:
ora
ora 是一個用於顯示加載動畫的庫,非常適合在命令行應用中顯示進度和狀態。它以 ES module 方式導出,需要定義 .mjs 文件。
安裝
首先通過 npm 安裝 ora:
npm install ora
基本使用
ora 在使用時需要手動調用開始和結束方法。以下是一個簡單的示例,顯示一個加載動畫:
import ora from "ora";
const spinner = ora({
text: "loading",
spinner: "dots",
}).start();
執行上述代碼後,命令行中會顯示一個持續的加載動畫,如下圖所示:
自定義屬性
ora 還支持定義其他屬性,如加載動畫效果、顏色、前綴文本等。
屬性説明:
- text:初始加載文本。
- spinner:加載動畫效果,可以是一個預定義的字符串(如 dots、line 等)或自定義對象。
- color:加載動畫的顏色。
- prefixText:加載文本的前綴。
- start():啓動加載動畫。
- stop():停止加載動畫。
- succeed(message):停止加載動畫並顯示成功消息。
- fail(message):停止加載動畫並顯示失敗消息。
- warn(message):停止加載動畫並顯示警告消息。
- info(message):停止加載動畫並顯示信息消息。
以下是一個更復雜的示例,展示瞭如何動態更新加載文本並最終停止加載動畫:
import ora from "ora";
const spinner = ora({
text: "loading",
spinner: "dots",
}).start();
// 設置加載顏色和前綴文本
spinner.color = "red";
spinner.prefixText = "download ora:";
let percent = 0;
let task = setInterval(() => {
percent += 10;
spinner.text = "Loading..." + percent + "%";
if (percent === 100) {
spinner.stop();
spinner.succeed("download success");
clearInterval(task);
}
}, 1000);
按以上邏輯,執行過程的中間態如下所示:
交互式命令行
在命令行應用中,經常會涉及到一些交互邏輯,例如在 eslint 初始化過程中會詢問用户當前使用的框架是 React、Vue 還是其他框架。用户可以通過鍵盤的上下左右鍵和回車進行選擇。inquirer 就是這樣一個用於命令行交互的第三方庫。
inquirer
安裝
inquirer 是一個強大的命令行交互庫,可以輕鬆地創建用户友好的命令行界面。
npm install inquirer
基本使用
以下是一個簡單的示例,展示瞭如何使用 inquirer 創建一個列表選擇:
import inquirer from "inquirer";
inquirer
.prompt([
{
type: "list",
name: "language",
message: "language",
choices: [
{
value: 1,
name: "react",
},
{
value: 2,
name: "vue",
},
{
value: 3,
name: "angular",
},
],
},
])
.then((res) => {
console.log("anwser", res);
});
執行上述代碼後,命令行中會出現一個選擇列表,如下圖所示:
多種交互類型
inquirer 支持多種交互類型,不僅限於列表選擇,還可以輸入文本、密碼、多選等。
以下是一個示例,展示了多種類型的交互問題:
import inquirer from "inquirer";
inquirer
.prompt([
{
type: "input",
name: "yourName",
message: "Your name",
},
{
type: "list",
name: "language",
message: "language",
choices: [
{
value: 1,
name: "react",
},
{
value: 2,
name: "vue",
},
{
value: 3,
name: "angular",
},
],
},
{
type: "expand",
name: "color",
message: "color",
choices: [
{
key: "R",
value: "red",
},
{
key: "G",
value: "green",
},
{
key: "B",
value: "blue",
},
],
},
{
type: "checkbox",
name: "fruits",
message: "fruits",
choices: [
{
value: 1,
name: "apple",
},
{
value: 2,
name: "banana",
},
{
value: 3,
name: "orange",
},
],
},
{
type: "password",
name: "password",
message: "password",
},
])
.then((res) => {
console.log("anwser", res);
});
執行上述代碼後,命令行中會依次顯示多個交互問題,最終,所有的用户輸入都會在 then 方法中統一獲取,如下圖所示:
通過 inquirer,你可以輕鬆地在命令行應用中實現各種交互邏輯。其豐富的交互類型和靈活的校驗機制使得 inquirer 成為一個非常實用的命令行交互庫。
通過結合這些工具和庫,可以輕鬆地構建出功能強大、用户體驗良好的命令行應用。
如果你對前端工程化有興趣,或者想了解更多相關的內容,歡迎查看我的其他文章,這些內容將持續更新,希望能給你帶來更多的靈感和技術分享~