用node.js开发一个可交互的命令行应用

时间: 2017-12-25阅读: 499标签: node

近几年, Node.js 在软件开发的一致性上助力很大.无论是前端开发,服务端脚本,跨平台桌面/移动端应用或是物联网应用,Node.js 都可以帮你完成.由于 Node.js 的出现,编写命令行工具比之前容易很多,这不是随意说说,而是可交互,真正有价值的并且能减少开发耗时的命令行工具.

译者:Icarus
原文链接: How To Develop An Interactive Command Line Application Using Node.js

如果你是一名前端开发者,那你一定听说过或者使用过诸如 Gulp, Angular CLI, Cordova, Yeoman或其它的命令行工具.举个例子,在使用 Angular CLI 的情况下,通过执行ng new <project-name>这个命令,你会创建一个基于基础配置的 Angular 项目.像 Yeoman 这样的命令行工具会在运行过程中需要你输入一些内容从而帮助你个性化定制项目的配置.Yeoman 中的生成器(generators)会帮助你在生产环境部署项目.这就是我们今天要学习的部分.

拓展阅读

A Detailed Introduction To Webpack
An Introduction To Node.js And MongoDB
Server-Side Rendering With React, Node And Express
Useful Node.js Tools, Tutorials And Resources

在这个教程中,我们会开发一个命令行应用,它可以接收一个 CSV 格式的用户信息文件,通过使用  SendGrid API可以像这些用户发送电子邮件.下面是教程的内容大纲:

  1. “Hello,World”
  2. 处理命令行参数
  3. 运行时的用户输入
  4. 异步网络会话
  5. 美化控制台的输出
  6. 封装成 shell 命令
  7. JavaScript 之外

“Hello,World”

这个教程假设你的系统里已经安装好了 Node.js. 如果你没有,请先安装它.在安装 Node.js的同时会附带一个叫 npm 的包管理器.使用 npm 你可以安装很多开源的包.你可以在 npm 的官网站点上获取全部的包列表.这个项目我们会用到一些开源的模块(之后会更多).现在,让我们用 npm 创建一个 Node.js 项目.

npm init
name: broadcast
version: 0.0.1
description: CLI utility to broadcast emails
entry point: broadcast.js

我创建了一个名为 broadcast 的文件夹,在里面我执行了 npm init 命令.正如你看到的那样,我已经提供了诸如项目名称,描述,版本号和入口文件等项目的基础信息.入口文件是最主要的 JS 文件,在这里脚本开始编译运行.Node.js 默认把 index.js 文件当做入口文件,而在这个例子里我们把入口文件改为 broadcast.js.当你执行 npm init命令的时候,你会得到更多的选项,比如 Git 仓库地址,开源许可证和作者名.你可以填写这些选项或者空着它们.

npm init成功执行之后,你会在文件夹里看到一个 package.json文件已经创建好了.这是我们的配置文件.与此同时,它也保存着我们在创建项目时提供的信息.你可以在 npm 官方文档中浏览更多有关package.json的内容.

既然项目已经创建好了,那就让我们创建一个”Hello world”程序.开始之前,你需要在你的项目中新建一个 broadcast.js文件,这个是之后主要用到的文件,在文件中写入如下代码段:

console.log('hello world');

现在让我们运行一下.

node broadcast
hello world

正如你看到的那样,”hello world”在控制台打印出来了.你可以使用node broadcast.js或者node broadcast来执行脚本. Node.js足以分辨它们的区别.

根据package.json的文档,有一个名为 dependencies 的选项,在这里我们可以填写所有我们计划在项目中使用的第三方模块,同时附上它们的版本号.像之前提到的,我们会使用很多第三方的开源模块去开发这个工具.在我们的项目中,package.json像下面这样:

{
  "name": "broadcast",
  "version": "0.0.1",
  "description": "CLI utility to broadcast emails",
  "main": "broadcast.js",
  "license": "MIT",
  "dependencies": {
    "async": "^2.1.4",
    "chalk": "^1.1.3",
    "commander": "^2.9.0",
    "csv": "^1.1.0",
    "inquirer": "^2.0.0",
    "sendgrid": "^4.7.1"
  }
}


