Node —— 寫一個實用cli工具

學(xué)習(xí)目標(biāo)
用node寫實用的cli工具,是我們工程化的一個必經(jīng)之路蝗敢,本文也能激起大家學(xué)習(xí)node的興趣捷泞,

本文實現(xiàn)一個vue腳手架,這個腳手架的主要實現(xiàn)的功能就是:

  • 自動克隆github項目
  • 自動安裝依賴
  • 自動npm run serve
  • 自動打開瀏覽器
  • 我們在view文件夾下面添加xxx.vue文件的時候寿谴,router.js自動生成

一锁右、創(chuàng)建工程

創(chuàng)建文件


image.png

安裝依賴

npm i commander download-git-repo ora handlebars figlet clear chalk open -s

編寫kkb.js文件

#!/usr/bin/env node
//指定解釋器類型
console.log ('Hello My-Cli');

編寫package.json文件,新增bin屬性讶泰,kkb就是我們注冊的命令

{
  "name": "vue-auto-router-cli",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "bin": {
    "kkb": "./bin/kkb.js"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "chalk": "^4.1.0",
    "clear": "^0.1.0",
    "commander": "^7.2.0",
    "download-git-repo": "^3.0.2",
    "figlet": "^1.5.0",
    "handlebars": "^4.7.7",
    "open": "^8.0.5",
    "ora": "^5.4.0"
  }
}

將我們編寫的cli工具安裝到全局骡湖,(就跟npm install xxx -g一樣),在項目根目錄下面運行以下命令

npm link

驗證
window+r打開一個新的終端峻厚,執(zhí)行kkb命令响蕴,是否配置成功,成功則輸出Hello My-Cli

image.png

二惠桃、編寫程序

1.使用commander定制命令行
  • command相當(dāng)于注冊了一個init命令name就是后面跟的參數(shù)浦夷,命令具體的操作在action里面寫,commander會將命令后面的參數(shù)傳到這個action接收的這個函數(shù)參數(shù)里面
#!/usr/bin/env node
//指定解釋器類型
const program = require ('commander');
program.version (require ('../package.json').version); //指定版本號
program.command ('init <name>').description ('初始化項目中...').action (payload => {
  console.log (payload);
}); //相當(dāng)于注冊一個命令
program.parse (process.argv); //process描述的是主進程  process.argv是命令后面的參數(shù)辜王,整個program是通過解析后面的參數(shù)來完成的

執(zhí)行命令kkb init project

輸出:project
2.打印一個歡迎界面

文件目錄


image.png

編輯kkb.js

#!/usr/bin/env node
//指定解釋器類型
const program = require ('commander');
program.version (require ('../package.json').version); //指定版本號
program
  .command ('init <name>')
  .description ('初始化項目...')
  .action (require ('../lib/init.js')); //相當(dāng)于注冊一個命令
program.parse (process.argv); //process描述的是主進程  process.argv是命令后面的參數(shù)劈狐,整個program是通過解析后面的參數(shù)來完成的

新建init.js文件

const {promisify} = require ('util'); //promisify 將異步函數(shù)轉(zhuǎn)換為Promise類型的;

const figlet = promisify (require ('figlet')); //藝術(shù)字;
const chalk = require ('chalk'); //粉筆;
const clear = require ('clear'); //清屏;
const log = content => console.log (chalk.red (content)); //封裝一個log方法,用chalk染色;
module.exports = async name => {
  clear ();首先清屏
  const data = await figlet ('Welcome My Cli');
  log (data);
};

運行kkb init name

image.png

3.實現(xiàn)克隆github項目的功能
  • 使用download-git-repo這個包
  • ora:進度條

新建download.js文件

const {promisify} = require ('util');
const ora = require ('ora'); //進度條
const download = promisify (require ('download-git-repo'));
module.exports = async (repo, name) => {
  const process = ora ('下載中...' + name);
  process.start ();
  await download (repo, name);
  process.succeed ();
};

編輯init.js文件

const {promisify} = require ('util'); //promisify 將異步函數(shù)轉(zhuǎn)換為Promise類型的;

const figlet = promisify (require ('figlet')); //藝術(shù)字;
const chalk = require ('chalk'); //粉筆;
const clear = require ('clear'); //清屏;
const log = content => console.log (chalk.red (content)); //封裝一個log方法呐馆,用chalk染色;
const download = require ('./download');
module.exports = async name => {
  clear ();
  const data = await figlet ('Welcome My Cli');
  log (data);
  log ('開始克隆項目');
  await download ('github:su37josephxia/vue-template', name);
};

運行kkb init vue-template命令肥缔,成功克隆項目

image.png

image.png

4.安裝依賴

