1. 程式人生 > 實用技巧 >手把手教你搭建腳手架工具 - (commander)

手把手教你搭建腳手架工具 - (commander)

隨著Nodejs的不斷髮展,對於前端來說要做的東西也就更多,vue腳手架react腳手架等等等一系列的東西都脫穎而出,進入到人們的視野當中,對於這些腳手架工具來講也只是停留在應用階段,從來沒有想過腳手架是如何實現的?vueinitwebpack專案名稱是如何通過這樣的命令建立了一個專案,其最重要的模組就是今天要說的Commander。

Commander基本用法

Commander文件寫的很詳細,跟著文章詳細的學習一下,Commander是一個Nodejs模組,需要在Node環境中執行,在使用前確認一下Node環境是否已安裝。

安裝依賴

npminstallcommander--save

Options 解析

在Commander模組下存在option方法用來定義commander的選項options,用來作為選項的文件。

var program = require('commander');

program
  .option('-g, --git [type]', 'Add [marble]', 'Angie')
  .parse(process.argv);

console.log("process.argv",process.argv)
console.log("program.args",program.args)

console.log('you ordered a pizza with:');
if (program.git) console.log('  - git');
console.log('  - %s git', program.git);

上面的示例將解析來自process.argv的args和options,然後將剩下的引數(未定義的引數)賦值給commander物件的args屬性(program.args),program.args是一個數組。

列印輸出一下process.argv和program.args並查看了一下輸出結果如下,使用如下命令執行一下檔案:

nodeindex-gtypeAaron
process.argv ['F:\\node\\installation\\node.exe',
              'C:\\Users\\wo_99\\Desktop\\cli-dome\\index',   
              '-g',
              'type',
              'Aaron' ]
program.args [ 'Aaron' ]  

option方法可以接收三個引數:

  1. 自定義標誌必須:分為長短標識,中間用逗號、豎線或者空格分割;標誌後面可跟必須引數或可選引數,前者用<>包含,後者用[]包含。
  2. 選項描述省略不報錯:在使用 --help 命令時顯示標誌描述
  3. 預設值可省略:當沒有傳入引數時則會使用預設值

若我們執行node index -g得到的結果則是Angie git其第三個引數則作為了預設值填寫在了對應的位置上。除了上面所說還可以使用如下命令:

//  執行 -g 引數 a
//  執行 -b 引數 s
node index -g a -b s
//  執行 -g和-b 傳入a引數給-g
//  -b 引數暫時不知道怎麼傳入
node index -gb a

版本選項

呼叫版本會預設將-V和--version選項新增到命令中。當存在這些選項中的任何一個時,該命令將列印版本號並退出。

var program = require('commander');
 
program
    .version('0.0.1')
    .parse(process.argv);
//  執行命令
//  node index -V

//  輸出結果
//  0.0.1

如果希望程式響應-v選項而不是-V選項,只需使用與option方法相同的語法將自定義標誌傳遞給version方法,版本標誌可以被命名為任何值,但是長選項是必需的。

var program = require('commander');
program
  .version('0.0.1', '-e, --version');

command 新增命令名稱

該方法允許使用命令列去執行一段命令,也就是一段:

var program = require('commander');

program
  .version('0.0.1', '-V, --version')
  .command('rm <dir>')
  .action(function (dir, cmd) {
    console.log('remove ' + dir + (cmd.recursive ? ' recursively' : ''))
  });

program.parse(process.argv);

//  執行命令
//  node index rm /aaa -r

//  輸出結果
//  remove /aaa recursively     即:程式碼中console內容

command函式接收三個引數:

  1. 命令名稱必須:命令後面可跟用<>或[]包含的引數;命令的最後一個引數可以是可變的,像例項中那樣在陣列後面加入...標誌;在命令後面傳入的引數會被傳入到action的回撥函式以及program.args陣列中。
  2. 命令描述可省略:如果存在,且沒有顯示呼叫action(fn),就會啟動子命令程式,否則會報錯
  3. 配置選項可省略:可配置noHelp、isDefault等

使執行命令時,將驗證該命令的options,任何未知的option都將報錯。但是,如果基於action的命令如果沒有定義action,則不驗證options。