你一定注意到了,我们会用到 Async, Chalk, Commander, CSV, Inquirer.js 和 SendGrid这些模块.随着我们教程的深入,这些模块的具体用法和细节会慢慢解释.

处理命令行参数

读取命令行参数并不是很难.你可以用 process.argv 很简单的去读取它们.但是分析它们的取值和选项是一项很繁琐的工作.为了避免重复造轮子,我们会使用 Commander 模块.Commander 是一个开源的 Node.js模块,它可以帮助你编写交互式的命令行工具.它带来很多解释命令行选项的有趣特性并且拥有类似 Git 的子命令,但我最喜欢的是它可以自动生成帮助命令.你不需要去写额外的代码 - 执行 --help 或者 -h选项就可以了.当你开始定义各种各样的命令行选项时,帮助命令会自动生成,让我们来试一试:

npm install commander --save

这会在你的 Node.js 项目中安装 Commander 模块.在 npm install 命令中加入 --save参数会自动将 Commander 模块添加到 package.json 文件中的 dependencies 参数中.在我们之前填写的 package.json 文件中,我们已经把所有的依赖都写好了,所以我们可以不加 --save 参数.

var program = require('commander');
program
  .version('0.0.1')
  .option('-l, --list [list]', 'list of customers in CSV file')
  .parse(process.argv)
console.log(program.list);

正如你看到的那样,处理命令行的参数就是这么直截了当.我们已经定义了一个 --list 参数.现在,我们在 --list 参数后面提供任何值,这个值都会储存在方括号包裹中的变量里.在这里,就是 list.你可以从 program 这个 Commander 的实例中获取到 list 的值.现在,这个程序只接受一个文件路径作为 --list 参数的取值,然后把它打印在控制台中.

node broadcast --list input/employees.csv
input/employees.csv

你一定注意到了这里我们定义了另一个方法 version.任何时候只要我们带着 --version或者 -V参数执行命令,定义中的值就会传入这个方法并且把它打印在控制台.

node broadcast --version
0.0.1

相似的,当你带着 --help 参数执行命令的时候,控制台会打印出所有你定义的选项和子命令.在这里,看起来是下面这样的:

node broadcast --help
Usage: broadcast [options]
Options:
    -h, --help                 output usage information
    -V, --version              output the version number
    -l, --list <list>          list of customers in CSV file


既然已经可以在命令行参数中接受文件路径,我们就可以开始使用 CSV 模块来读取 CSV 文件了.CSV 模块是处理 CSV 文件的一个解决方案.从创建一个 CSV 文件到解析处理它,这个模块可以解决任何相关的问题.

因为计划使用 sendGrid API 来发送电子邮件,我们可以使用下面的文档作为一个 CSV 文件的示例.使用 CSV 模块,我们会读取其中的数据并且在表格中展示姓名和对应的电子邮件地址.

First name Last name Email
Dwight Schrute dwight.schrute@dundermifflin.com
Jim Halpert jim.halpert@dundermifflin.com
Pam Beesly pam.beesly@dundermifflin.com
Ryan Howard ryan.howard@dundermifflin.com
Stanley Hudson stanley.hudson@dundermifflin.com

现在,让我们写一个程序来读取 CSV 文件并且将其中的数据打印在控制台.

const program = require('commander');
const csv = require('csv');
const fs = require('fs');
program
  .version('0.0.1')
  .option('-l, --list [list]', 'List of customers in CSV')
  .parse(process.argv)
let parse = csv.parse;
let stream = fs.createReadStream(program.list)
    .pipe(parse({ delimiter : ',' }));
stream
  .on('data', function (data) {
    let firstname = data[0];
    let lastname = data[1];
    let email = data[2];
    console.log(firstname, lastname, email);
  });

使用 Node.js原生的文件模块,我们可以通过命令行参数来读取文件.文件模块执行后是我们提前定义的事件 data,它会在数据被读取时被触发.CSV 模块中的 parse 方法会将 CSV 文件分割成独立的行并且触发多次 data 事件.每一个 data 事件传递一个列数据的数组.这些数据就会以下面这种形式被打印出来:

node broadcast --list input/employees.csv
Dwight Schrute dwight.schrute@dundermifflin.com
Jim Halpert jim.halpert@dundermifflin.com
Pam Beesly pam.beesly@dundermifflin.com
Ryan Howard ryan.howard@dundermifflin.com
Stanley Hudson stanley.hudson@dundermifflin.com


运行时的用户输入

现在我们了解了如何接收命令行参数并且去解析它们.但是如果我们希望在运行过程中接受用户的输入呢?一个名为 Inquirer.js 的模块让我们接受许多种输入的方式,从直接输入文本到输入密码甚至到一个多选列表.

在这个样例里,我们会在运行过程的输入中接收发送者的电子邮件地址和姓名.

let questions = [
  {
    type : "input",
    name : "sender.email",
    message : "Sender's email address - "
  },
  {
    type : "input",
    name : "sender.name",
    message : "Sender's name - "
  },
  {
    type : "input",
    name : "subject",
    message : "Subject - "
  }
];
let contactList = [];
let parse = csv.parse;
let stream = fs.createReadStream(program.list)
    .pipe(parse({ delimiter : "," }));
stream
  .on("error", function (err) {
    return console.error(err.message);
  })
  .on("data", function (data) {
    let name = data[0] + " " + data[1];
    let email = data[2];
    contactList.push({ name : name, email : email });
  })
  .on("end", function () {
    inquirer.prompt(questions).then(function (answers) {
      console.log(answers);
    });
  });

首先,你会注意到上面的示例中我们创建了一个名为 contactList 的数组,它是我们用来存储 CSV 文件中的数据的.

Inquirer.js 带来了一个名为 prompt 的方法,这个方法接收一个问题的数组,里面保存着运行期间我们想要问的问题.在这里,我们想要知道发送者的姓名,电子邮件地址和他们邮件的主题.我们已经创建了一个保存了所有问题的 questions 数组.这个数组接受对象作为数组成员,对象中包含 type 属性,可以选择 input,password和 raw list等值.完整的可用值可以在官方文档中找到.在这里,name 定义了保存用户输入的索引(key).prompt 方法返回一个 promise 对象.当用户回答所有的问题之后,这个 promise 对象会触发一系列的成功或失败的回调.answers 作为 then 回调的参数传递,用户的回复可以通过它来获取.下面是执行代码时发生的事情:

node broadcast -l input/employees.csv
? Sender's email address -  michael.scott@dundermifflin.com
? Sender's name -  Micheal Scott
? Subject - Greetings from Dunder Mifflin
{ sender:
   { email: 'michael.scott@dundermifflin.com',
     name: 'Michael Scott' },
  subject: 'Greetings from Dunder Mifflin' }


异步网络会话

既然我们已经可以从 CSV 文件中读取接收者的数据并且接收到发送者通过命令行提示填写的信息,是时候发送电子邮件了.我们会使用 SendGrid API来发送电子邮件.

let __sendEmail = function (to, from, subject, callback) {
  let template = "Wishing you a Merry Christmas and a " +
    "prosperous year ahead. P.S. Toby, I hate you.";
  let helper = require('sendgrid').mail;
  let fromEmail = new helper.Email(from.email, from.name);
  let toEmail = new helper.Email(to.email, to.name);
  let body = new helper.Content("text/plain", template);
  let mail = new helper.Mail(fromEmail, subject, toEmail, body);
  let sg = require('sendgrid')(process.env.SENDGRID_API_KEY);
  let request = sg.emptyRequest({
    method: 'POST',
    path: '/v3/mail/send',
    body: mail.toJSON(),
  });
  sg.API(request, function(error, response) {
    if (error) { return callback(error); }
    callback();
  });
};
stream
  .on("error", function (err) {
    return console.error(err.response);
  })
  .on("data", function (data) {
    let name = data[0] + " " + data[1];
    let email = data[2];
    contactList.push({ name : name, email : email });
  })
  .on("end", function () {
    inquirer.prompt(questions).then(function (ans) {
      async.each(contactList, function (recipient, fn) {
        __sendEmail(recipient, ans.sender, ans.subject, fn);
      });
    });
  });


