翻譯計劃 - 用 node.js 開發一個可交互的命令行應用

譯者:Icarus
原文連接:How To Develop An Interactive Command Line Application Using Node.jsjavascript

近幾年, Node.js 在軟件開發的一致性上助力很大.不管是前端開發,服務端腳本,跨平臺桌面/移動端應用或是物聯網應用,Node.js 均可以幫你完成.因爲 Node.js 的出現,編寫命令行工具比以前容易不少,這不是隨意說說,而是可交互,真正有價值的而且能減小開發耗時的命令行工具.前端

若是你是一名前端開發者,那你必定據說過或者使用過諸如 Gulp, Angular CLI, Cordova, Yeoman或其它的命令行工具.舉個例子,在使用 Angular CLI 的狀況下,經過執行ng new <project-name>這個命令,你會建立一個基於基礎配置的 Angular 項目.像 Yeoman 這樣的命令行工具會在運行過程當中須要你輸入一些內容從而幫助你個性化定製項目的配置.Yeoman 中的生成器(generators)會幫助你在生產環境部署項目.這就是咱們今天要學習的部分.java

拓展閱讀

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 Resourcesnode

在這個教程中,咱們會開發一個命令行應用,它能夠接收一個 CSV 格式的用戶信息文件,經過使用 SendGrid API能夠像這些用戶發送電子郵件.下面是教程的內容大綱:react

  1. "Hello,World"
  2. 處理命令行參數
  3. 運行時的用戶輸入
  4. 異步網絡會話
  5. 美化控制檯的輸出
  6. 封裝成 shell 命令
  7. JavaScript 以外

"Hello,World"

這個教程假設你的系統裏已經安裝好了 Node.js. 若是你沒有,請先安裝它.在安裝 Node.js的同時會附帶一個叫 npm 的包管理器.使用 npm 你能夠安裝不少開源的包.你能夠在 npm 的官網站點上獲取所有的包列表.這個項目咱們會用到一些開源的模塊(以後會更多).如今,讓咱們用 npm 建立一個 Node.js 項目.webpack

$ 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 倉庫地址,開源許可證和做者名.你能夠填寫這些選項或者空着它們.git

npm init成功執行以後,你會在文件夾裏看到一個 package.json文件已經建立好了.這是咱們的配置文件.與此同時,它也保存着咱們在建立項目時提供的信息.你能夠在npm 官方文檔中瀏覽更多有關package.json的內容.github

既然項目已經建立好了,那就讓咱們建立一個"Hello world"程序.開始以前,你須要在你的項目中新建一個 broadcast.js文件,這個是以後主要用到的文件,在文件中寫入以下代碼段:web

console.log('hello world');複製代碼

如今讓咱們運行一下.mongodb

$ 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.jsSendGrid這些模塊.隨着咱們教程的深刻,這些模塊的具體用法和細節會慢慢解釋.

處理命令行參數

讀取命令行參數並非很難.你能夠用 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,passwordraw 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 APIAsync 模塊異步發送郵件.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 以外

JavaScript 項目以外,有不少相似的 CLI 工具在不少領域都運轉良好.若是你在軟件開發領域有一些經驗,你就會明白 Bash 工具在開發過程當中是必不可少的.從部署腳本到備份的定時任務,你能夠用 Bash 腳本自動化任何工做.在 Docker, Chef 和 Puppet 成爲事實上的基礎設施管理標準以前,全靠 Bash 來完成這些工做.雖然 Bash 腳本老是會存在問題.它不能簡單的融入到開發工做流中.一般狀況,咱們會使用各類各樣的編程語言,而Bash 極少做爲核心開發的一部分.甚至在 Bash 腳本中寫一個簡單的條件判斷都要無窮無盡的調試和查閱文檔.

可是,使用 JavaScript 可以讓整個過程變得更簡單更搞笑.全部工具都是自然跨平臺的.若是你想在運行一個原生的 shell 命令,好比 git, mongodb或者 heroku, 使用 Node.jsChild Process 模塊很是容易實現.這讓咱們能夠在編寫工具的時候充分享受到 JavaScript 的便利.

我但願這個教程對你有幫助,若是有任何問題,能夠評論或者聯繫我.

相關文章
相關標籤/搜索