Node命令行工具

Node 给前端开发带来了很大的改变,促进了前端开发的自动化,我们可以简化开发工作,然后利用各种工具包生成生产环境。如运行sass src/sass/main.scss dist/css/main.css即可编译 Sass 文件。在实际的开发过程中,我们可能会有自己的特定需求,那么我们得学会如何创建一个Node命令行工具。

命令行接口:Cmmand Line Interface,简称 CLI,是 Node 提供的一个用于命令行交互的工具,本质是基于 Node 引擎运行的。

我们的初步设想是,在指定目录下执行一个命令(假设为autogo)

autogo demo

就会生成一个目录名为demo的项目,里面包含有我们所需的基础文件结构。

开始

1.首先咱们创建一个程序包清单(package.json文件)包含了该命令包的相关信息:

npm init

2.创建一个用于运行命令的脚本bin/autogo.js:

#! /usr/bin/env node
console.log("hello")

然后我们执行

node bin/autogo.js

能够看到输出了hello,当然这不是我们想要的结果,我们是要直接运行autogo命令。

3.告诉 npm 你的命令脚本文件是哪一个,这里我们需要给package.json添加一个bin字段:

{
  ...
  "bin": {
    "autogo": "./bin/autogo.js"
  }
  ...
}

这里我们指定autogo命令的执行文件为./bin/autogo.js。

4.启用命令行:

npm link

这里我们通过npm link在本地安装了这个包用于测试,然后就可以通过:

autogo

来运行命令了。

创建项目结构

根据需要的项目结构创建所需的文件夹和文件:

实际上我们可以先创建一个完整的结构,然后再在执行命令时,通过程序把这些文件和文件夹整个复制到目标项目文件夹中去,最后再对某些文件做一些修改即可。

按照这个思路,我们根据上面的结构,将这些文件和文件夹创建到structure下,然后创建一个生成结构的方法lib/generate.js(这里将功能模块放在了lib/目录下)

var Promise = require("bluebird"),
    fs = Promise.promisifyAll(require('fs-extra'));

function generate(project){
  return fs.copyAsync('structure', project,{clobber: true})
    .then(function(err){
      if (err) return console.error(err)
    })
}

module.exports = generate;

上面的代码就是通过fs-extra这个包(查看文档) 将structure目录下的内容复制到了project参数的目标文件夹中。fs-extra是对fs包的一个扩展,方便我们对文件的操作。

这里用到了bluebird(查看文档),这是一个实现 Promise 的库,因为这里牵涉到了对文件的操作,所以会有异步方法,而 Promise 就是专门解决这些异步操作嵌套回调的,能将其扁平化。

自然,我们应该安装这两个包:

npm install bluebird --save
npm install fs-extra --save

这里加上--save参数是为了在安装后就自动将该依赖加入到package.json中。然后咱们改造一下bin/autogo.js:

#!/usr/bin/env node
var gs = require('../lib/generate');

gs("demo");

然后执行:

autogo

可以看到当前目录下生成了一个demo文件夹,里面包含了和structure相同的文件结构。

我们的目标已经初步达成了,接下来我们就来细化该命令。

命名参数

上面的命令中,我们执行autogo时,是生成了一个固定的demo项目,实际上这个名字是不能写死的,而是应该通过命令中的参数传进去。像下面这样:

autogo demo

因此,我们得在bin/autogo.js中去接收参数了。为了方便起见,我们这里直接使用一个专门用于处理命令行工具的包commander文档)。

同样,首先安装:

npm install commander --save

然后改造bin/autogo.js为:

#!/usr/bin/env node

var program = require('commander'),
    gs = require('../lib/generate');

program
  .version(require('../package.json').version)
  .usage('[options] [project name]')
  .parse(process.argv);

var pname = program.args[0]

gs(pname);

这里的

.version()意思是返回该命令包的版本号,即运行:

autogo --version //- 返回1.0.0

会返回package.json中定义的版本号。

.usage()显示基本使用方法执行:

autogo --help

会输出:

Usage: autogo [options] [project name]

  Options:

    -h, --help     output usage information
    -V, --version  output the version number

可以看到 Commander 帮我们做好了用法(Usage) 信息,以及两个参数(Options)-h, --help和-V, --version。

.parse(process.argv); 是将接收到的参数加入 Commander 的处理管道。

program.args是获取到命令后的参数,注意这里是一个数组

autogo                 //- 返回  []
autogo demo            //-返回 ['demo']
autogo demo hello      //-返回 ['demo','hello']

这里咱们取第一个参数作为项目名,然后调用:

var pname = program.args[0]
gs(pname);

现在我们执行:

autogo demo2

就可以看到新的项目demo2生成了,看上去我们已经完成工作了,只要运行autogo <项目名>就可以生成一个新的项目结构,里面包含了处理 Sass、coffee、jade 的 gulp 构建工具。

如果我们直接运行autogo是会报错的,因为没有传入项目名,实际上我们在运行一个命令而不传入任何参数时,可以直接返回帮助信息:

...
var pname = program.args[0]
if (!pname) program.help();
...

上面我们判断是否存在参数,如果不存在就调用program.help()方法,这是 commander 为我们提供的显示帮助信息的方法,可以直接调用。

如果我不想用jade,就喜欢写原生的 HTML,很明显我们做了多余的事,而且整个结构就不那么合理了,我们需要的是一个干净的项目结构。这个时候我们就需要把与jade相关的文件都删掉(这里不是删structure目录下的文件,而是新项目下的指定文件)。与jade有关的文件有:

  • /structure/views/下的index.jade和layouts/layout.jade
  • /structure/gulpfile.js中的templates任务代码

