- 原文地址: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。javascript
Node.js 可以如此流行的緣由之一就是它有豐富的包生態系統,現在在 npm
註冊處 已經有超過 900000 個包。經過在 Node.js 中寫你本身的 CLI,你就能夠進入這個生態系統,而其中也包含了鉅額數目的針對 CLI 的包。包括:html
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
複製代碼
或者它也能夠做爲項目依賴: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 init
和 yarn
都支持使用 CLI 來構建項目,命令的名字形如 create-*
。例如,剛纔說的 create-flex-plugin
,咱們要作的就是:react
# 使用 Node.js:
npm init flex-plugin
# 使用 Yarn:
yarn create flex-plugin
複製代碼
若是你更喜歡看視頻學習,點擊這裏在 YouTube 觀看教程。android
目前咱們已經解釋過用 Node.js 建立 CLI 的緣由,如今就讓咱們開始構建一個 CLI 吧。在本篇教程裏,咱們會使用 npm
,但若是你想用 yarn
,絕大多數的命令也都是相同的。確保你的系統中已經安裝了 Node.js 和 npm
。ios
本篇教程中,咱們將會建立一個 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
文件中添加合適的入口。別忘了也要更新屬性 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,儘管發送消息給我!
使用 JavaScript 還能夠構建更多:
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。