原文譯自Smashing Magazine -- How To Develop An Interactive Command Line Application Using Node.js
相信很多前端都聽說過或者使用過Gulp, Angular CLI, Cordova, Yeoman或其他類似的命令行工具铺韧。但有想過這些程序是怎么實現(xiàn)的嗎诲侮?例如在Angular CLI中使用ng new <project-name>
后會建立一個已經有基本配置的angular項目翩肌;又或者像Yeoman迂苛,也能運行時候輸入或者選擇配置項,讓用戶能夠自定義項目配置弃揽,快速搭建好開發(fā)時候需要用到的開發(fā)環(huán)境脯爪。下面的教程,就是講如何使用node寫一個像這樣的命令行工具矿微。
在這篇教程中痕慢,我們會開發(fā)一個命令行工具,用戶能夠輸入一個CSV文件地址涌矢,從而獲取到文件里面的用戶信息掖举,然后模擬群發(fā)郵件(原文是使用SendGrid Api模擬發(fā)送)
文章目錄:
1."Hello World"
2.處理命令行參數(shù)
3.運行時輸入參數(shù)
4.模擬發(fā)送郵件
5.改變輸出內容樣式
6.變成shell命令
“Hello World”
開始前,首先你得有node娜庇,如果沒有拇泛,請自行安裝下滨巴。node中自帶npm,使用npm能安裝許多開源的node模塊俺叭。首先,使用npm創(chuàng)建一個node項目
$ npm init
name: broadcast
version: 0.0.1
description: CLI utility to broadcast email
entry point: broadcast.js
除這些參數(shù)外泰偿,npm還提供了其他如Git repository等參數(shù)熄守,可根據(jù)自身需求設置輸入。執(zhí)行完npm init
后耗跛,會發(fā)現(xiàn)在同目錄下生成了一個package.json
文件裕照,文件里面包含了上面命令輸入的信息。配置內容信息可以在package.json文檔中找到调塌。
然后晋南,還是從最簡單的Hello World入手。首先在同目錄下建一個broadcast.js
文件
// broadcast.js
console.log('Hello World!')
然后在terminal中執(zhí)行
$ node broadcast
Hello World!
well done, 根據(jù)package.json文檔羔砾,我們可以找到一個dependencies
參數(shù)负间,在這參數(shù)中你可以找到所有這項目需要用到的第三方模塊和它們的版本號,上面也有提及到姜凄,我們需要用到模塊去開發(fā)這個工具政溃。最后開發(fā)完成,package.json
應該如下
{
"name": "broadcast",
"version": "0.0.1",
"description": "CLI utility to broadcast emails",
"main": "broadcast.js",
"license": "MIT",
"dependencies": {
"chalk": "^1.1.3",
"commander": "^2.9.0",
"csv": "^1.1.0",
"inquirer": "^2.0.0"
}
}
這幾個模塊 Chalk, Commander, Inquirer, CSV的具體用處跟其他參數(shù)态秧,可以自行查看董虱。
處理命令行參數(shù)
node原生也有讀取命令行的函數(shù)process.argv
,但是解析參數(shù)是個繁瑣的工作申鱼,所以我們會使用Commander去替代這些工作愤诱。Commande的另外一個好處就是不用額外的去寫一個--help
函數(shù),只要定義了其他參數(shù)捐友,--help
函數(shù)就會自動生成淫半。首先安裝一下Commander和其他package
$ npm install commander chalk csv inquirer --save
然后修改broadcast.js
// broadcast
const 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)
從上面可以看出,處理一個參數(shù)是十分簡單的楚殿。我們定義了一個--list
的參數(shù)撮慨,現(xiàn)在我們就能通過--list
參數(shù)獲取到命令行傳過來的值。在這程序中脆粥,list應該是接收一個csv的地址參數(shù)砌溺,然后打印在console中。
$ node broadcast --list ./test.csv
./test.csv
從js中可以看到還有一個version
參數(shù)变隔,所以我們可以使用--version
讀取版本號规伐。
$ node broadcast --version
0.0.1
又或者能使用--help
獲取app能接收的參數(shù)
$ 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
現(xiàn)在我們已經能夠接收到命令行傳遞過來的參數(shù)了,下面我們會利用接收到的CSV文件地址匣缘,并使用CSV模塊處理CSV文件的內容猖闪。
我們會使用下面的比哦啊哥內容作為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 |
現(xiàn)在,更新下broadcast.js
吵护,使用CSV讀取內容并打印在console
// broadcast.js
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)
const stream = fs.createReadStream(program.list)
stream
.pipe(csv.parse({ delimiter : "," }))
.on('data', function(data) {
const firstname = data[0]
const lastname = data[1]
const email = data[2]
console.log(firstname, lastname, email)
})
除csv模塊外盒音,還使用了node的File System模塊讀取文件內容,csv的parse
方法把列數(shù)據(jù)解析為數(shù)組馅而,然后在terminal中運行一下命令
$ node broadcast.js --list ./test.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
運行時輸入參數(shù)
上面已經實現(xiàn)了獲取命令行參數(shù)祥诽,但如果想在運行時候接收參數(shù)值的話我們就需要另外一個模塊inquirer.js,通過這個模塊瓮恭,用戶可以自定義多種參數(shù)類型雄坪,如文本,密碼屯蹦,單選或者多選列表等维哈。
下面的demo會通過inquirer接收郵件發(fā)送人的名字,email還有郵件主題颇玷。
// broadcast.js
...
const inquirer = require('inquirer')
const 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 - "
}
]
program
.version('0.0.1')
.option('-l, --list [list]', 'List of customers in CSV')
.parse(process.argv)
// 儲存CSV數(shù)據(jù)
const contactList = []
const stream = fs.createReadStream(program.list)
.pipe(csv.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)
})
})
Inquire.js的prompt
方法接受一個數(shù)組參數(shù)笨农,數(shù)組里可以自定義運行時需要接受的問題參數(shù),在這demo里面帖渠,我們想知道發(fā)送者的名字還要email還有郵件主題谒亦,所以定義了一個questions
的數(shù)組來儲存問題,從對象里面可以看到有一個input
的參數(shù)空郊,除此外還可以接受password
等其他類型份招,具體可以查詢一下inquirer的文檔。此外狞甚,參數(shù)name
保存input的key值锁摔。prompt
方法會返還一個promise對象,promise中會返回一個answer
變量哼审,里面帶有剛才輸入的值谐腰。
$ node broadcast -l input/employees.csv
? Sender's email address - kitssang_demo@163.com
? Sender's name - kit
? Subject - Hello World
{ sender:
{ email: ' kitssang_demo@163.com',
name: 'kit' },
subject: 'Hello World' }
模擬發(fā)送郵件
由于原文使用的sendgrid
沒有跑通,所以只組裝了一下數(shù)據(jù)模擬了發(fā)送郵件涩盾。原本的第五部分也在這里一起用上了十气。
// broadcast.js
...
program
.version('0.0.1')
.option('-l, --list [list]', 'list of customers in CSV file')
.parse(process.argv)
const sendEmail = function(to, from, subject) {
const sender = chalk.green(`${from.name}(${from.email})`)
const receiver = chalk.green(`${to.name}(${to.email})`)
const theme = chalk.blue(subject)
console.log(`${sender} send a mail to ${receiver} and the subject of the email is ${theme}`)
}
// 儲存CSV數(shù)據(jù)
let concatList = []
const stream = fs.createReadStream(program.list)
.pipe(csv.parse({
delimiter: ','
}))
.on('data', function(data) {
const name = data[0] + ' ' + data[1]
const email = data[2]
concatList.push({
name: name,
email: email
})
})
.on('end', function() {
inquirer.prompt(questions).then((ans) => {
for (let i = 0; i < concatList.length; i++) {
sendEmail(concatList[i], ans.sender, ans.subject)
}
}).catch((err) => {
console.log(err)
})
})
由于沒有異步請求,async
模塊沒有用上春霍,另外使用了chalk
模塊改變了console打印結果的顏色砸西。
變成shell命令
至此,整個工具已經基本完成,但是如果想像一個普通的shell命令(不加$ node xx
)執(zhí)行芹枷,還需要做以下操作衅疙。首先,添加shebang在js的頭部鸳慈,讓shell知道如何執(zhí)行這個文件饱溢。
#!/usr/bin/env node
// broadcast.js
const program = require("commander")
const inquirer = require("inquirer")
...
然后再配置一下package.json
使代碼可運行
…
"description": "CLI utility to broadcast emails",
"main": "broadcast.js",
"bin" : {
"broadcast" : "./broadcast.js"
}
…
從代碼可以看到加了一個bin
的參數(shù),這個參數(shù)可以使broadcast命令與broadcast.js建立連接蝶涩。
最后一步理朋,在全局安裝一下依賴包。在項目目錄運行一下下面的命令绿聘。
$ npm install -g
然后測試一下命令
$ broadcast --help
需要注意的是,在開發(fā)時候如果使用commaner
默認給出的命令執(zhí)行broadcast
則在代碼中所做的任何更改都是看不見的次舌。假如輸入which broadcast
熄攘,你會發(fā)現(xiàn)地址不是你當前目錄,所以這時應該要用npm link
去查看命令的目錄映射彼念。