手把手教你如何使用nodejs編寫(xiě)cli命令行
前端日常開(kāi)發(fā)中,會(huì)遇見(jiàn)各種各樣的cli,比如一行命令幫你打包的webpack,一行命令幫你生成vue項(xiàng)目模板的vue-cli,還有創(chuàng)建react項(xiàng)目的create-react-app等等等等。這些工具極大地方便了我們的日常工作,讓計(jì)算機(jī)自己去干繁瑣的工作,而我們,就可以節(jié)省出大量的時(shí)間用于學(xué)習(xí)、交流、開(kāi)發(fā)、 逛steam 。
但是有時(shí)候一些十分特別的需求,我們是找不到適合的cli工具去做的。比如說(shuō),你的項(xiàng)目十分龐大,你給項(xiàng)目添加一個(gè)新的路由,要經(jīng)過(guò) 創(chuàng)建目錄 -> 創(chuàng)建.vue文件 -> 更新vue-router的路由列表 這一趟流程,就算快捷鍵創(chuàng)建目錄文件用得再熟悉,也比不過(guò)你一行命令來(lái)得快,特別是路由目錄嵌套深,.vue文件初始化模板復(fù)雜的時(shí)候。
所以呢,何不為自己項(xiàng)目寫(xiě)一個(gè)cli?就專門(mén)做這些繁瑣的活?
0x1 hello world
nodejs的cli,本質(zhì)就是跑node腳本嘛,基本上每位前端er都會(huì):
// index.js
console.log('hello world')
然后命令行調(diào)用
> node index.js ## 輸出: > hello world
可以做得更逼真一點(diǎn),我們?cè)趐ackage.json里面的scripts字段上添加一下腳本名:
{
"scripts":{
"hello":"node index.js"
}
}
然后命令行調(diào)用:
> npm run hello

但是,看到這里你肯定會(huì)說(shuō),人家webpack還有vue-cli都是“有名字”的!什么 vue-cli init app 、 webpack -p 的,多漂亮,看看這個(gè)命令行, node index.js ,還 npm run hello ,誰(shuí)不會(huì)啊,丑不拉幾的,怕又不是來(lái)水文章的哦?差評(píng)!!
別急啊各位大人,接下來(lái)就說(shuō)說(shuō),如何給這個(gè)node腳本起個(gè)名字。
0x2 起名字
姑且,先把這個(gè)cli的名字命名為 hello-cli ,就是我們能夠在命令行里面,輸入 hello-cli ,然后它就打印一句 hello world ,沒(méi)有 node 也沒(méi)有 npm ,就是:

這里,我們需要做幾步操作:
1、index.js文件頂部聲明執(zhí)行環(huán)境:
// index.js
#!/usr/bin/env node
console.log('hello world')
添加 #!/usr/bin/env node 或者 #!/usr/bin/node ,這是告訴系統(tǒng),下面這個(gè)腳本,使用nodejs來(lái)執(zhí)行。當(dāng)然,這個(gè)系統(tǒng)不包括windows,因?yàn)閣indows下有個(gè)JScript的歷史遺留物在,會(huì)讓你的腳本跑不起來(lái)。
#!/usr/bin/env node 的意思是讓系統(tǒng)自己去找node的執(zhí)行程序。
#!/usr/bin/node 的意思是,明確告訴系統(tǒng),node的執(zhí)行程序在路徑為 /usr/bin/node 。
2、添加package.json的bin字段。
可以在index.js當(dāng)前的目錄下執(zhí)行 npm init 創(chuàng)建一個(gè)package.json,然后在package.json里面,添加一個(gè)bin字段:
{
"name": "hello-test",
"version": "1.0.0",
"bin":{
"hello-cli":"index.js"
}
}
bin字段里面寫(xiě)上這個(gè)命令行的名字,也就是 hello-cli ,它告訴npm,里面的js腳本可以通過(guò)命令行的方式執(zhí)行,以 hello-cli 的命令調(diào)用。當(dāng)然命令行的名字你想寫(xiě)什么都是你的自由,比如:


3、 在當(dāng)前package.json目錄下,打開(kāi)命令行工具,執(zhí)行 npm link ,將當(dāng)前的代碼在npm全局目錄下留個(gè)快捷方式。
npm檢測(cè)到package.json里面存在一個(gè)bin字段,它就同時(shí)在全局npm包目錄下生成了一個(gè)可執(zhí)行文件:

當(dāng)我們?cè)谙到y(tǒng)命令行直接執(zhí)行 hello-cli 的時(shí)候,實(shí)際上就是執(zhí)行這里的腳本。
因?yàn)榘惭bnode的時(shí)候,npm將這個(gè)目錄配置為系統(tǒng)變量環(huán)境了,當(dāng)你執(zhí)行命令的時(shí)候,系統(tǒng)會(huì)先找系統(tǒng)命令和系統(tǒng)變量,然后到變量環(huán)境里面去查找這個(gè)命令名,然后找到這個(gè)目錄后,發(fā)現(xiàn)匹配上了該命令名的可執(zhí)行文件,接著就直接執(zhí)行它。vue-cli也好,webpack-cli也好,都是這樣執(zhí)行的。
這樣,你的第一個(gè)cli腳本就成功安裝了,可以在命令行里面,直接敲你的cli名字,看看結(jié)果輸出吧。
另外,如果你僅希望你的cli腳本僅在項(xiàng)目里執(zhí)行,則需要在你項(xiàng)目里面新建一個(gè)目錄,重復(fù)上述的操作,只是在第三步的時(shí)候,不要llink到全局里面去,而是使用 npm i -D file:<你的腳本cli目錄路徑> ,把它當(dāng)成項(xiàng)目的依賴安裝到node_modules里面去,如果安裝成功,那么在項(xiàng)目的package.json你會(huì)看到多了一條依賴,這條依賴的值不是版本號(hào),而是你腳本的路徑。然后在node_modules里面會(huì)有一個(gè).bin目錄,里面就存放著你的可執(zhí)行文件。
局部安裝建議用 npm i -D file:xxx ,這樣它會(huì)在package.json留條記錄,方便其他小伙伴看到。自然,你的腳本最好也是放進(jìn)項(xiàng)目目錄里面。
當(dāng)然,這樣安裝的cli腳本,必須在項(xiàng)目的package.json的scripts字段上聲明腳本命令,然后通過(guò) npm run 的方式執(zhí)行。

哦?這樣子使用的話不就回到最最最開(kāi)始的時(shí)候那種原始的 npm run hello 一樣么。
是的,但是有質(zhì)的區(qū)別。使用 node index.js 這種方式調(diào)用的話固然簡(jiǎn)單靈活,但是嚴(yán)重依賴腳本路徑,一旦目錄結(jié)構(gòu)發(fā)生變動(dòng),寫(xiě)在scripts的命令就要更改一次;但是使用npm安裝之后,本地的cli腳本就被拉到node_modules里面,目錄結(jié)構(gòu)變動(dòng)對(duì)其影響不大。其次是不利于分享與發(fā)布,如果你想把你的cli腳本發(fā)布出去,那么有一個(gè)好聽(tīng)響亮的名字,比起在說(shuō)明文檔里面告訴使用者如何找到你的腳本路徑再用node執(zhí)行它,簡(jiǎn)直好上那么一萬(wàn)倍不是么?
這里也給我們提供了一個(gè)cli開(kāi)發(fā)流程思路:
- 初期開(kāi)發(fā)可以通過(guò)node index.js來(lái)看效果。
- 測(cè)試的時(shí)候可以通過(guò)npm link的方式進(jìn)行安裝測(cè)試。
- 發(fā)布
0x3 參數(shù)讀取:process.argv
名字有了,輸出也有了,看看我們跟那些大名鼎鼎的cli工具,在形式上還差點(diǎn)啥?對(duì)了,人家可以支持不同參數(shù)選項(xiàng)的,還可以根據(jù)輸入的不同,產(chǎn)生不同的結(jié)果。
這樣吧,我們給這個(gè)cli加一個(gè)功能,既然叫 hello-cli ,那不能只會(huì) hello world 吧,必須要見(jiàn)誰(shuí)就說(shuō) hello 才行:
> hello-cli older ## 輸出 > hello older
雖然這個(gè)功能很簡(jiǎn)單,但是至少也是實(shí)現(xiàn)了“根據(jù)輸入的不同,產(chǎn)生不同結(jié)果”的效果。
命令行上的參數(shù),可以通過(guò) process 這個(gè)變量獲取, process 是一個(gè)全局對(duì)象而不是一個(gè)包,不需要通過(guò) require 引入。通過(guò) process 這個(gè)對(duì)象我們可以拿到當(dāng)前腳本執(zhí)行環(huán)境等一系列信息,其中就包括命令行的輸入情況,這個(gè)信息,保存在 process.argv 這個(gè)屬性里。我們可以打印一下:
//index.js console.log(process.argv);
打印結(jié)果:

可以看出,argv是個(gè)數(shù)組,前兩位是固定的,分別是node程序的路徑和腳本存放的位置,從第三位開(kāi)始才是額外輸入的內(nèi)容。那么實(shí)現(xiàn)上面的功能就很簡(jiǎn)單了,只要讀取argv數(shù)組的第三位,然后輸出出來(lái)就可以了。
//index.js
console.log(`hello ${process.argv[2]||'world'}`)
npm社區(qū)中也有一些優(yōu)秀的命令行參數(shù)解析包,比如yargs ,tj的commander.js 等等
如果你想使用比較復(fù)雜的參數(shù)或者命令,建議還是用第三方包比較好,手寫(xiě)解析太耗精力了。
0x4 子進(jìn)程
現(xiàn)在,你可以自由自在的寫(xiě)你自己的cli腳本了。
如果你希望寫(xiě)一個(gè)項(xiàng)目打完包自動(dòng)推上git的cli,或者自動(dòng)從git倉(cāng)庫(kù)里面拉取項(xiàng)目啟動(dòng)模板,那么,你需要通過(guò)node的 child_process 模塊開(kāi)啟子進(jìn)程,在子進(jìn)程內(nèi)調(diào)用git命令:
//test.js
const child_process = require('child_process');
let subProcess=child_process.exec("git version",function(err,stdout){
if(err)console.log(err);
console.log(stdout);
subProcess.kill()
});
不僅是git命令,包括系統(tǒng)命令、其他cli命令都可以在這里執(zhí)行。特別是系統(tǒng)命令,使用系統(tǒng)命令對(duì)文件目錄進(jìn)行操作,效率比f(wàn)s高到不知道哪里去了。
社區(qū)上也有一些不錯(cuò)的包,比如阮一峰老師推薦的shelljs
0x5 美化輸出
如果你不那么希望你的cli用起來(lái)那么“硬核”,希望更人性化一點(diǎn),比如提供一些友好的輸入、提示啊,給你的輸出加點(diǎn)顏色區(qū)分重點(diǎn)啊,寫(xiě)個(gè)簡(jiǎn)單的進(jìn)度條啊等等,那么你就需要美化一下你的輸出了。