因此,咱们得把上面这些文件和代码干掉。

移除指定模块

首先,咱们创建一个lib/jadeWithout.js用来移除 jade:

var Promise = require("bluebird"),
  fs = Promise.promisifyAll(require('fs-extra')),
  del = require('../lib/delfile');

var files =  ['/views/layouts/layout.jade','/views/index.jade'];
function jadeWithout(project){
  return Promise.all([del(project,files)])
    .then(function(){      
      return  console.log('remove jade success');
    })
}
module.exports = jadeWithout;

这里咱们将指定的files数组中的文件都删除了,这里我用了一个公共的删除文件模块/lib/delFile.js:

var Promise = require("bluebird"),
  fs = Promise.promisifyAll(require('fs-extra'));

function del(project,files){
  return files.map(function(item){
    return fs.removeAsync(project + item)
  }) 
} 

function delFile(project,files){
  return Promise.all([del(project,files)])  
}

module.exports = delFile;

因为我们这里不光有jade,还有sass和coffee可以被移除,所以我们创建一个公共入口withoutFile.js:

var Promise = require("bluebird");

function deal(project,outs){
  return outs.map(function(item){
    var action = require('../lib/'+item+'Without');
    return action(project)
  }) 
}

function withoutFile(project,outs){
  return Promise.all([deal(project,outs)])
}

module.exports = withoutFile;

这里我们需要传入一个要移除的列表(如['sass','jade']),然后对每个模块进行删除。

最后,我们将withoutFile引入到bin/autogo.js中:

var gs = require('../lib/generateStructure');
var wf = require('../lib/withoutFile');

Promise.all([gs(pname)])
  .then(function(){
    return wf(pname,["jade",'sass'])
  })

然后我们再次执行:

autogo demo

可看到控制台依次输出了:

generate project success
remove jade success
remove sass success

而且目标项目中相关文件已经被删除了。

这里咱们是wf(pname,["jade",'sass'])写死了 outs 参数作为测试,实际上是要再传入一个数组,那么这个数组从哪儿来呢?很明显,得从命令行参数中获取。

我们希望的是这样:

autogo --without jade demo

option

commander 为我们提供了一个option管道来配置命令参数,修改bin/autogo.js:

program
  .version(require('../package.json').version)
  .usage('[options] [project name]')
  .option('-W, --without <str | array>', 
    'generate project without some models(
    value can be `sass`、`coffee`、`jade`)')
  .parse(process.argv);

这里咱们添加了option,其格式为.option('-<大写标识>, --<小写全称> <可取参数类型>', '参数功能描述')。接着处理without参数:

var outs = program.without ? [program.without] : []

Promise.all([gs(pname)])
  .then(function(){
    return wf(pname,outs)
  })

然后咱们再运行:

autogo --without jade demo

可以看到这里只移除了 jade 模块,那如果我想移除多个呢?是不是可以这样:

autogo --without [jade,sass] demo

注意,这样是会报错的,因为获取到的program.without是一个字符串'[jade,sass]'而不是数组,所以咱们可以这样:

autogo --without jade,sass demo

program.without则为'jade,sass'然后再:

program.without.split(',')

既可以获取到一个数组了,因此咱们的代码就变成了:

var outs = program.without ? program.without.split(',') : []

Promise.all([gs(pname)])
  .then(function(){
    return wf(pname,outs)
  })

这下我们就可以这样来运行了:

autogo demo --without sass,jade

发布

到目前为止,我们开发的 autogo 还是在本地的,现在就该将其发布到 npm 上了。

  1. 首先咱们得 注册一个账号 ;
  2. 回到项目中,执行:

    npm login
    

    输入用户名、密码和邮箱便可将本地机器与 npm 连接起来了。

  3. 执行:

    npm publish
    

然后回到你的 npm 个人主页,就可以看到我们发布成功了。

从包的路径规则来看,是没有包含用户名的,由此可知,同名的包是不会被允许的,所以大家在跟着做的时候要给项目取一个不同的名字。

然后咱们来测试一下刚刚发布的包:

首先删除本地开发做的 autogo 链接:

npm unlink

然后

npm install autogo -g

注意这里需要带上-g参数,因为命令行是应该安装在全局环境中。安装成功后,我们切换到另外一个目录下,执行:

autogo demo

然而结果并非我们想象的那样:

Unhandled rejection Error: ENOENT, lstat 'structure'
    at Error (native)

意思是找不到structure,这是怎么回事呢?

实际上当我们执行npm install autogo -g的时候,实际上是将命令包安装在了/usr/local/lib/node_modules/autogo下面,所以在执行命令的目录下是找不到structure文件夹的。

那该怎么办呢?我们能想到的就是,得在程序中去获取这个包安装的实际路径。幸运的是 Node 给我们提供了 __dirname 这个变量用于获取当前执行文件的路径。 我们在 lib/generate.js下console.log(__dirname)会输出/usr/local/lib/node_modules/autogo/lib,然后我们把后面的lib去掉就是根目录了:

var root = __dirname.replace(/autogo\/lib/,'autogo/')

function generate(project,outs){
  return fs.copyAsync(root + 'structure', project)
    .then(function(err){
      return err ?  console.error(err) : console.log('generate project success');
    })
}

修改后,我们按照下面的方式更新,重新安装,然后:

autogo demo
cd demo
npm install
gulp watch

OK 一个新的项目诞生了,准备开发吧...

更新

首先修改package.json配置文件中的version字段,比如这里我从0.1.0改成0.1.1(只能大于当前版本),然后再次:

npm publish

即可成功发布新版本。想将该项目从 npm 中移除吗?执行:

npm unpublish autogo --force

results matching ""

    No results matching ""