在日常的前端開發工作中,我們經常依賴各種命令列工具來提高效率和程式碼質量。例如,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]。 - 接著是
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
成為一個非常實用的命令列互動庫。
透過結合這些工具和庫,可以輕鬆地構建出功能強大、使用者體驗良好的命令列應用。
如果你對前端工程化有興趣,或者想了解更多相關的內容,歡迎檢視我的其他文章,這些內容將持續更新,希望能給你帶來更多的靈感和技術分享~