[譯] 如何使用 Node.js 構建一個命令行應用(CLI)

atZ3n9vMFjjXDl_XxDtL_FCRSOt6EF0d8LnbMRCCJQUesMme8lzdGpCyMr4-wt1nlIGuoT29EI_tkVpuD_P2mxzbfhbn-ZPcqmZ5QCY_nM9d4ywWEYQxKYc9mjxUnp_uFJzMOMnr

Node.js 內建的命令行應用(CLI)讓你可以在使用其龐大的生態系統的同時自動化地執行重複性的任務。而且,多虧了像 npmyarn 這樣的包管理工具,讓這些命令行應用能夠很容易就在多個平臺上分發和使用。在本篇文章中,我將會講述爲什麼須要寫 CLI,如何使用 Node.js 完成它,一些實用的包,以及你如何發佈你新寫好的 CLI。javascript

爲何要用 Node.js 建立命令行應用

Node.js 可以如此流行的緣由之一就是它有豐富的包生態系統,現在在 npm 註冊處 已經有超過 900000 個包。經過在 Node.js 中寫你本身的 CLI,你就能夠進入這個生態系統,而其中也包含了鉅額數目的針對 CLI 的包。包括:html

  • inquirerenquirer 或者 prompts,可用於處理複雜的輸入提示
  • email-prompt 可方便地提示郵箱輸入
  • chalkkleur 可用於彩色輸出
  • ora 是一個好看的加載提示
  • boxen 能夠用於在你的輸出外加上邊框
  • stmux 能夠提供一個和 tmux 相似的多終端界面
  • listr 能夠展現進程列表
  • ink 可使用 React 構建 CLI
  • meow 或者 arg 能夠用於基本的參數解析
  • commanderyargs 能夠用來比較複雜的參數解析,並支持子命令
  • oclif 是一個用於構建可擴展 CLI 的框架,做者是 Heroku(gluegun 可做爲替換方案)

還有不少方便的方法能夠用來使用 CLI,它們都發布在 npm 上,能夠同時使用 yarnnpm 進行管理。例如 create-flex-plugin,是一個能夠用來爲 Twilio Flex 建立插件的 CLI。你可使用全局命令來安裝它:前端

# 使用 npm 安裝:
npm install -g create-flex-plugin
# 使用 yarn 安裝:
yarn global add create-flex-plugin
# 安裝以後你就可使用了:
create-flex-plugin
複製代碼

或者它也能夠做爲項目依賴:java

# 使用 npm 安裝:
npm install create-flex-plugin --save-dev
# 使用 yarn 安裝:
yarn add create-flex-plugin --dev
# 安裝以後命令將被保存在
./node_modules/.bin/create-flex-plugin
# 或者經過由 npm 支持的 npx 使用:
npx create-flex-plugin
# 以及經過 yarn 使用:
yarn create-flex-plugin
複製代碼

事實上,npx 能支持在沒有安裝的時候就執行 CLI。只須要運行 npx create-flex-plugin,這時候若是找不到本地或者全局的已安裝版本,它將會自動下載這個包並放入緩存中。node

npm 6.1 版本後,npm inityarn 都支持使用 CLI 來構建項目,命令的名字形如 create-*。例如,剛纔說的 create-flex-plugin,咱們要作的就是:react

# 使用 Node.js:
npm init flex-plugin
# 使用 Yarn:
yarn create flex-plugin
複製代碼

構建第一個 CLI

若是你更喜歡看視頻學習,點擊這裏在 YouTube 觀看教程android

目前咱們已經解釋過用 Node.js 建立 CLI 的緣由,如今就讓咱們開始構建一個 CLI 吧。在本篇教程裏,咱們會使用 npm,但若是你想用 yarn,絕大多數的命令也都是相同的。確保你的系統中已經安裝了 Node.jsnpmios

本篇教程中,咱們將會建立一個 CLI,經過運行命令 npm init @your-username/project,它能夠根據你的偏好構建一個新的項目。git

