编写一个基于node的CLI

最近因为需要给其他同事提供一些方便开发的工具,就写了一个基于node的CLI,下面给大家分享下怎么自己去动手写一个基于node的CLI。

原理

当前端工程安装到全局目录下之后,可以直接通过别名来执行package.json当中bin下面的js文件,我们就是通过编写这个js文件来实现相应命令。

{
  "name": "testnode-cli",
  "version": "1.0.0",
  "description": "CLI测试",
  "main": "index.js",
  "bin": {
    "testnode": "./bin/index.js"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "git@github.com:yangzq007/testnode-cli.git"
  },
  "author": "yangzq007@126.com",
  "license": "ISC",
  "dependencies": {
    "commander": "^2.15.1"
  }
}

上面所展示的示例当中testnode字段是别名,命令头也是由这里决定的,执行的入口是./bin/下的index.js文件

编写

1.创建一个CLI项目文件夹,并在项目目录当中创建一个如上的package.json文件(可以使用npm init命令来创建)

2.然后在该文件夹目录下运行npm install命令初始化该工程

3.添加package.json文件当中bin字段对应的js文件

4.编写js脚本文件实现命令(文件内容如下所示)

5.使用命令npm install -g将当前项目安装到全局

6.愉快测试和使用自己编写的命令

如下是一个简单的js脚本内容,编写安装完毕后,当你在终端输入testnode时,就会输出对应的log

#!/usr/bin/env node

function show () {
  console.log("testnode-cli is comming!");
}

show();

添加参数

我们虽然可以在show方法中添加我们的处理逻辑,但是我们的脚本只能执行这样一个命令,怎么给它加上参数执行多条命令呢,比如testnode showtestnode deletetestnode add等来执行不同的操作,那我们需要给它添加参数处理的逻辑

如下是一个简单的参数处理的逻辑

#!/usr/bin/env node

var params = {};
process.argv.slice(2).forEach( function (item) {
  switch (item) {
    case "show":
      config.show = true;
      break;
    case "delete":
      config.delete = true;
      break;
    case "add":
      config.add = true;
      break;
    case "-l":
      config.log = true;
      break;
  }
});

function show () {
  console.log("testnode-cli is comming!");
}

function delete () {
    console.log("do some delete");
    config.log && console.log("delete logging");
}

function add () {
    console.log("do some add");
    config.log && console.log("add logging");
}

config.show && show();
config.delete && delete();
config.add && add();

或者我们可以使用commander插件来帮我们处理参数,这样效率会更高,commander在开头的package.json文件当中已经引入,实现大致如下

#!/usr/bin/env node

function show () {
  console.log("testnode-cli is comming!");
}

function delete () {
    console.log("do some delete");
}

function add (name)) {
    console.log(`do some add, name:${name}`);
}

var program = require('commander');

program
  .version(require('../package.json').version);

program
  .command('show')              //命令
  .description('展示相关描述')    //命令描述
  .action(function() {
    show();                     //命令动作
  }).on('--help',function() {
    console.log('展示相关信息');  //选项
  });

program
  .command('delete')
  .description('删除相关文件')
  .action(function() {
    delete();
  });

program
  .command('add [name]')        //带参数的处理
  .description('添加相关文件')
  .action(function(name) {
    add(name);
  });

  program.parse(process.argv);

commander更详细的用法介绍参考https://github.com/tj/commander.js

命令实现

上面的操作已经基本实现了一个CLI的结构,我们剩下要做的就是实现命令的具体内容了,我们可以通过一些现有的工具来方便实现我们的操作。

//我们可以使用execSync来创建一个阻塞node的事件循环的shell客户端来执行我们常用的shell命令
const execSync = require('child_process').execSync;
execSync('ls');

脚手架实现

我们常用的前端脚手架的实现思路一般是在远端创建一个模板工程,在创建我们自己的工程时将远端的模板工程克隆到本地,然后通过CLI实现对模板工程相应关键信息的修改来转换成对应的开发工程。

为了方便实现替换,我们可能需要在模板当中对一些关键信息使用特殊样式的字符替换,例如用&{projectName}&来占位项目名称,这样在替换时就方便检索和替换,避免出现误操作。

相关的文件处理和文字处理可能还需要特殊的工具,这里不再详细描述。

解释器声明

你可能注意到脚本文件的头一行有个声明,这个是表示用node来执行这个文件,如果没有这句声明,就会用默认的文本编辑器打开相应的js脚本文件。

windows

#! node

linux/unix/mac

#!/usr/bin/env node

#!/usr/bin node和#!/usr/bin/env node的区别

这个在unix类的操作系统才有意义。#!/usr/bin node是告诉操作系统执行这个脚本的时候,调用/usr/bin下的node解释器,#!/usr/bin/env node这种用法是为了防止操作系统用户没有将node装在默认的/usr/bin路径里。当系统看到这一行的时候,首先会到env设置里查找node的安装路径,再调用对应路径下的解释器程序完成操作。