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

日期:2020-05-07编辑作者:Web前端

时间: 2019-09-08阅读: 245标签: 脚手架


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

node-cli 即用nodejs与shell交互,完成指定工作的工具。他们通常是长这样的:

Commander模块又国外TJ大神所编写,项目地址:Commander

sass xx.scss:xx.css
webpack ....

Commander基本用法

等等,我们实现的这个工具是为了拉取CavinHuang/webpack-multi-skeleton webpack 多页面骨架用于本地快速构建项目的脚手架工具,设想通过以下命令来实现:

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

webpack-template i  # install git端所有的模板列表供选择,选择其中之一后进行本地缓存
webpack-template init # 通过一些选项,初始化整个项目

安装依赖

整个设想大概就是这些,下面就从最简单的开始,来一步一步实现。

npminstallcommander--save

实现第一个自己的node命令

我们直接用npm init初始化一个项目出来

npm init node-cli-demo

一路yes即可,进入项目,在package.json中添加如下代码

"bin": {
  "hi": "./bin/hi.js"
},

创建bin目录和hi.js,在hi.js中写下如下代码

#!/usr/bin/env node
console.log('Hi Welcome node cli')

用命令行进入当前项目目录,输入

hi

如果提示没有这个命令,输入

npm link

刷新命令即可。

一个最简单的node-cli就完成了。我们来解释下:

Options 解析

- !/usr/bin/env node在这里有什么作用?

首先我们都知道操作系统中都会有一个 PATH 环境变量,当系统调用一个命令的时候,就会在PATH变量中注册的路径中寻找,如果注册的路径中有就调用,否则就提示命令没找到。我们可以通过process.env获取本机系统中所有的环境变量,所以这句话主要是帮助脚本找到node的脚本解释器,可以理解为调用系统中的node来解析我们的脚本。

在Commander模块下存在option方法用来定义commander的选项options,用来作为选项的文档。

处理命令行参数

node process对象一个提供有关当前Node.js进程的信息和控制的全局对象,在node环境下无需通过require()即可调用。

process.argv属性返回一个数组,其中包含启动Node.js进程时传递的命令行参数。第一个元素是process.execPath, 如果需要访问argv[0]的原始值,可以使用process.argv0,第二个元素将是要执行的JavaScript文件的路径, 其余元素将是任何其他命令行参数。

#!/usr/bin/env node
console.log('call %s', process.argv[2]);

然后输入test hello,打印出call hello。

对于命令行参数处理,我们用现成的模块commander来处理,commander提供了用户命令行输入和参数解析强大功能。这里我们就使用轻量级,表达力强大的commander进行处理。

官网:commander 官方网站

看一个官网的例子

#!/usr/bin/env node
var program = require('commander');

program
  .version('0.1.0')
  .option('-C, --chdir <path>', 'change the working directory')
  .option('-c, --config <path>', 'set config path. defaults to ./deploy.conf')
  .option('-T, --no-tests', 'ignore test hook');

program
  .command('setup [env]')
  .description('run setup commands for all envs')
  .option("-s, --setup_mode [mode]", "Which setup mode to use")
  .action(function(env, options){
    var mode = options.setup_mode || "normal";
    env = env || 'all';
    console.log('setup for %s env(s) with %s mode', env, mode);
  });

program
  .command('exec <cmd>')
  .alias('ex')
  .description('execute the given remote cmd')
  .option("-e, --exec_mode <mode>", "Which exec mode to use")
  .action(function(cmd, options){
    console.log('exec "%s" using %s mode', cmd, options.exec_mode);
  }).on('--help', function() {
    console.log('  Examples:');
    console.log();
    console.log('    $ deploy exec sequential');
    console.log('    $ deploy exec async');
    console.log();
  });

program
  .command('*')
  .action(function(env){
    console.log('deploying "%s"', env);
  });

program.parse(process.argv);

首先安装commander

yarn add commander # OR npm install commander

看下效果:

hi -V

hi setup

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