經過運行以下代碼,開始一個新的 Node.js 項目:es6

mkdir create-project && cd create-project
npm init --yes
複製代碼

以後在項目的根目錄下建立一個名爲 src/ 的目錄,而後將一個名爲 cli.js 的文件放在這個目錄下,並在文件中寫入代碼:

export function cli(args) {
 console.log(args);
}
複製代碼

在這個函數中,咱們將會解析參數邏輯並觸發實際須要的業務邏輯。接下來,咱們須要建立 CLI 的入口。在項目根目錄下建立目錄 bin/ 而後建立一個名爲 create-project 的文件。寫入代碼:

#!/usr/bin/env node

require = require('esm')(module /*, options*/);
require('../src/cli').cli(process.argv);
複製代碼

在這一小片代碼中,完成了幾件事情。首先,咱們引入了一個名爲 esm 的模塊,這個模塊讓咱們能在其餘文件中使用 import。這和構建 CLI 並不直接相關,可是本篇教程中咱們須要使用 ES 模塊,而包 esm 讓咱們能在 Node.js 版本不支持時無需代碼轉換而使用 ES 模塊。而後咱們引入 cli.js 文件並調用函數 cli,並將 process.argv 傳入,它是從命令行傳入函數腳本的參數數組。

在咱們測試腳本以前,須要經過運行以下命令安裝 esm 依賴:

npm install esm
複製代碼

另外,咱們還要將暴露 CLI 腳本的需求同步給包管理器。方法是在 package.json 文件中添加合適的入口。別忘了也要更新屬性 descriptionnamekeywordmain

{
 "name": "@your_npm_username/create-project",
 "version": "1.0.0",
 "description": "A CLI to bootstrap my new projects",
 "main": "src/index.js",
 "bin": {
   "@your_npm_username/create-project": "bin/create-project",
   "create-project": "bin/create-project"
 },
 "publishConfig": {
   "access": "public"
 },
 "scripts": {
   "test": "echo \"Error: no test specified\" && exit 1"
 },
 "keywords": [
   "cli",
   "create-project"
 ],
 "author": "YOUR_AUTHOR",
 "license": "MIT",
 "dependencies": {
   "esm": "^3.2.18"
 }
}
複製代碼

若是你注意到 bin 屬性,你會發現咱們將其定義爲一個具備兩個鍵值對的對象。這個對象內定義的是包管理器將會安裝的 CLI 命令。在上述的例子中,咱們爲同一段腳本註冊了兩個命令。一個經過加上了咱們的用戶名來使用本身的 npm 做用域,另外一個是爲了方便使用的通用的 create-project 命令。

作好了這些,咱們能夠測試腳本了。最簡單的測試方法是使用 npm link 命令。在你的項目終端中運行:

npm link
複製代碼

這個命令將會全局地安裝你當前項目的連接,因此當你更新代碼的時候,也並不須要從新運行 npm link 命令。在運行 npm link 命令後,你的 CLI 命令應該已經可用了。試着運行:

create-project
複製代碼

你應該能夠看到相似的輸出:

[ '/usr/local/Cellar/node/11.6.0/bin/node',
  '/Users/dkundel/dev/create-project/bin/create-project' ]
複製代碼

注意,這兩個地址依賴於你的項目地址和 Node.js 安裝地址,並會隨之變化而不一樣。而且這個數組會隨着你增長參數而變長。試試運行:

create-project --yes
複製代碼

此時輸出能夠反映出添加了新的參數:

[ '/usr/local/Cellar/node/11.6.0/bin/node',
  '/Users/dkundel/dev/create-project/bin/create-project',
  '--yes' ]
複製代碼

參數解析與輸入處理