除了顏色這部分,不使用第三方包實(shí)現(xiàn)起來(lái)非常繁瑣復(fù)雜,其他的功能,都可以試試自己寫(xiě)。
顏色部分使用了第三方包 colors ,這里就不演示了。
其他都是由nodejs自帶的readline模塊實(shí)現(xiàn)的。
//index.js
const readline = require('readline');
const unloadChar='-';
const loadedChar='=';
const rl=readline.createInterface({
input: process.stdin,
output: process.stdout
});
rl.question('你想對(duì)誰(shuí)說(shuō)聲hello? ',answer=>{
let i = 0;
let time = setInterval(()=>{
if(i>10){
clearInterval(time);
readline.cursorTo(process.stdout, 0, 0);
readline.clearScreenDown(process.stdout);
console.log(`hello ${answer}`);
process.exit(0)
return
}
readline.cursorTo(process.stdout,0,1);
readline.clearScreenDown(process.stdout);
renderProgress('saying hello',i);
i++
},200);
});
function renderProgress(text,step){
const PERCENT = Math.round(step*10);
const COUNT = 2;
const unloadStr = new Array(COUNT*(10-step)).fill(unloadChar).join('');
const loadedStr = new Array(COUNT*(step)).fill(loadedChar).join('');
process.stdout.write(`${text}:【${loadedStr}${unloadStr}|${PERCENT}%】`)
}
首先,通過(guò) readline.createInterface 方法創(chuàng)建一個(gè) interface 類 ,這個(gè)類下面有一個(gè)方法 .question ,用這個(gè)方法在命令行上拋出一個(gè)問(wèn)題,在第二個(gè)參數(shù)傳入一個(gè)函數(shù)進(jìn)行監(jiān)聽(tīng)。一旦用戶輸入完畢敲下回車,就會(huì)觸發(fā)回調(diào)函數(shù)。
然后我們?cè)诨卣{(diào)函數(shù)里面寫(xiě)了個(gè)計(jì)時(shí)器,假裝我們?cè)谔幚砟承┦聞?wù)。
使用 readline.cursorTo 這個(gè)方法,可以改變命令行上的光標(biāo)的位置。
readline.cursorTo(process.stdout, 0, 0); 是移動(dòng)到第1列第1行上,
readline.cursorTo(process.stdout, 0, 1); 是移動(dòng)到第1列第2行上。
使用 readline.clearScreenDown 這個(gè)方法,是讓命令行從當(dāng)前行開(kāi)始,到最后一行結(jié)束,將這兩行之間所有內(nèi)容清除。
renderProgress 是自己封裝的一個(gè)方法,通過(guò) process.stdout.write 方法輸出一行看起來(lái)像是進(jìn)度條的字符串到命令行上。
所以在計(jì)時(shí)器里面,當(dāng)計(jì)數(shù)小于10的時(shí)候,我們讓光標(biāo)移到第一行上,然后清除所有輸出,輸出進(jìn)度條字符串;當(dāng)計(jì)數(shù)大于10的時(shí)候,我們關(guān)掉計(jì)時(shí)器,清除輸出,打印結(jié)果。
最后不要忘記關(guān)掉進(jìn)程,可以使用 interface 這個(gè)類的 .close 方法關(guān)掉readline進(jìn)程,也可以直接調(diào)用 process.exit 退出。
繪制的思路跟canvas繪制動(dòng)畫(huà)一樣,只不過(guò)canvas是清除畫(huà)布,而命令行這里是通過(guò) readline.clearScreenDown 清除輸出。
這樣,一個(gè)簡(jiǎn)易的,人性化的,帶點(diǎn)點(diǎn)進(jìn)度條動(dòng)畫(huà)的命令行cli工具就寫(xiě)好了,你也可以發(fā)揮你的想象力,去寫(xiě)一些更有趣的效果出來(lái)。
畢竟我們前端,有瀏覽器我們可以寫(xiě)動(dòng)畫(huà),沒(méi)了瀏覽器我們一樣可以寫(xiě)動(dòng)畫(huà)。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
node.js中的fs.fchmod方法使用說(shuō)明
這篇文章主要介紹了node.js中的fs.fchmod方法使用說(shuō)明,本文介紹了fs.fchmod的方法說(shuō)明、語(yǔ)法、接收參數(shù)、使用實(shí)例和實(shí)現(xiàn)源碼,需要的朋友可以參考下2014-12-12
Mongoose經(jīng)常返回e11000 error的原因分析
這篇文章主要給大家分析了Mongoose經(jīng)常返回e11000 error的原因,文中介紹的非常詳細(xì),對(duì)大家具有一定的參考價(jià)值,需要的朋友可以們下面來(lái)一起看看吧。2017-03-03
node.js中的path.extname方法使用說(shuō)明
這篇文章主要介紹了node.js中的path.extname方法使用說(shuō)明,本文介紹了path.extname的方法說(shuō)明、語(yǔ)法、使用實(shí)例和實(shí)現(xiàn)源碼,需要的朋友可以參考下2014-12-12
快速使用node.js進(jìn)行web開(kāi)發(fā)詳解
本篇文章主要介紹了快速使用node.js進(jìn)行web開(kāi)發(fā)詳解,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-04-04
NodeJS前端自動(dòng)化部署實(shí)現(xiàn)實(shí)例詳解
這篇文章主要為大家介紹了NodeJS前端自動(dòng)化部署實(shí)現(xiàn)實(shí)例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-10-10

