Node交互式命令行工具开发——自动化文档工具

1,778 阅读5分钟

  nodejs开发命令行工具,流程相对简单,但一套完整的命令行程序开发流程下来,还是需要下点功夫,网上资料大多零散,这篇教程意在整合一下完整的开发流程。   npm上命令行开发相关包很多,例如minimistoptimistnoptcommander.jsyargs等等,使用方法和效果类似。其中用得比较多的是TJ大神的commanderyargs,本文以commander为基础讲述,可以参考这篇教程,yargs教程可以参考阮大神的或者这一篇。   另外,一个完整的命令行工具开发,还需要了解processshelljspathlinebyline等模块,这些都是node基础模块或一些简单模块,非常简单,就不多说了,另外如果你不想用回调函数处理异步还需要了解一下PromiseGenerator函数。这是教程:i5ting大神的《深入浅出js(Node.js)异步流程控制》和阮大神的异步编程教程以及promise小人书,另外想尝试ES7 stage3阶段的async/await异步解决方案,可参考这篇教程async/await解决方案需要babel转码,这是教程。本人喜欢async/await(哪个node开发者不喜欢呢?)但不喜欢倒腾,况且async/await本身就是Promise的语法糖,所以没选择使用,据江湖消息,nodejs将在今年晚些时候(10月份?)支持async/await,很是期待。   以下是文章末尾实例用到的一些依赖。

"dependencies": {
    "bluebird": "^3.4.1",
    "co": "^4.6.0",
    "colors": "^1.1.2",
    "commander": "^2.9.0",
    "dox": "^0.9.0",
    "handlebars": "^4.0.5",
    "linebyline": "^1.3.0",
    "mkdirp": "^0.5.1"
  }

  其中bluebird用于Promise化,TJ大神的co用于执行Generator函数,handlebars是一种模板,linebyline用于分行读取文件,colors用于美化输出,mkdirp用于创建目录,另外教程中的示例是一款工具,可以自动化生成数据库和API接口的markdown文档,并通过修改git hooks,使项目的每次commit都会自动更新文档,借助了TJ大神的dox模块。   所有推荐教程/教材,仅供参考,自行甄选阅读。

安装Node

  各操作系统下安装见Nodejs官网,安装完成之后用node -v或者which node等命令测试安装是否成功。which在命令行开发中是一个非常有用的命令,使用which命令确保你的系统中不存在名字相同的命令行工具,例如which commandName,例如which testdev命令返回空白那么说明testdev命令名称还没有被使用。

初始化

  1. 新建一个.js文件,即是你的命令要执行的主程序入口文件,例如testdev.js。在文件第一行加入#!/usr/bin/env node指明系统在运行这个文件的时候使用node作为解释器,等价于node testdev.js命令。
  2. 初始化package.json文件,使用npm init命令根据提示信息创建,也可以是使用npm init -y使用默认设置创建。创建完成之后需要修改package.json文件内容加入"bin": {"testdev": "./testdev.js"}这条信息用于告诉npm你的命令(testdev)要执行的脚本文件的路径和名字,这里我们指定testdev命令的执行文件为当前目录下的testdev.js文件。
  3. 为了方便测试在testdev.js文件中加入代码console.log('hello world');,这里只是用于测试环境是否搭建成功,更加复杂的程序逻辑和过程需要按照实际情况进行编写

测试

  使用npm link命令,可以在本地安装刚刚创建的包,然后就可以用testdev来运行命令了,如果正常的话在控制台会打印出hello world

commander

  TJ的commander非常简洁,README.md已经把使用方法写的非常清晰。下面是例子中的代码:

const program = require('commander'),
  co = require('co');

const appInfo = require('./../package.json'),
  asyncFunc = require('./../common/asyncfunc.js');

program.allowUnknownOption();
program.version(appInfo.version);

program
  .command('init')
  .description('初始化当前目录doc.json文件')
  .action(() => co(asyncFunc.initAction));

program
  .command('show')
  .description('显示配置文件状态')
  .action(() => co(asyncFunc.showAction));