如今咱們準備解析傳入腳本的參數,並賦予其邏輯意義。咱們的 CLI 支持一個參數及多個選項:

  • [template]:咱們支持開箱即用的多模版。若是用戶沒有傳入這個參數,咱們將給出提示讓用戶選擇
  • --git:它將會運行 git init,來實例化一個新的 git 項目
  • --install:它將會自動地爲項目安裝全部依賴
  • --yes:它將會跳過全部提示,直接使用默認選項

對於咱們的項目,將會使用 inquirer 來提示輸入參數,並使用 arg 庫來解析 CLI 參數。經過運行以下命令來安裝依賴:

npm install inquirer arg
複製代碼

首先咱們來寫解析參數的邏輯,解析過程將會把參數解析爲一個 options 對象,供咱們使用。將以下代碼加入到 cli.js 中:

import arg from 'arg';

function parseArgumentsIntoOptions(rawArgs) {
 const args = arg(
   {
     '--git': Boolean,
     '--yes': Boolean,
     '--install': Boolean,
     '-g': '--git',
     '-y': '--yes',
     '-i': '--install',
   },
   {
     argv: rawArgs.slice(2),
   }
 );
 return {
   skipPrompts: args['--yes'] || false,
   git: args['--git'] || false,
   template: args._[0],
   runInstall: args['--install'] || false,
 };
}

export function cli(args) {
 let options = parseArgumentsIntoOptions(args);
 console.log(options);
}
複製代碼

運行 create-project --yes,你將能看到 skipPrompt 會變成 true,或者試着傳遞其餘參數例如 create-project cli,那麼 template 屬性就會被設置。

如今咱們已經能解析 CLI 參數了,咱們還須要添加方法來提示用戶輸入參數信息,以及當 --yes 標誌被輸入的時候,略過提示信息並使用默認參數。將以下代碼加入 cli.js 文件:

import arg from 'arg';
import inquirer from 'inquirer';

function parseArgumentsIntoOptions(rawArgs) {
// ...
}

async function promptForMissingOptions(options) {
 const defaultTemplate = 'JavaScript';
 if (options.skipPrompts) {
   return {
     ...options,
     template: options.template || defaultTemplate,
   };
 }

 const questions = [];
 if (!options.template) {
   questions.push({
     type: 'list',
     name: 'template',
     message: 'Please choose which project template to use',
     choices: ['JavaScript', 'TypeScript'],
     default: defaultTemplate,
   });
 }

 if (!options.git) {
   questions.push({
     type: 'confirm',
     name: 'git',
     message: 'Initialize a git repository?',
     default: false,
   });
 }

 const answers = await inquirer.prompt(questions);
 return {
   ...options,
   template: options.template || answers.template,
   git: options.git || answers.git,
 };
}

export async function cli(args) {
 let options = parseArgumentsIntoOptions(args);
 options = await promptForMissingOptions(options);
 console.log(options);
}
複製代碼

保存文件並運行 create-project,你將會看到這樣的模版選擇提示:

以後,你將會被詢問是否要初始化 git。兩個問題都做出先擇後,你將看到打印出了這樣的輸出:

{ skipPrompts: false,
  git: false,
  template: 'JavaScript',
  runInstall: false }
複製代碼

嘗試運行 create-project -y 命令,此時全部的提示都會被忽略。你將會立刻看到命令行輸入的選項:

編寫代碼邏輯

如今咱們已經能夠經過提示信息以及命令行參數來決定對應的邏輯選項,下面咱們來寫可以建立項目的邏輯代碼。咱們的 CLI 將會和 npm init 命令相似,寫入一個已經存在的目錄,並會將全部在 templates 目錄下的文件拷貝到項目中。咱們也容許經過選項修改目標目錄地址,這樣你能夠在其餘項目中重用這段邏輯。

