- 原文地址:How to build a CLI with Node.js
- 原文作者:dkundel
- 譯文出自:掘金翻譯計劃
- 本文永久連結:github.com/xitu/gold-m…
- 譯者:EmilyQiRabbit
- 校對者:suhanyujie
Node.js 內建的命令列應用(CLI)讓你能夠在使用其龐大的生態系統的同時自動化地執行重複性的任務。並且,多虧了像 npm
和 yarn
這樣的包管理工具,讓這些命令列應用可以很容易就在多個平臺上分發和使用。在本篇文章中,我將會講述為何需要寫 CLI,如何使用 Node.js 完成它,一些實用的包,以及你如何釋出你新寫好的 CLI。
為什麼要用 Node.js 建立命令列應用
Node.js 能夠如此流行的原因之一就是它有豐富的包生態系統,如今在 npm
註冊處 已經有超過 900000 個包。通過在 Node.js 中寫你自己的 CLI,你就可以進入這個生態系統,而其中也包含了鉅額數目的針對 CLI 的包。包括:
inquirer
,enquirer
或者prompts
,可用於處理複雜的輸入提示email-prompt
可方便地提示郵箱輸入chalk
或kleur
可用於彩色輸出ora
是一個好看的載入提示boxen
可以用於在你的輸出外加上邊框stmux
可以提供一個和tmux
類似的多終端介面listr
可以展示程式列表ink
可以使用 React 構建 CLImeow
或者arg
可以用於基本的引數解析commander
和yargs
可以用來比較複雜的引數解析,並支援子命令oclif
是一個用於構建可擴充套件 CLI 的框架,作者是 Heroku(gluegun
可作為替換方案)
還有很多方便的方法可以用來使用 CLI,它們都發布在 npm
上,可以同時使用 yarn
和 npm
進行管理。例如 create-flex-plugin
,是一個可以用來為 Twilio Flex 建立外掛的 CLI。你可以使用全域性命令來安裝它:
# 使用 npm 安裝:
npm install -g create-flex-plugin
# 使用 yarn 安裝:
yarn global add create-flex-plugin
# 安裝之後你就可以使用了:
create-flex-plugin
複製程式碼
或者它也可以作為專案依賴:
# 使用 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
,這時候如果找不到本地或者全域性的已安裝版本,它將會自動下載這個包並放入快取中。
從 npm
6.1 版本後,npm init
和 yarn
都支援使用 CLI 來構建專案,命令的名字形如 create-*
。例如,剛才說的 create-flex-plugin
,我們要做的就是:
# 使用 Node.js:
npm init flex-plugin
# 使用 Yarn:
yarn create flex-plugin
複製程式碼
構建第一個 CLI
如果你更喜歡看視訊學習,點選這裡在 YouTube 觀看教程。
目前我們已經解釋過用 Node.js 建立 CLI 的原因,現在就讓我們開始構建一個 CLI 吧。在本篇教程裡,我們會使用 npm
,但如果你想用 yarn
,絕大多數的命令也都是相同的。確保你的系統中已經安裝了 Node.js 和 npm
。
本篇教程中,我們將會建立一個 CLI,通過執行命令 npm init @your-username/project
,它可以根據你的偏好構建一個新的專案。
通過執行如下程式碼,開始一個新的 Node.js 專案:
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
檔案中新增合適的入口。別忘了也要更新屬性 description
、name
、keyword
和 main
:
{
"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
的目錄,並將目錄 typescript
和 javascript
放在此目錄下。它們的名字都是小寫的版本,我們將會提示使用者從中選擇一個。在本篇文章中,我們就使用這兩個名字,但其實你可以使用你任意喜歡的命名。在這個目錄下,放入檔案 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 install
或npm 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
。
還有很多的功能你可以加入進來。在我的專案中,我還新增了一些依賴,用於為我建立 LICENSE
、CODE_OF_CONDUCT.md
和 .gitignore
。你可以在 GitHub 找到實現這些功能的原始碼,或著檢視上面提到的倉庫來擴充附加功能。如果你發現某個你覺得應該被列出在文章中而我並沒有列出的庫,或者想要給我看你的 CLI,儘管傳送訊息給我!
- Email:dkundel@twilio.com
- Twitter:@dkundel
- GitHub:dkundel
- dkundel.com
使用 JavaScript 還可以構建更多:
- Changelog: Twilio Chat JavaScript SDK
- Sync SDK for JavaScript
- How to Build a Real Time MMS Photostream with Twilio and Socket.IO
- Implementing Chat in JavaScript, Node.js and React Apps
- How to play music over phone calls with Twilio Voice and JavaScript
如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。