命令列應用開發初學者指南:腳手架篇、UI 庫和互動工具

一颗冰淇淋發表於2024-11-27

在日常的前端開發工作中,我們經常依賴各種命令列工具來提高效率和程式碼質量。例如,create-react-appeslint 等工具不僅簡化了專案的初始化過程,還能自動執行程式碼檢查和格式化任務。當我們使用這些工具時,它們通常會透過一系列互動式的問答來收集必要的資訊,從而根據我們的選擇進行相應的配置和安裝。

eslint 工具為例(如下圖所示),當你首次執行 eslint --init 命令時,它會引導你完成一系列選擇題,包括你使用的框架(如 ReactVue.js 或其他),以及其他配置選項。透過這種方式,eslint 能夠為你生成一個最適合專案需求的配置檔案。

本篇文章將介紹在開發命令列工具過程中常用的第三方庫。這些庫主要分為三類:

  • 腳手架框架:用於解析命令列引數,例如 eslint --init 中的 --init。常用的腳手架框架有 yargscommand

  • 命令列輸出美化庫:基於 ANSI Escape 規範,用於對命令列輸出進行顏色和樣式美化。常用的美化庫有 chalkora

  • 互動式命令列庫:用於建立互動式的命令列介面,例如 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}...`);
  }
}

在日常開發中,我們通常不會自己去解析命令列引數,因為這涉及到大量的邊界情況和錯誤處理。使用社群廣泛認可的第三方庫可以更加高效和嚴謹。其中,yargscommander 是兩個非常優秀的推薦庫。

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 listice-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

註冊指令

commanderyargs 在註冊指令的語法上有一些區別。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"。

yargscommander 解析命令列引數,生成幫助資訊,並註冊命令。它們提供了強大的命令列介面構建能力,使得命令列工具更加靈活和易用。

命令列輸出美化庫

在進行命令列互動時,經常需要對某些內容加粗、加字型顏色,以區分使用者選中的內容和需要重點關注的問題。為了實現這些效果,存在一個命令列渲染標準,稱為 ANSI escape code。此外,還有一些成熟的第三方庫,如 chalkora,可以幫助我們更方便地實現這些功能。

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 來實現顏色和樣式變化,但在實際開發中這樣做會非常繁瑣。你需要手動定義跳脫字元、分隔符,還要查詢每個顏色對應的編碼。幸運的是,已經有成熟的第三方庫可以幫助我們解決這些問題,例如 chalkora

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 初始化過程中會詢問使用者當前使用的框架是 ReactVue 還是其他框架。使用者可以透過鍵盤的上下左右鍵和回車進行選擇。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 成為一個非常實用的命令列互動庫。

透過結合這些工具和庫,可以輕鬆地構建出功能強大、使用者體驗良好的命令列應用。

如果你對前端工程化有興趣,或者想了解更多相關的內容,歡迎檢視我的其他文章,這些內容將持續更新,希望能給你帶來更多的靈感和技術分享~

相關文章