在咱們寫邏輯代碼以前,在項目根目錄下建立一個名爲 templates 的目錄,並將目錄 typescriptjavascript 放在此目錄下。它們的名字都是小寫的版本,咱們將會提示用戶從中選擇一個。在本篇文章中,咱們就使用這兩個名字,但其實你可使用你任意喜歡的命名。在這個目錄下,放入文件 package.json 並加入任意你須要的項目基礎依賴,以及任意你須要拷貝到項目中的文件。以後咱們的代碼將會把這些文件全都拷貝到新的項目中。若是你須要一些創做靈感,你能夠在 github.com/dkundel/create-project 查看我使用的文件。

爲了遞歸的拷貝全部的文件,咱們將會使用一個名爲 ncp 的庫。這個庫可以支持跨平臺的遞歸拷貝,甚至有標識能夠支持強制覆蓋已有文件。另外,爲了可以展現彩色輸出,咱們還將安裝 chalk。運行以下代碼來安裝依賴:

npm install ncp chalk
複製代碼

咱們將會把項目核心的邏輯都放到 src/ 目錄下的 main.js 文件中。建立新文件並將以下代碼加入:

import chalk from 'chalk';
import fs from 'fs';
import ncp from 'ncp';
import path from 'path';
import { promisify } from 'util';

const access = promisify(fs.access);
const copy = promisify(ncp);

async function copyTemplateFiles(options) {
 return copy(options.templateDirectory, options.targetDirectory, {
   clobber: false,
 });
}

export async function createProject(options) {
 options = {
   ...options,
   targetDirectory: options.targetDirectory || process.cwd(),
 };

 const currentFileUrl = import.meta.url;
 const templateDir = path.resolve(
   new URL(currentFileUrl).pathname,
   '../../templates',
   options.template.toLowerCase()
 );
 options.templateDirectory = templateDir;

 try {
   await access(templateDir, fs.constants.R_OK);
 } catch (err) {
   console.error('%s Invalid template name', chalk.red.bold('ERROR'));
   process.exit(1);
 }

 console.log('Copy project files');
 await copyTemplateFiles(options);

 console.log('%s Project ready', chalk.green.bold('DONE'));
 return true;
}
複製代碼

這段代碼會導出一個名爲 createProject 的新函數,這個函數會首先檢查指定的模版是不是可用的,檢查的方法是使用 fs.access 來檢查文件的可讀性(fs.constants.R_OK),而後使用 ncp 將文件拷貝到指定的目錄下。另外,在拷貝成功後,咱們還要輸出一些帶顏色的日誌,內容爲 DONE Project ready

以後,更新 cli.js,加入對新函數 createProject 的調用:

import arg from 'arg';
import inquirer from 'inquirer';
import { createProject } from './main';

function parseArgumentsIntoOptions(rawArgs) {
// ...
}

async function promptForMissingOptions(options) {
// ...
}

export async function cli(args) {
 let options = parseArgumentsIntoOptions(args);
 options = await promptForMissingOptions(options);
 await createProject(options);
}
複製代碼

爲了測試咱們的進度,在你的系統中某個位置例如 ~/test-dir 中建立一個新目錄,而後在這個文件夾內使用某個模版運行命令。好比:

create-project typescript --git
複製代碼

你應該能看到一個通知,代表項目已經被建立,而且文件已經被拷貝到了這個目錄下。

如今還有另外兩步須要作。咱們但願可配置的初始化 git 並安裝依賴。爲了完成這個,咱們須要另外三個依賴:

  • execa 用於讓咱們能在代碼中很便捷的運行像 git 這樣的外部命令
  • pkg-install 用於基於用戶使用什麼而觸發命令 yarn installnpm install
  • listr 讓咱們能指定任務列表,並給用戶一個整齊的進程概覽

經過運行以下命令來安裝依賴:

npm install execa pkg-install listr
複製代碼

以後更新 main.js,加入以下代碼:

import chalk from 'chalk';
import fs from 'fs';
import ncp from 'ncp';
import path from 'path';
import { promisify } from 'util';
import execa from 'execa';
import Listr from 'listr';
import { projectInstall } from 'pkg-install';

const access = promisify(fs.access);
const copy = promisify(ncp);

