乐趣区

搭建脚手架工具-commander

随着 NodeJs 的不断发展,对于前端来说要做的东西也就更多,Vue脚手架 React 脚手架等等等一系列的东西都脱颖而出,进入到人们的视野当中,对于这些脚手架工具来讲也只是停留在应用阶段,从来没有想过脚手架是如何实现的?vue init webpack 项目名称 是如何通过这样的命令创建了一个项目,其最重要的模块就是今天要说的Commander

Commander模块又国外 TJ 大神所编写

项目地址:Commander

Commander 基本用法

Commander文档写的很详细,跟着文章详细的学习一下,Commander是一个 Nodejs 模块,需要在 Node 环境中运行,在使用前确认一下 Node 环境是否已安装。

安装依赖

npm install commander --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.argvargsoptions,然后将剩下的参数(未定义的参数)赋值给commander 对象的 args 属性 (program.args),program.args 是一个数组。

打印输出一下 process.argvprogram.args并查看了一下输出结果如下,使用如下命令运行一下文件:

node index -g type Aaron
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

自定义事件侦听器

用于捕获 optioncommand,当其被使用贼会被触发函数。

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);

分别执行 commandoption,会依次触发对应的函数,但是 command:* 具体是什么时候触发的?

  1. commandoption 已经定义但是没有进行事件捕获时会触发
  2. 规定参数或没有参数时,传入了参数也会触发该函数
  3. 没有该命令

以上情况就会触发 command:* 对应的事件,option:紧紧跟随的是 option 的长名。才会捕获到该事件。

开发本地模块

创建项目文件如下:

├─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

命令:npm install --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

总结

commanderVue-cli、creat-app(react) 中都起到了很大的作用,这种创建脚手架的方式与 vue-cli 的方式不同,vue-cli则是使用 git 远程拉取项目再完成初始化,这样一来要比这种更加的方便灵活,每次模板变更不需要再次上传包,只需要更改 git 仓库就好了,方便快捷。

git 地址:cli-dome

退出移动版