Node.js 開(kāi)發(fā) cli 工具

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)命令行交互

image-20201225105156614.png

接下來(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ù)制成功了呢 ?

image-20201225110252405.png

當(dāng)然唱捣,你通過(guò)以下命令在拷貝項(xiàng)目成功后添加依賴

xh-cli --install

最終两蟀,發(fā)布到npm 中

npm publish 
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市震缭,隨后出現(xiàn)的幾起案子赂毯,更是在濱河造成了極大的恐慌,老刑警劉巖拣宰,帶你破解...
    沈念sama閱讀 211,194評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件党涕,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡巡社,警方通過(guò)查閱死者的電腦和手機(jī)膛堤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)晌该,“玉大人肥荔,你說(shuō)我怎么就攤上這事〕海” “怎么了燕耿?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,780評(píng)論 0 346
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)姜胖。 經(jīng)常有香客問(wèn)我誉帅,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,388評(píng)論 1 283
  • 正文 為了忘掉前任蚜锨,我火速辦了婚禮档插,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘踏志。我一直安慰自己阀捅,他們只是感情好胀瞪,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,430評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布针余。 她就那樣靜靜地躺著,像睡著了一般凄诞。 火紅的嫁衣襯著肌膚如雪圆雁。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,764評(píng)論 1 290
  • 那天帆谍,我揣著相機(jī)與錄音伪朽,去河邊找鬼。 笑死汛蝙,一個(gè)胖子當(dāng)著我的面吹牛烈涮,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播窖剑,決...
    沈念sama閱讀 38,907評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼坚洽,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了西土?” 一聲冷哼從身側(cè)響起讶舰,我...
    開(kāi)封第一講書(shū)人閱讀 37,679評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎需了,沒(méi)想到半個(gè)月后跳昼,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,122評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡肋乍,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,459評(píng)論 2 325
  • 正文 我和宋清朗相戀三年鹅颊,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片墓造。...
    茶點(diǎn)故事閱讀 38,605評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡挪略,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出滔岳,到底是詐尸還是另有隱情杠娱,我是刑警寧澤,帶...
    沈念sama閱讀 34,270評(píng)論 4 329
  • 正文 年R本政府宣布谱煤,位于F島的核電站摊求,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏刘离。R本人自食惡果不足惜室叉,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,867評(píng)論 3 312
  • 文/蒙蒙 一睹栖、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧茧痕,春花似錦野来、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,734評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至令野,卻和暖如春舀患,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背气破。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,961評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工聊浅, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人现使。 一個(gè)月前我還...
    沈念sama閱讀 46,297評(píng)論 2 360
  • 正文 我出身青樓低匙,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親碳锈。 傳聞我的和親對(duì)象是個(gè)殘疾皇子顽冶,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,472評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容