項目成功克隆之后,接下來常規(guī)操作安裝依賴汹来,運行npm install命令续膳,然后npm run serve啟動,那么在nodejs里面我們?nèi)绾螌懩_本讓他自動執(zhí)行呢收班?

  • 使用Promise封裝spawn方法坟岔,創(chuàng)建一個子進程讓他去執(zhí)行npm install這個命令。
  • 因為子進程執(zhí)行摔桦,我們是看不見的社付,所以通過pipe(管道)對接到主進程,讓他執(zhí)行過程能在我們終端顯示出來邻耕,你也可以把proc.stdout.pipe (process.stdout); proc.stderr.pipe (process.stderr);這倆句注釋掉鸥咖,結(jié)果就是控制臺不會打印任何信息,但項目依然能啟動兄世。如此啼辣,顯而易見。
  • 為什么要用npm.cmd碘饼,可以參考這篇文章熙兔。
  • 關(guān)于child_process這個模塊你可以自己下去仔細學(xué)習(xí)下悲伶,這個包很重要,這篇文章不做贅述住涉。
const {promisify} = require ('util'); //promisify 將異步函數(shù)轉(zhuǎn)換為Promise類型的;

const figlet = promisify (require ('figlet')); //藝術(shù)字;
const chalk = require ('chalk'); //粉筆;
const clear = require ('clear'); //清屏;
const log = content => console.log (chalk.red (content)); //封裝一個log方法麸锉,用chalk染色;
const open = require ('open');
// const download = require ('./download');

// 封裝spawn方法
const spawn = async (...args) => {
  const {spawn} = require ('child_process');
  return new Promise (resolve => {
    const proc = spawn (...args);
    proc.stdout.pipe (process.stdout);
    proc.stderr.pipe (process.stderr);
    proc.on ('close', () => {
      resolve ();
    });
  });
};
module.exports = async name => {
  clear ();
  const data = await figlet ('Welcome My Cli');
  log (data);
  // 克隆項目
  // log ('開始克隆項目');
  // await download ('github:su37josephxia/vue-template', name);//克隆github項目

  // 安裝依賴
  log ('開始安裝依賴');
  await spawn ('npm.cmd', ['install'], {cwd: `./${name}`});
};

image.png

image.png
5.啟動項目并且打開瀏覽器
  • open:使用系統(tǒng)瀏覽器打開一個網(wǎng)址;
const {promisify} = require ('util'); //promisify 將異步函數(shù)轉(zhuǎn)換為Promise類型的;

const figlet = promisify (require ('figlet')); //藝術(shù)字;
const chalk = require ('chalk'); //粉筆;
const clear = require ('clear'); //清屏;
const log = content => console.log (chalk.red (content)); //封裝一個log方法舆声,用chalk染色;
const open = require ('open');
// const download = require ('./download');

// 封裝spawn方法
const spawn = async (...args) => {
  const {spawn} = require ('child_process');
  return new Promise (resolve => {
    const proc = spawn (...args);
    proc.stdout.pipe (process.stdout);
    proc.stderr.pipe (process.stderr);
    proc.on ('close', () => {
      resolve ();
    });
  });
};
module.exports = async name => {
  clear ();
  const data = await figlet ('Welcome My Cli');
  log (data);
  // 克隆項目
  // log ('開始克隆項目');
  // await download ('github:su37josephxia/vue-template', name);//克隆github項目

  // 安裝依賴
  // log ('開始安裝依賴');
  // await spawn ('npm.cmd', ['install'], {cwd: `./${name}`});

  // 打開瀏覽器安裝運行
  open ('http://localhost:8080');
  await spawn ('npm.cmd', ['run', 'serve'], {
    cwd: `./${name}`,
  });
};
image.png
6.自動生成router.jsApp.vue中的router-link

我們?nèi)粘i_發(fā)項目的時候花沉,每次新加一個頁面都要編輯router.js和App.vue里面加一個鏈接,這樣的重復(fù)操作給我們帶來了很大的心智負擔(dān)媳握,所以我們接下來要實現(xiàn)的就是運行命令碱屁,自動生成。

看一下此時的目錄結(jié)構(gòu)


image.png

在我們克隆的項目vue-template中新建一個tempalte文件夾蛾找,以及文件App.vue.hbsrouter.js.hbs娩脾,這倆個文件將來要給handelbars這個包使用。

