用node.js開發一個可交互的命令行應用
近幾年, Node.js 在軟件開發的一致性上助力很大.無論是前端開發,服務端腳本,跨平臺桌面/移動端應用或是物聯網應用, Node.js 都可以幫你完成.由于 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 可以像這些用戶發送電子郵件.下面是教程的內容大綱:
- “Hello,World”
- 處理命令行參數
- 運行時的用戶輸入
- 異步網絡會話
- 美化控制臺的輸出
- 封裝成 shell 命令
- 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 | |
---|---|---|
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 之外
在 JavaScript 項目之外,有很多類似的 CLI 工具在很多領域都運轉良好.如果你在軟件開發領域有一些經驗,你就會明白 Bash 工具在開發過程中是必不可少的.從部署腳本到備份的定時任務,你可以用 Bash 腳本自動化任何工作.在 Docker, Chef 和 Puppet 成為事實上的基礎設施管理標準之前,全靠 Bash 來完成這些工作.雖然 Bash 腳本總是會存在問題.它不能簡單的融入到開發工作流中.通常情況,我們會使用各種各樣的編程語言,而 Bash 極少作為核心開發的一部分.甚至在 Bash 腳本中寫一個簡單的條件判斷都要無窮無盡的調試和查閱文檔.
但是,使用 JavaScript 能夠讓整個過程變得更簡單更搞笑.所有工具都是天然跨平臺的.如果你想在運行一個原生的 shell 命令,比如 git , mongodb 或者 heroku , 使用 Node.js 的 Child Process 模塊非常容易實現.這讓我們可以在編寫工具的時候充分享受到 JavaScript 的便利.
我希望這個教程對你有幫助,如果有任何問題,可以評論或者聯系我.
來自:http://xdlrt.github.io/2017/04/15/2017-03-20/