var program = require('commander');

program
  .version('0.0.1', '-V, --version')
  .command('rm <dir>')
  .option('-r, --recursive', 'Remove recursively')
  .action(function (dir, cmd) {
    console.log('remove ' + dir + (cmd.recursive ? ' recursively' : ''))
  });
program.parse(process.argv);
console.log(program.args)

//  執行命令
//  node index rm /aaa -r /ddd

//  輸出結果
//  remove /ddd recursively
//  [ '/aaa' ]

helpOption 幫助

提供幫助資訊

var program = require('commander');
program
  .version('0.1.0')
  .helpOption('-h,--HELP')
  .option('-f, --foo', 'enable some foo')
  .option('-b, --bar', 'enable some bar')
  .option('-B, --baz', 'enable some baz');
program.parse(process.argv);

//  執行命令
//  node index -h 或 node index --HELP
/*  輸出結果
 *  Options:
 *    -V, --version  output the version number
 *    -f, --foo      enable some foo
 *    -b, --bar      enable some bar
 *    -B, --baz      enable some baz
 *    -h,--HELP      output usage information 
 */

輸出幫助資訊並立即退出。可選的回撥cb允許在顯示幫助文字之前對其進行後處理。helpOption也提供長名,-h,--HELP前面為短名後面為長名呼叫時使用兩這都是可以的。與version的使用是類似的。

description 命令描述

用來描述命令,也就是命令的說明,上面說過command第二個引數也同樣是命令的描述,當與description同時存在的話,則會優先於第二個引數的描述,description則會作為全域性描述在最頂部顯示。該描述只用在使用-HELP的時候才能看見。

var program = require('commander');

program
  .version('0.0.1', '-V, --version')
  .command('rm <dir>',"arg is description")
  .description("this is description")
  .option('-r, --recursive', 'Remove recursively')
  .action(function (dir, cmd) {
    console.log('remove ' + dir + (cmd.recursive ? ' recursively' : ''))
  });

program.parse(process.argv);

//  執行命令
//  node index -h
//  輸出結果
/*
this is description

Options:
  -V, --version    output the version number
  -r, --recursive  Remove recursively       
  -h, --help       output usage information 

Commands:
  rm <dir>         arg is description       
  help [cmd]       display help for [cmd] 
*/

通過上面的輸出結果可以看的出,rm命令最後的描述是arg is description,若刪除第二個引數則會輸出this is description。

自定義事件偵聽器

用於捕獲option與command,當其被使用賊會被觸發函式。

var program = require('commander');

program
  .version('0.0.1', '-V, --version')
  .command('rm <dir>',"arg is description")
  .option('-r, --recursive', 'Remove recursively')
  .option('-g, --git [type]', 'Add [marble]', 'Angie')
  .option('-a, --am',"ampm")
  .action(() => {
    console.log(123)
  });
program.on('option:am', function () {
  console.log("on:am")
});
program.on('option:recursive', function () {
  console.log("option:recursive")
});
program.on('command:rm', function () {
  console.log("command:rm")
});
program.on('option:git', function () {
  console.log("option:git")
});
program.on('command:*', function () {
  console.log(987)
  console.error('Invalid command: %s\nSee --help for a list of available commands.', program.args.join(' '));
  process.exit(1);
});
program.on('--help', function() {
  console.log('****************');
  console.log('Examples:');
  console.log('****************');
  console.log('  $ deploy exec sequential');
  console.log('  $ deploy exec async');
});
program.parse(process.argv);

分別執行command和option,會依次觸發對應的函式,但是command:*具體是什麼時候觸發的?

  1. command和option已經定義但是沒有進行事件捕獲時會觸發
  2. 規定引數或沒有引數時,傳入了引數也會觸發該函式
  3. 沒有該命令

以上情況就會觸發command:*對應的事件,option:緊緊跟隨的是option的長名。才會捕獲到該事件。

廣州品牌設計公司https://www.houdianzi.com

開發本地模組

建立專案檔案如下:

├─bin
│  └─init-project.js
├─lib
│  └─install.js
└─templates
    └─dome1

建立好專案目錄以後,安裝如下依賴包:

  1. chalk
  2. commander
  3. fs-extra
  4. path
  5. through2
  6. vinyl-fs
  7. which