async function copyTemplateFiles(options) {
 return copy(options.templateDirectory, options.targetDirectory, {
   clobber: false,
 });
}

async function initGit(options) {
 const result = await execa('git', ['init'], {
   cwd: options.targetDirectory,
 });
 if (result.failed) {
   return Promise.reject(new Error('Failed to initialize git'));
 }
 return;
}

export async function createProject(options) {
 options = {
   ...options,
   targetDirectory: options.targetDirectory || process.cwd()
 };

 const templateDir = path.resolve(
   new URL(import.meta.url).pathname,
   '../../templates',
   options.template
 );
 options.templateDirectory = templateDir;

 try {
   await access(templateDir, fs.constants.R_OK);
 } catch (err) {
   console.error('%s Invalid template name', chalk.red.bold('ERROR'));
   process.exit(1);
 }

 const tasks = new Listr([
   {
     title: 'Copy project files',
     task: () => copyTemplateFiles(options),
   },
   {
     title: 'Initialize git',
     task: () => initGit(options),
     enabled: () => options.git,
   },
   {
     title: 'Install dependencies',
     task: () =>
       projectInstall({
         cwd: options.targetDirectory,
       }),
     skip: () =>
       !options.runInstall
         ? 'Pass --install to automatically install dependencies'
         : undefined,
   },
 ]);

 await tasks.run();
 console.log('%s Project ready', chalk.green.bold('DONE'));
 return true;
}
複製代碼

這段代碼將會在傳入 --git 或者用戶在提示中選擇了 git 的時候運行 git init,而且會在傳入 --install 的時候運行 npm install 或者 yarn,不然它將會跳過這兩個任務,並用一段消息通知用戶若是他們想要自動安裝,請傳入 --install

首先刪除掉已經存在的測試文件夾而後建立一個新的,而後試一下效果如何。運行命令:

create-project typescript --git --install
複製代碼

在你的文件夾中,你應該能看到 .git 文件夾和 node_modules 文件夾,表示 git 已經被初始化,以及 package.json 中指定的依賴已經被安裝了。

恭喜你,你的第一個 CLI 已經整裝待發了!

若是你但願你的代碼能做爲實際的模塊使用,這樣其餘人能夠在他們的代碼中複用你的邏輯,你還須要在目錄 src/ 下添加文件 index.js,這個文件暴露出了 main.js 的內容:

require = require('esm')(module);
require('../src/cli').cli(process.argv);
複製代碼

接下來作什麼?

如今你的 CLI 代碼已經準備好,你能夠由此爲基礎,向更多的方向發展。若是你僅僅想本身使用,而不想和其餘人分享,那麼你就須要繼續沿用 npm link 便可。事實上,運行 npm init project 試試看,你的代碼也將被觸發。

若是你想要和其餘人分享你的代碼模版,你能夠將代碼推送到 GitHub 來供參閱,或者更好的方法是,使用 npm publish 將它做爲一個包推送到 npm 註冊處。在你發佈以前,你還須要確保在 package.json 文件中添加一個 files 屬性,來指明那些文件應該被髮布:

},
 "files": [
   "bin/",
   "src/",
   "templates/"
 ]
}
複製代碼

若是你想要檢查那個文件將會被髮布,運行 npm pack --dry-run 而後查看輸出。以後使用 npm publish 來發布你的 CLI。你能夠在 @dkundel/create-project 找到個人項目,或者試試看運行 npm init @dkundel/project

還有不少的功能你能夠加入進來。在個人項目中,我還添加了一些依賴,用於爲我建立 LICENSECODE_OF_CONDUCT.md.gitignore。你能夠在 GitHub 找到實現這些功能的源代碼,或着查看上面提到的倉庫來擴充附加功能。若是你發現某個你以爲應該被列出在文章中而我並無列出的庫,或者想要給我看你的 CLI,儘管發送消息給我!

使用 JavaScript 還能夠構建更多:

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索