commander.js API

  • Option() ——> 初始化自定义参数对象,设置“关键字”和“描述”
  • Command() ——> 初始化命令行参数对象,直接获得命令行输入,返回一个数组或者string
  • Command#command() ——> 定义命令名称
  • Command#arguments() ——> 定义初始命令的参数
  • Command#parseExpectedArgs() ——> 解析预期参数
  • Command#action() ——> 注册命令的回调函数
  • Command#option() ——> 定义参数,需要设置“关键字”和“描述”,关键字包括“简写”和“全写”两部分,以”,”,”|”,”空格”做分隔
  • Command#allowUnknownOption() ——> 允许命令行未知参数
  • Command#parse() ——> 解析process.argv,设置选项和定义时调用命令
  • Command#parseOptions() ——> 解析参数
  • Command#opts() ——>设置参数
  • Command#description() ——> 添加命令描述
  • Command#alias() ——> 设置命令别名
  • Command#usage() ——> 设置/获取用法
  • Command#name()
  • Command#outputHelp() ——> 设置展示的help信息
  • Command#help()

有了上面的API我们来实现一个用于罗列出当前文件夹下所有文件和文件夹的命令list:

program
    .command( 'list' ) //声明hi下有一个命令叫list
    .description( 'list files in current working directory' ) //给出list这个命令的描述
    .option( '-a, --all', 'Whether to display hidden files' ) //设置list这个命令的参数
    .action( function ( options ) { //list命令的实现体
        var fs = require( 'fs' );
        //获取当前运行目录下的文件信息
        fs.readdir( process.cwd(), function ( err, files ) {
            var list = files;
            if ( !options.all ) { //检查用户是否给了--all或者-a的参数,如果没有,则过滤掉那些以.开头的文件
                list = files.filter( function ( file ) {
                    return file.indexOf( '.' ) !== 0;
                } );
            }
            console.log( list.join( 'nr' ) ); //控制台将所有文件名打印出来
        } );
    } );

运行

hi list # hi list -a 或者 --all来查看效果

第一阶段的代码github地址:github传送门,0.0.1分支为第一版本的代码

上面的示例将解析来自process.argv的args和options,然后将剩下的参数(未定义的参数)赋值给commander对象的args属性(program.args),program.args是一个数组。

搭建正式版本的开发环境,使它支持es6语法,支持eslint

yarn add -D babel-cli babel-eslint babel-plugin-transform-es2015-modules-commonjs babel-preset-latest-node

在项目根目录新建.babelrc,内容为:

{
  "presets": [
    ["env", {
      "targets": {
        "node": "current"
      }
    }]
  ],
  "plugins": [
    "transform-es2015-modules-commonjs"
  ]
}

新建src目录,用于开发,新建src/command目录和src/utils目录,用于开发使用。
建好后目录结构如下:

├─bin             # 脚本启动文件所在目录
├─node_modules    # libraray 目录
│  └─commander    
│      └─typings  
└─src             # 开发目录
    ├─command     # 命令实现目录,一个命令对应一个文件
    └─utils       # 工具目录

接下来我们实现一个入口,把功能转到对应的命令实现文件,来具体实现。新建index.js用于处理入口,再建立src/index.js用于实际的功能转发
index.js 内容如下

// babel解析
require( "babel-register" )
require( "babel-core" )
    .transform( "code", {
        presets: [ [ require( 'babel-preset-latest-node' ), {
            target: 'current'
        } ] ]
    } );
require( 'babel-polyfill' )

require('./src')

src/index.js 内容如下:

var program = require( 'commander' );
program.parse( process.argv ); //开始解析用户输入的命令
require( './command/' + program.args + '.js' ) // 根据不同的命令转到不同的命令处理文件

解释一下,为什么我想这样做:

  • 为了保证文件单一职责,方便维护;
  • 方便dev和product加载。
    接下来我们建立相应的问价即可,src/command/init.js src/command/install.js 两个命令处理文件,内容如下:

src/command/list.js:

var program = require( 'commander' );
program
    .command( 'init' )
    .description( 'init project for local' )
    .action( function ( options ) { //list命令的实现体
        // to do
        console.log( 'init command' );
    } );
program.parse( process.argv ); //开始解析用户输入的命令

src/command/install.js:

var program = require( 'commander' );
program
    .command( 'install' )
    .description( 'install github project to local' )
    .action( function ( options ) { //list命令的实现体
        // to do
        console.log( 'install command' );
    } );
program.parse( process.argv ); //开始解析用户输入的命令

在命令行输入以下命令来测试:

webpack-template install

webpack-template init

第二版完成代码地址:【第二版github地址,可以clone下来试试】