命令:npminstall --save-dev chalk commander fs-extra through2 vinyl-fs which path

首先在init-project.js中第一行新增#! /usr/bin/env node,這是用來指定指令碼的執行程式,這裡的Node可以用!/usr/bin/node,若使用者將Node安裝在非預設路徑下會找不到Node。So~最好選擇env環境變數查詢Node安裝目錄。

init-project.js

#! /usr/bin/env node

// 引入依賴
var program = require('commander');
var vfs = require('vinyl-fs');
var through = require('through2');
const chalk = require('chalk');
const fs = require('fs-extra');
const path = require('path');

// 定義版本號以及命令選項
program
  .version('1.0.0')
  .option('-i --init [name]', 'init a project', 'myFirstProject')

program.parse(process.argv);

if (program.init) {
  // 獲取將要構建的專案根目錄
  var projectPath = path.resolve(program.init);
  // 獲取將要構建的的專案名稱
  var projectName = path.basename(projectPath);
  console.log(`Start to init a project in ${chalk.green(projectPath)}`);

  // 根據將要構建的專案名稱建立資料夾
  fs.ensureDirSync(projectName);

  // 獲取本地模組下的demo1目錄
  var cwd = path.join(__dirname, '../templates/demo1');

  // 從demo1目錄中讀取除node_modules目錄下的所有檔案並篩選處理
  vfs.src(['**/*', '!node_modules/**/*'], { cwd: cwd, dot: true }).
  pipe(through.obj(function (file, enc, callback) {
    if (!file.stat.isFile()) {
      return callback();
    }

    this.push(file);
    return callback();
  }))
    // 將從demo1目錄下讀取的檔案流寫入到之前建立的資料夾中
    .pipe(vfs.dest(projectPath))
    .on('end', function () {
      console.log('Installing packages...')

      // 將node工作目錄更改成構建的專案根目錄下
      process.chdir(projectPath);

      // 執行安裝命令
      require('../lib/install');
    })
    .resume();
}

install.js

// 引入依賴
var which = require('which');
const chalk = require('chalk');

var childProcess = require('child_process');

// 開啟子程序來執行npm install命令
function runCmd(cmd, args, fn) {
  args = args || [];
  var runner = childProcess.spawn(cmd, args, {
    stdio: 'inherit'
  });

  runner.on('close', function (code) {
    if (fn) {
      fn(code);
    }
  })
}

// 查詢系統中用於安裝依賴包的命令
function findNpm() {
  var npms = ['tnpm', 'cnpm', 'npm'];
  for (var i = 0; i < npms.length; i++) {
    try {
      // 查詢環境變數下指定的可執行檔案的第一個例項
      which.sync(npms[i]);
      console.log('use npm: ' + npms[i]);
      return npms[i]
    } catch (e) {
    }
  }
  throw new Error(chalk.red('please install npm'));
}

var npm = findNpm();
runCmd(which.sync(npm), ['install'], function () {
  console.log(npm + ' install end');
})

完成如上程式碼之後,更改package.json新增屬性如下:

{
  "bin": {
    "q-init": "./bin/init-project.js"
  }
}

注:自定義指令後指定的檔案,一定要新增.js字尾檔名,否則會丟擲錯誤。

接下來剩下的就是測試了,對於測試來說不需要把安裝包推到npm中,npm為了方便,提供了npm link命令,可以實現預釋出。在專案根目錄中使用npm link沒有報錯的話,就說明推送成功了。現在就可以在全域性中使用q-init了。

在全域性中使用initP -h命令,能夠輸出所編譯的help資訊就說明可以初始化專案了。

Usage: init-project [options]

Options:
  -V, --version     output the version number
  -i --init [name]  init a project (default: "myFirstProject")
  -h, --help        output usage information

總結

commander在Vue-cli、creat-app(react)中都起到了很大的作用,這種建立腳手架的方式與vue-cli的方式不同,vue-cli則是使用git遠端拉取專案再完成初始化,這樣一來要比這種更加的方便靈活,每次模板變更不需要再次上傳包,只需要更改git倉庫就好了,方便快捷。