使用 SendGrid 模块需要我们去获取一个 API key.你可以在 SendGrid 的仪表盘生成这个 API key(需要创建一个账户),我们需要把它存在 Node.js 环境变量的 SENDGRID_API_KEY中.你可以使用 process.env 来获取环境变量.

在上面的代码中,我们使用 SendGrid API 和 Async 模块异步发送邮件.Async 模块是 Node.js 中最有用的模块之一.处理异步回调经常会导致回调地狱, 这通常出现在你的一个回调函数里处理了太多其他的回调函数,导致回调没有尽头.对于一个 JavaScript 开发者来说处理回调中的错误太过复杂,而 Async 模块可以帮你去解决回调地狱,提供了像 each, series, map 等许多实用的方法.这些方法能帮助我们更好的组织代码,从另一个方面讲,会让我们的异步代码更像同步的写法.

在这个示例中,相较于向 SendGrid 发送同步请求,我们选择发送异步请求来发送电子邮件.基于请求的响应,我们会发送随后的请求,使用 Async 模块中的 each 方法,我们遍历了 contactList 数组并且触发 __sendEmail函数.这个函数接受收件人和发送人的信息,邮件主题和异步请求的回调函数.__sendEmail 使用SendGrid API来发送电子邮件,它的官方文档上可以了解更多关于它的内容.一旦一封电子邮件成功送达,异步请求的回调函数就会触发,接着就会根据 contactList 下一项的内容继续发送邮件.到这里,我们已经成功创建了一个可以接收 CSV 文件输入并且发送邮件的命令行应用!


美化控制台的输出

既然已经完成了基本功能,现在让我们想一下如何美化控制台的输出结果,比如说错误和成功的信息.为了实现这个功能,我们需要使用用来优化控制台命令展示的 Chalk 模块.

stream
  .on("error", function (err) {
    return console.error(err.response);
  })
  .on("data", function (data) {
    let name = data[0] + " " + data[1];
    let email = data[2];
    contactList.push({ name : name, email : email });
  })
  .on("end", function () {
    inquirer.prompt(questions).then(function (ans) {
      async.each(contactList, function (recipient, fn) {
        __sendEmail(recipient, ans.sender, ans.subject, fn);
      }, function (err) {
        if (err) {
          return console.error(chalk.red(err.message));
        }
        console.log(chalk.green('Success'));
      });
    });
  });

在上面的代码片段中,我们在发送邮件的过程中添加了一个回调函数,它在任何一个异步过程里由于执行过程中的错误导致的完成或中断都会被触发.当异步过程没有完成,控制台会打印红色的信息,相反的,我们用绿色打印成功的信息.

如果你浏览一下 Chalk 的文档,你会发现有很多可自定义的选项,包括一系列的控制台颜色可选,还有下划线和加粗字体.


封装成 shell 命令

既然我们的工具已经完成了,是时候去让它执行起来像一个普通的 shell 命令了.首先,让我们在 broadcast.js 的顶部添加一个注释(shebang),这会告诉 shell 如何去执行这个脚本.

#!/usr/bin/env node
const program = require("commander");
const inquirer = require("inquirer");

现在让我们配置一下 package.json 来让命令变得可执行.

"description": "CLI utility to broadcast emails",
"main": "broadcast.js",
 "bin" : {
    "broadcast" : "./broadcast.js"
}


我们已经添加了一个新的属性 bin ,在这里我们提供了执行 broadcast.js 需要用到的命令.最后一步,让我们把脚本装载到全局环境上,这样我们就可以像一个普通的 shell 命令一样去执行它.

npm install -g

在执行这个命令之前,确认你在项目的目录中.安装完成后,你可以进行测试.

broadcast --help

这应该会打印出执行 node broadcat --help 后所有可用的选项.现在你可以准备向世界展示你自己的工具了.

有一件事要记住: 在开发过程中,当你只是简单的执行 broadcast 命令,任何你做的改变都不会生效,你会意识到命令的目录和你正在工作的项目目录是不同的.为了避免这种情况,在你的项目文件夹中运行 npm link???即可,这样会在你执行的命令和目录之间自动建立联系.在这之后,无论你做了任何改动同样也会反映在 broadcast 命令中.