接下来我们分别实现install功能和init功能。
首先,install步骤设想如下:

  • 通过github api拉取仓库里的模板项目
  • 通过选择模板进行下载
  • 缓存至本地临时目录,供下次直接使用

首先,去github api v3找到所需的api接口,
为了方便单独管理模板项目,我新建了一个organization来管理。所以,我主要是通过

/orgs/:org/repos #获取项目
和
/repos/:owner/:repo #获取版本

项目已经建好,可以通过以下api来查看仓库详情
1、项目列表

url -i https://api.github.com/orgs/cavinHuangORG/repos

2、项目版本

curl -i https://api.github.com/repos/cavinHuangORG/webpack-multipage-template/tags

通过命令行选择选项,效果如下:

图片 1

inquirer.gif

这里我们用到另外一个命令行交互的库,inquirer.js,主要用来命令行选择和输入;
我们先实现一个简单的在insatll.js完成如下代码:

var inquirer = require( 'inquirer' );
program
    .command( 'install' )
    .description( 'install github project to local' )
    .action( function ( options ) { //list命令的实现体
        // to do
        console.log( 'install command' );
        let choices = [ 'aaa', 'bbb', 'ccc', 'dddd' ];
        let questions = [ {
            type: 'list',
            name: 'repo',
            message: 'which repo do you want to install?',
            choices
  } ];
        // 调用问题
        inquirer.prompt( questions )
            .then( answers => {
                console.log( answers ); // 输出最终的答案
            } )
    } );
program.parse( process.argv ); //开始解析用户输入的命令

最终结果如下:

图片 2

install-2.gif

到此已经我们要的效果已经差不多完成了。下一步,我希望可以通过用户输入一些特定的参数,来初始化整个项目。

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

download-git-repo

下面我们要用到一个库,来下载github库的代码,download-git-repo,用法如下:

download(repository, destination, options, callback)

Download a git repository to a destination folder with options, and callback.
将Git存储库下载到带有选项的目标文件夹和回调函数

  • repository github库地址

    • GitHub - github:owner/name 或者简写为 owner/name
    • GitLab - gitlab:owner/name
    • Bitbucket - bitbucket:owner/name
  • destination 目标文件夹

  • options 下载时携带的参数

    • clone 默认false
  • callback 完成之后的回调

nodeindex-gtypeAaron

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

download-git-repo 用法实例

const downloadGitRepo = require('download-git repo')
// 把目标项目下载到当前目录下的test下
downloadGitRepo('CavinHuang/node-cli-demo', './test', false, err => {
  console.log(err ? 'SUCCESS' : "FAIL");
} )

option方法可以接收三个参数:

完成git操作类

我们专门分装一个类用来获取git仓库列表、版本信息、下载git代码等操作,主要有以下几个方法,代码就不贴了,代码全在git仓库0.0.3分支

/**
 * 获取git仓库列表
 */
async fetchRepoList() {}

/**
 * 获取仓库所有的版本
 * @param  {[string]} repo [仓库名称]
 * @return {[type]}      [description]
 */
async fetchRepoTagList( repo ) {}

/**
 * 获取仓库详细信息
 * @param  {[string]} repo [仓库名称]
 * @return {[type]}      [description]
 */
async fetchGitInfo( repo ) {}

/**
 * 下载git仓库代码到指定文件夹
 * @param  {[string]} repo [仓库名称]
 * @return {[type]}      [description]
 */
async downloadGitRepo( repo ) {}

在install.js里,首先我们要把仓库里的所有的模板拉出来供选择,只要把choices换成我们通过api获取的git长裤列表即可

import gitCtrl from '../utils/gitCtrl'
import config from '../config'
// 初始化git操作类
let git = new gitCtrl.gitCtrl( config.repoType, config.registry )

action里的改成:

// 获取git仓库列表
let choices = await git.fetchRepoList();

下面是根据用户选择仓库下载代码到本地, 我们新建一个config文件夹用来存放一些配置,定义一些常用的变量,如缓存目录,版本等等,新建constant.js

const os = require( 'os' );
import {
    name,
    version,
    engines
} from '../../package.json';

// 系统user文件夹
const home = process.env[ ( process.platform === 'win32' ) ? 'USERPROFILE' : 'HOME' ];

// user agent
export const ua = `${name}-${version}`;

