Node.js 開(kāi)發(fā) cli 工具
一荔燎、為什么需要 cli 工具
我們見(jiàn)識(shí)過(guò)了很多的cli工具,例如 vue-cli、create-react-app绍哎、angular/cli等。它讓開(kāi)發(fā)者擺脫了wepack復(fù)雜繁瑣的配置鞋真,快速崇堰、個(gè)性化地搭建項(xiàng)目,從而更好地專注業(yè)務(wù)層面的開(kāi)發(fā)涩咖。
二蘸炸、起步
cli的開(kāi)發(fā)思路是復(fù)制拷貝,其實(shí)與拷貝git倉(cāng)庫(kù)無(wú)異钟些。
創(chuàng)建一個(gè)文件夾勺阐,運(yùn)行
npm init -y
cnpm i arg chalk esm inquirer listr lodash ncp pkg-install --save
上面是我們所需要的依賴,以下是各個(gè)安裝包的說(shuō)明
arg : 解析cli 命令行的參數(shù)
chalk : 讓控制臺(tái)打印顏色
esm : 這個(gè)包可以通過(guò)裝飾 require盾似,從而可以在模塊中使用最新的ECMAScript語(yǔ)法
inquirer : 控制臺(tái)交互式命令集合
lodash : JavaScript 實(shí)用工具庫(kù)
ncp : 拷貝文件使用庫(kù)
pkg-install : 用于在生成的項(xiàng)目中安裝依賴
編輯 package.json
{
"name": "zdxhxh-cli",
"version": "0.0.1",
"description": "cli 工具",
"main": "src/main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"license": "ISC",
"bin": {
"xh-cli": "bin/create-project.js"
},
"author": {
"name": "xh"
},
"keywords": [
"cli",
"create-project"
],
"files": [
"bin/",
"src/",
"templates/"
]
}
說(shuō)明 :
- bin : 用來(lái)指定內(nèi)部命令對(duì)應(yīng)可執(zhí)行文件的位置敬辣,當(dāng)全局安裝時(shí)這個(gè)包時(shí),由于
node_modules/.bin/
目錄會(huì)在運(yùn)行時(shí)零院,加入系統(tǒng)的PATH環(huán)境變量溉跃,所以在命令行行中直接通過(guò)命令來(lái)調(diào)用這些腳本- files : 用來(lái)指定那些目錄會(huì)在發(fā)布時(shí),上傳到NPM服務(wù)器上告抄。這里我們需要
bin撰茎、src、template
這幾個(gè)目錄
按照以下目錄創(chuàng)建文件
├─bin
└─ create-project.js
├─src
| └─cli.js
| └─main.js
│ └─utils
| └─parseArgumentsIntoOptions.js
| └─promptForMissingOptions.js
└─templates
三打洼、編碼
在bin/create-project.js
中 龄糊,當(dāng)我們運(yùn)行 xh-cli
時(shí)逆粹,就會(huì)執(zhí)行該文件
#!/usr/bin/env node
// 這樣包裝后,引入的模塊文件可以使用最新的 ES語(yǔ)法
require = require("esm")(module /*, options*/);
// 屬性會(huì)返回一個(gè)數(shù)組炫惩,其中包含當(dāng) Node.js 進(jìn)程被啟動(dòng)時(shí)傳入的命令行參數(shù)
require("../src/cli").cli(process.argv);
接下來(lái)的目標(biāo)是 : 解析命令行參數(shù)僻弹。
在src/utils/parseArgumentsIntoOptions.js
導(dǎo)出方法,代碼如下
import arg from "arg";
import get from "lodash/get";
// 解析命令行參數(shù) rawArgs即 process.argv
export default function (rawArgs) {
const args = arg(
{
"--git": Boolean,
"--yes": Boolean,
"--install": Boolean,
"--version": Boolean,
"-y": "--yes",
"-i": "--install",
"-v": "--version",
},
{
// process.argv除了前兩個(gè)他嚷,其余的元素才是額外的命令行參數(shù)
argv: rawArgs.slice(2),
}
);
return {
skipPrompts: get(args, "--yes", false), // 快速跳過(guò)配置
version : get(args, "--version", false),
template: args._[0],
runInstall: get(args, "--install", false), // 拷貝后自動(dòng)安裝依賴
};
}
接下來(lái)的目標(biāo)是 : 在沒(méi)有傳入默認(rèn)的語(yǔ)言參數(shù)下蹋绽,讓用戶在命令行中選擇參數(shù).
在src/utils/promptForMissingOptions.js
導(dǎo)出方法,代碼如下
import inquirer from "inquirer";
import get from "lodash/get";
export default async function promptForMissingOptions(options) {
if (options.skipPrompts) {
return {
...options,
template: get(options, "template", "javascript")
};
}
const questions = [];
// 如果沒(méi)有指定
if (!options.template) {
questions.push({
type: "list",
name: "template",
message: "請(qǐng)選擇殼工程語(yǔ)言",
choices: ["javascript", "typescript"],
default: "javascript"
});
}
const answers = await inquirer.prompt(questions);
return {
...options,
template: get(options, "template", answers.template)
};
}
在src/cli.js
中導(dǎo)出 cli 方法
import parseArgumentsIntoOptions from "./utils/parseArgumentsIntoOptions";
import promptForMissingOptions from "./utils/promptForMissingOptions";
import get from "lodash/get";
import pjson from "../package.json";
import chalk from "chalk";
export async function cli(args) {
let options = parseArgumentsIntoOptions(args);
// 打印版本信息
if (get(options, "version")) {
return console.log(chalk.green(`當(dāng)前cli版本為 : ${pjson.version}`));
}
options = await promptForMissingOptions(options);
console.log(options);
}
在命令行執(zhí)行以下命令
npm link
xh-cli
則會(huì)出現(xiàn)命令行交互
接下來(lái)的目標(biāo) : 執(zhí)行上述命令交互后筋蓖,根據(jù)所選項(xiàng)將項(xiàng)目復(fù)制到當(dāng)前執(zhí)行命令的路徑上卸耘。
我們?cè)?code>src/main.js中,增加以下代碼
import chalk from "chalk";
import fs from "fs";
// 用于遞歸復(fù)制文件
import ncp from "ncp";
import path from "path";
import { promisify } from "util";
import Listr from "listr";
import { projectInstall } from "pkg-install";
import get from "lodash/get";
// 遞歸拷貝文件
const copy = promisify(ncp);
async function copyTemplateFiles(options) {
return copy(options.templateDirectory, options.targetDirectory, {
// 是否覆蓋原有內(nèi)容
clobber: false
});
}
export async function createProject(options) {
options = {
...options,
targetDirectory: get(
options,
"targetDirectory",
path.resolve(process.cwd(), get(options, "template"))
)
};
const currentFileUrl = import.meta.url;
const templateDir = path.resolve(
new URL(currentFileUrl).pathname.replace(/^\//, ""),
"../../templates",
options.template.toLowerCase()
);
options.templateDirectory = templateDir;
const tasks = new Listr([
{
title: "拷貝中",
task: () => copyTemplateFiles(options)
},
{
title: "安裝依賴",
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;
}
在 src/cli.js
中增加以下代碼
import { createProject } from "./main";
export async function cli(args) {
// ...
await createProject(options);
}
這樣粘咖,cli工具的雛形已經(jīng)完成了鹊奖。
四、運(yùn)行
在template
創(chuàng)建typescript
涂炎、javascript
兩個(gè)文件夾忠聚,并各自創(chuàng)建README.md
文件
├─javascript
└─typescript
在任意目錄下執(zhí)行
xh-cli
選擇任意一個(gè),你看是不是復(fù)制成功了呢 ?
當(dāng)然唱捣,你通過(guò)以下命令在拷貝項(xiàng)目成功后添加依賴
xh-cli --install
最終两蟀,發(fā)布到npm 中
npm publish