program
  .command('run')
  .description('启动程序')
  .action(() => co(asyncFunc.runAction));

program
  .command('modifyhook')
  .description('修改项目下的hook文件')
  .action(() => co(asyncFunc.modifyhookAction));

program
  .command('*')
  .action((env) => {
    console.error('不存在命令 "%s"', env);
  });

program.on('--help', () => {
  console.log('  Examples:');
  console.log('');
  console.log('    $ createDOC --help');
  console.log('    $ createDOC -h');
  console.log('    $ createDOC show');
  console.log('');
});

program.parse(process.argv);

  定义了四个命令和个性化帮助说明。

交互式命令行process

  commander只是实现了命令行参数与回复一对一的固定功能,也就是一个命令必然对应一个回复,那如何实现人机交互式的命令行呢,类似npm init或者eslint --init这样的与用户交互,交互之后根据用户的不同需求反馈不同的结果呢。这里就需要node内置的process模块。   这是我实现的一个init命令功能代码:

exports.initAction = function* () {
  try {
    var docPath = yield exists(process.cwd() + '/doc.json');
    if (docPath) {
      func.initRepl(config.coverInit, arr => {
        co(newDoc(arr));
      })
    } else {
      func.initRepl(config.newInit, arr => {
        co(newDoc(arr));
      })
    }
  } catch (err) {
    console.warn(err);
  }

  首先检查doc.json文件是否存在,如果存在执行覆盖交互,如果不存在执行生成交互,try...catch捕获错误。   交互内容配置如下:

    newInit:
    [
        {
            title:'initConfirm',
            description:'初始化createDOC,生成doc.json.确认?(y/n)  ',
            defaults: 'y'
        },
        {
            title:'defaultConfirm',
            description:'是否使用默认配置.(y/n)  ',
            defaults: 'y'
        },
        {
            title:'showConfig',
            description:'是否显示doc.json当前配置?(y/n)  ',
            defaults: 'y'
        }
    ],
    coverInit:[
        {
            title:'modifyConfirm',
            description:'doc.json已存在,初始化将覆盖文件.确认?(y/n)  ',
            defaults: 'y'
        },
        {
            title:'defaultConfirm',
            description:'是否使用默认配置.(y/n)  ',
            defaults: 'y'
        },
        {
            title:'showConfig',
            description:'是否显示doc.json当前配置?(y/n)  ',
            defaults: 'y'
        }
    ],

  人机交互部分代码也就是initRepl函数内容如下:

//初始化命令,人机交互控制
exports.initRepl = function (init, func) {
  var i = 1;
  var inputArr = [];
  var len = init.length;
  process.stdout.write(init[0].description);
  process.stdin.resume();
  process.stdin.setEncoding('utf-8');
  process.stdin.on('data', (chunk) => {
    chunk = chunk.replace(/[\s\n]/, '');
    if (chunk !== 'y' && chunk !== 'Y' && chunk !== 'n' && chunk !== 'N') {
      console.log(config.colors.red('您输入的命令是: ' + chunk));
      console.warn(config.colors.red('请输入正确指令:y/n'));
      process.exit();
    }
    if (
      (init[i - 1].title === 'modifyConfirm' || init[i - 1].title === 'initConfirm') &&
      (chunk === 'n' || chunk === 'N')
    ) {
      process.exit();
    }
    var inputJson = {
      title: init[i - 1].title,
      value: chunk,
    };
    inputArr.push(inputJson);
    if ((len--) > 1) {
      process.stdout.write(init[i++].description)
    } else {
      process.stdin.pause();
      func(inputArr);
    }
  });
}

  人机交互才用向用户提问根据用户不同输入产生不同结果的形式进行,顺序读取提问列表并记录用户输入结果,如果用户输入n/N则终止交互,用户输入非法字符(除y/Y/n/N以外)提示输入命令错误。

文档自动化

  文档自动化,其中数据库文档自动化,才用依赖sequelize的方法手写(根据需求不同自行编写逻辑),API文档才用TJ的dox也很简单。由于此处代码与命令行功能相关度不大,请读者自行去示例地址查看代码。

示例地址

github地址 npm地址