/**
 * 文件夹定义
 * @type {Object}
 */
export const dirs = {
    home,
    download: `${home}/.webpack-project`,
    rc: `${home}/.webpack-project`,
    tmp: os.tmpdir(),
    metalsmith: 'metalsmith'
};

/**
 * 版本
 * @type {Object}
 */
export const versions = {
    node: process.version.substr( 1 ),
    nodeEngines: engines.node,
  [ name ]: version
};

index.js

/**
 * 配置文件
 */

export default {
    registry: 'cavinHuangORG', // 仓库地址
    repoType: 'org', // ['org', 'user']
    metalsmith: true
}

有了这些,下边我们就下载代码了:

// 下载库
let result = await git.downloadGitRepo( answers.repo )
console.log( result ? 'SUCCESS' : result )

这时我们运行

webpack-template install

结果如下:

图片 3

install-2.gif

下面我们添加版本选择,我们把install.js里的代码,稍微修改下,加上版本选择:

// 取出选择的git仓库
const repo = answers.repo;
// 获取选择仓库所有的版本
const tags = await git.fetchRepoTagList( repo );

if ( tags.length === 0 ) {
  version = '';
} else {
  choices = tags.map( ( {
    name
  } ) => name );

  answers = await inquirer.prompt( [
    {
      type: 'list',
      name: 'version',
      message: 'which version do you want to install?',
      choices
  }
] );
  version = answers.version;
}
console.log( answers ); // 输出最终的答案
let result = await git.downloadGitRepo( [ repo, version ].join( '@' ) );
console.log( result ? 'SUCCESS' : result )

图片 4

install-3.gif

这时我们去看系统的user文件夹下的.webpack-project下,就会找到我们换成的项目了。
到这里,我们install代码已经完成了,github地址

自定义标志必须:分为长短标识,中间用逗号、竖线或者空格分割;标志后面可跟必须参数或可选参数,前者用包含,后者用[]包含。选项描述省略不报错:在使用 --help 命令时显示标志描述默认值可省略:当没有传入参数时则会使用默认值

完成init命令

init命令是通过收录一些用户填写的信息来初始化本地项目,其实原理就是把收录的参数进行替换,把下载到缓存目录的项目copy到当前命令行执行目录。
首先我们还是完成最简单的命令行用户输入信息的收入,此处依然使用inquirer来完成:

// 1、选择哪个模板
// 2、当前项目的名字,也是初始化项目的文件夹名字
let questions = [
  {
    type: 'list',
    name: 'template',
    message: 'which template do you want to init?',
    choices: list
  }, {
    type: 'input',
    name: 'dir',
    message: 'project name',
    async validate( input ) {
      // 下面这行代码用于通知异步任务
      const done = this.async();
      if ( input.length === 0 ) {
        done( 'You must input project name' );
        return;
      }
      const dir = resolve( process.cwd(), input );
      if ( await exists( dir ) ) {
        done( 'The project name is already existed. Please change another name' );
      }
      done( null, true );
    }
  }
];
const answers = await inquirer.prompt( questions )

若我们执行node index -g得到的结果则是Angie git其第三个参数则作为了默认值填写在了对应的位置上。除了上面所说还可以使用如下命令:

ncp使用帮助

下面是准备收集更加详细的信息,并且把下载的文件copy一份到临时目录,用于处理,此处copy文件用的是成熟的ncp库,这是一个与linux cp命令接口一致的库。官方网站,基本调用方式: ncp [source] [dest] [--limit=concurrency limit] [--filter=filter] --stopOnErr
实例代码:

var ncp = require('ncp').ncp;

ncp.limit = 16;

ncp(source, destination, function (err) {
 if (err) {
   return console.error(err);
 }
 console.log('done!');
});
// 执行 -g 参数 a// 执行 -b 参数 snode index -g a -b s// 执行 -g和-b 传入a参数给-g// -b 参数暂时不知道怎么传入node index -gb a

mkdirp使用帮助

主要作用跟linux mkdir -p 是一样的,只是它运行在node里,也就是递归创建目录。
主要用法:

var mkdirp = require('mkdirp');

mkdirp('/tmp/foo/bar/baz', function (err) {
    if (err) console.error(err)
    else console.log('pow!')
});

根据这两个库,我们分装一个专门用来copy我们项目的工具函数