在 JavaScript 项目之外,有很多类似的 CLI 工具在很多领域都运转良好.如果你在软件开发领域有一些经验,你就会明白 Bash 工具在开发过程中是必不可少的.从部署脚本到备份的定时任务,你可以用 Bash 脚本自动化任何工作.在 Docker, Chef 和 Puppet 成为事实上的基础设施管理标准之前,全靠 Bash 来完成这些工作.虽然 Bash 脚本总是会存在问题.它不能简单的融入到开发工作流中.通常情况,我们会使用各种各样的编程语言,而Bash 极少作为核心开发的一部分.甚至在 Bash 脚本中写一个简单的条件判断都要无穷无尽的调试和查阅文档.

但是,使用 JavaScript 能够让整个过程变得更简单更搞笑.所有工具都是天然跨平台的.如果你想在运行一个原生的 shell 命令,比如 git, mongodb或者 heroku, 使用 Node.js 的 Child Process 模块非常容易实现.这让我们可以在编写工具的时候充分享受到 JavaScript 的便利.

Node.js项目拆包工程化

在我们开发的过程中,经常会遇到这样的问题,开发完了一些代码或者一个接口,别的小伙伴过来问你,代码可不可以给他复用,接口可以给他调用。这说明代码的复用和抽象对团队协作是很重要的

使用 Node.js 开发简单的脚手架工具

像我们熟悉的 vue-cli,react-native-cli 等脚手架,只需要输入简单的命令 vue init webpack project,即可快速帮我们生成一个初始项目。在实际工作中,我们可以定制一个属于自己的脚手架,来提高自己的工作效率。

Kotlin + Node.js 搭建教程

Kotlin是JetBrains推出的一款语言, 相比Java有更简洁的语法, 能编译为Java Class, 也能编译为JavaScript Node.js则是可以运行在服务端的JavaScript, 这里把二者结合, 搭建一个用Kotlin编写的服务端应用

用node探究http缓存

用node搞web服务和直接用tomcat、Apache做服务器不太一样, 很多工作都需要自己做。缓存策略也要自己选择,虽然有像koa-static,express.static这些东西可以用来管理静态资源,但是为了开发或配置时更加得心应手,知其所以然,有了解http缓存的必要。另外,http缓存作为一个前端优化的一个要点,也应该有所了解。

node.js中常用的fs文件系统

node.js中常用的fs文件系统:fs文件系统模块对于系统文件及目录进行一些读写操作。模块中的方法均有异步和同步版本,例如读取文件内容的函数有异步的 fs.readFile() 和同步的 fs.readFileSync()。

happypack提升项目构建速度

运行在 Node.js 之上的 Webpack 是单线程模型的,也就是说 Webpack 需要处理的任务需要一件件挨着做,不能多个事情一起做。happypack把任务分解给多个子进程去并发的执行,子进程处理完后再把结果发送给主进程。

Node.js 前端开发指南

我们经常跟Node.js打交道,即使你是一名前端开发人员 -- npm脚本,webpack配置,gulp任务,程序打包 或 运行测试等。即使你真的不需要深入理解这些任务,但有时候你会感到困惑,会因为缺少Node.js的一些核心概念而以非常奇怪的方式来编码。

Node.js 应用:Koa2 使用 JWT 进行鉴权

在前后端分离的开发中,通过 Restful API 进行数据交互时,如果没有对 API 进行保护,那么别人就可以很容易地获取并调用这些 API 进行操作。那么服务器端要如何进行鉴权呢?

了解node.js事件循环

node.js的第一个基本论点是I / O的性能消耗是很昂贵。因此,使用当前编程技术的最大浪费来自于等待I / O完成。有几种方法可以处理性能影响

为什么要把 JavaScript 放到服务器端上运行?

JavaScript比C的开发门槛要低,尽管服务器端JavaScript存在已经很多年了,但是后端部分一直没有市场,JavaScript在浏览器中有广泛的事件驱动方面的应用,考虑到高性能、符合事件驱动、没有历史包袱这3个主要原因,JavaScript成为了Node的实现语言。