//App.vue.hbs
<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link> 
      {{#each list}}
      | <router-link to="/{{name}}">{{name}}</router-link>
      {{/each}}
    </div>
    <router-view/>
  </div>
</template>

<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>


//router.js.hbs文件
import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue'

Vue.use(Router)

export default new Router({
  mode: 'history',
  base: process.env.BASE_URL,
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home
    },
    {{#each list}}
    {
      path: '/{{name}}',
      name: '{{name}}',
      component: () => import('./views/{{file}}')
    },
    {{/each}}
  ]
})

lib文件夾下面創(chuàng)建refresh.js

const fs = require ('fs');
const handlebar = require ('handlebars'); //
module.exports = async () => {
  const list = fs.readdirSync ('./vue-template/src/views').map (v => ({
    name: v.replace ('.vue', '').toLowerCase (),
    file: v,
  })); //文件集合
  compile (
    {list},
    './vue-template/src/router.js',
    './vue-template/template/router.js.hbs'
  ); //生成router.js
  compile (
    {list},
    './vue-template/src/App.vue',
    './vue-template/template/App.vue.hbs'
  );//生成App.vue
  function compile (meta, filePath, templatePath) {
    if (fs.existsSync (templatePath)) {
      const content = fs.readFileSync (templatePath).toString ();
      const data = handlebar.compile (content) (meta);
      fs.writeFileSync (filePath, data);
      console.log (`${filePath}創(chuàng)建成功`);
    }
  }
};

編輯kkb.js打毛,新增一個命令kkb refresh

#!/usr/bin/env node
//指定解釋器類型
const program = require ('commander');
program.version (require ('../package.json').version); //指定版本號

// 初始化項目
program
  .command ('init <name>')
  .description ('初始化項目...')
  .action (require ('../lib/init.js')); //相當(dāng)于注冊一個命令

// 刷新路由文件
program
  .command ('refresh')
  .description ('自動生成路由...')
  .action (require ('../lib/refresh'));

program.parse (process.argv); //process描述的是主進程  process.argv是命令后面的參數(shù)柿赊,整個program是通過解析后面的參數(shù)來完成的

views下面新增一個文件,執(zhí)行kkb refresh命令幻枉,我們會看到router.jsApp.vue自動生成

image.png

image.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末碰声,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子熬甫,更是在濱河造成了極大的恐慌胰挑,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,888評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件椿肩,死亡現(xiàn)場離奇詭異瞻颂,居然都是意外死亡,警方通過查閱死者的電腦和手機覆旱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,677評論 3 399
  • 文/潘曉璐 我一進店門蘸朋,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人扣唱,你說我怎么就攤上這事⊥拍希” “怎么了噪沙?”我有些...
    開封第一講書人閱讀 168,386評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長吐根。 經(jīng)常有香客問我正歼,道長,這世上最難降的妖魔是什么拷橘? 我笑而不...
    開封第一講書人閱讀 59,726評論 1 297
  • 正文 為了忘掉前任局义,我火速辦了婚禮喜爷,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘萄唇。我一直安慰自己檩帐,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 68,729評論 6 397
  • 文/花漫 我一把揭開白布另萤。 她就那樣靜靜地躺著湃密,像睡著了一般。 火紅的嫁衣襯著肌膚如雪四敞。 梳的紋絲不亂的頭發(fā)上泛源,一...
    開封第一講書人閱讀 52,337評論 1 310
  • 那天,我揣著相機與錄音忿危,去河邊找鬼达箍。 笑死,一個胖子當(dāng)著我的面吹牛铺厨,可吹牛的內(nèi)容都是我干的幻梯。 我是一名探鬼主播,決...
    沈念sama閱讀 40,902評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼努释,長吁一口氣:“原來是場噩夢啊……” “哼碘梢!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起伐蒂,我...
    開封第一講書人閱讀 39,807評論 0 276
  • 序言:老撾萬榮一對情侶失蹤煞躬,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后逸邦,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體恩沛,經(jīng)...
    沈念sama閱讀 46,349評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,439評論 3 340
  • 正文 我和宋清朗相戀三年缕减,在試婚紗的時候發(fā)現(xiàn)自己被綠了雷客。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,567評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡桥狡,死狀恐怖搅裙,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情裹芝,我是刑警寧澤部逮,帶...
    沈念sama閱讀 36,242評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站嫂易,受9級特大地震影響兄朋,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜怜械,卻給世界環(huán)境...
    茶點故事閱讀 41,933評論 3 334
  • 文/蒙蒙 一颅和、第九天 我趴在偏房一處隱蔽的房頂上張望傅事。 院中可真熱鬧,春花似錦峡扩、人聲如沸蹭越。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,420評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽般又。三九已至,卻和暖如春巍佑,著一層夾襖步出監(jiān)牢的瞬間茴迁,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,531評論 1 272
  • 我被黑心中介騙來泰國打工萤衰, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留堕义,地道東北人。 一個月前我還...
    沈念sama閱讀 48,995評論 3 377
  • 正文 我出身青樓脆栋,卻偏偏與公主長得像倦卖,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子椿争,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,585評論 2 359

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