import {
    ncp
} from 'ncp';
import mkdirp from 'mkdirp'
import {
    exists
} from 'mz/fs'
export default function copyFile( src, dest ) {
    return new Promise( async ( resolve, reject ) => {
        if ( !( await exists( src ) ) ) {
            mkdirp.sync( src ); //异步创建
        }
        ncp( src, dest, ( err ) => {
            if ( err ) {
                reject( err );
                return;
            }
            resolve();
        } );
    } );
}

copy到临时文件夹,生成项目是,要经过一个数据填充的过程,这个过程主要用的是一个静态站点生成器(Metalsmith)和swig以及consolidate一个模板引擎合并库

在init.js里添加copy的动作和编译的动作

const answers = await inquirer.prompt( questions )
const metalsmith = config.metalsmith;
if ( metalsmith ) {
  const tmp = `${dirs.tmp}/${answers.template}`;
  // 复制一份到临时目录,在临时目录编译生成
  await copyFile( `${dirs.download}/${answers.template}`, tmp );
  await metalsmithACtion( answers.template ); // 根据参数编译
  await copyFile( `${tmp}/${dirs.metalsmith}`, answers.dir );
  await rmfr( tmp ); // 清除临时文件夹
} else {
  await copyFile( `${dirs.download}/${answers.template}`, answers.dir );
}

最后所有的目录结构如下:

│  .babelrc
│  .gitignore
│  index.js
│  package.json
│  
├─bin
│      hi.js
│                      
└─src
    │  index.js
    │  
    ├─command
    │      init.js
    │      install.js
    │      
    ├─config
    │      constant.js
    │      index.js
    │      
    └─utils
            copyFile.js
            gitCtrl.js
            initProjectQuestion.js    #初始化项目的问题
            metalsmithACtion.js       #临时文件夹编译动作
            render.js                 #编译模板的插件

到此所有的功能就已经实现了,为了让整个命令用起来更加人性化,更加流程,我们引入ora这个库,项目地址:ora,主要效果如下:

在utils里新建OraLoading.js

import ora from 'ora';

export default function OraLoading( action = 'getting', repo = '' ) {
    const l = ora( `${action} ${repo}` );
    return l.start();
}

好了,到了这里所有的东西都已经写完了,下面我们来试试效果:
首先是install

图片 5

install-last.gif

init也是一样的就不演示了

版本选项

npm 发布

  • 到npm.com注册好自己的账户,命令行然后切换到当前目录的文件夹,执行npm login命令,输入自己的账号密码,进行登录即可。

执行

npm publish .

就可以发布自己的npm包了,注意此处一个坑,如果你是用的淘宝源,需要切换回npm源,

npm config set registry http://registry.npmjs.org

否则验证不通过。

最后奉上github完成代码地址: github传送门

请各位老铁不要吝啬自己的start,感谢鼓励!

调用版本会默认将-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函数接收三个参数:

命令名称必须:命令后面可跟用或[]包含的参数;命令的最后一个参数可以是可变的,像实例中那样在数组后面加入...标志;在命令后面传入的参数会被传入到action的回调函数以及program.args数组中。命令描述可省略:如果存在,且没有显示调用action(fn),就会启动子命令程序,否则会报错配置选项可省略:可配置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 */

本文由www.129028.com金沙发布于Web前端,转载请注明出处:手把手教你搭建脚手架工具 - (commander)

关键词:

node中的内置模块fswww.129028.com金沙

获取文件或文件夹的信息异步 fs.stat(路径,(err,data)={})同步 let res =fs.statSync(路径)流式读取1、创建可读流2、创建一个可...

详细>>

Vue中在新窗口打开页面及Vue-router的使用_vue.js_脚本之家

时间: 2019-09-07阅读: 110标签: 跳转 背景 route-link是在html中静态定义的,也可以在代码中动态跳转: 在开发提分加项目...

详细>>

逼真的HTML5 3D水波动画 可多视角浏览

时间: 2019-09-06阅读: 115标签: 3D 本文由 www.129028.com金沙,码农网  –小峰原创,转载请看清文末的转载要求,欢迎参与...

详细>>

nginx负载均衡如何实现www.129028.com金沙?

什么是nginx? Nginx是一个免费的,开源的,高性能的服务器和反向代理服务器软件,同时它也可以为IMAP和POP3服务器代...

详细>>