手把手教你使用nodejs編寫cli(命令行)

前端日常開發(fā)中抄谐,會遇見各種各樣的cli鸵隧,比如一行命令幫你打包的webpack碳抄,一行命令幫你生成vue項目模板的vue-cli箫爷,還有創(chuàng)建react項目的create-react-app等等等等犁苏。這些工具極大地方便了我們的日常工作硬萍,讓計算機自己去干繁瑣的工作,而我們围详,就可以節(jié)省出大量的時間用于學習朴乖、交流、開發(fā)助赞、逛steam买羞。

但是有時候一些十分特別的需求,我們是找不到適合的cli工具去做的雹食。比如說畜普,你的項目十分龐大,你給項目添加一個新的路由群叶,要經(jīng)過創(chuàng)建目錄 -> 創(chuàng)建.vue文件 -> 更新vue-router的路由列表這一趟流程吃挑,就算快捷鍵創(chuàng)建目錄文件用得再熟悉钝荡,也比不過你一行命令來得快,特別是路由目錄嵌套深舶衬,.vue文件初始化模板復雜的時候埠通。

所以呢,何不為自己項目寫一個cli逛犹?就專門做這些繁瑣的活端辱?

0x1 hello world

nodejs的cli,本質就是跑node腳本嘛圾浅,基本上每位前端er都會:

// index.js
console.log('hello world')

然后命令行調用

> node index.js

## 輸出:
> hello world

可以做得更逼真一點掠手,我們在package.json里面的scripts字段上添加一下腳本名:

{
    "scripts":{
        "hello":"node index.js"
    }
}

然后命令行調用:

> npm run hello
輸出

但是,看到這里你肯定會說狸捕,人家webpack還有vue-cli都是“有名字”的喷鸽!什么vue-cli init appwebpack -p的灸拍,多漂亮做祝,看看這個命令行,node index.js鸡岗,還npm run hello混槐,誰不會啊,丑不拉幾的轩性,怕又不是來水文章的哦声登?差評!揣苏!

別急啊各位大人悯嗓,接下來就說說,如何給這個node腳本起個名字卸察。

0x2 起名字

姑且脯厨,先把這個cli的名字命名為hello-cli,就是我們能夠在命令行里面坑质,輸入hello-cli合武,然后它就打印一句hello world,沒有node也沒有npm涡扼,就是:

hello-cli

這里稼跳,我們需要做幾步操作:

  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)壳澳,下面這個腳本岂贩,使用nodejs來執(zhí)行。當然,這個系統(tǒng)不包括windows萎津,因為windows下有個JScript的歷史遺留物在卸伞,會讓你的腳本跑不起來。
    #!/usr/bin/env node的意思是讓系統(tǒng)自己去找node的執(zhí)行程序锉屈。
    #!/usr/bin/node的意思是荤傲,明確告訴系統(tǒng),node的執(zhí)行程序在路徑為/usr/bin/node颈渊。
  2. 添加package.json的bin字段遂黍。
    可以在index.js當前的目錄下執(zhí)行npm init創(chuàng)建一個package.json,然后在package.json里面俊嗽,添加一個bin字段:
    {
         "name": "hello-test",
         "version": "1.0.0",
         "bin":{
             "hello-cli":"index.js"  
         }
    }
    
    bin字段里面寫上這個命令行的名字雾家,也就是hello-cli,它告訴npm绍豁,里面的js腳本可以通過命令行的方式執(zhí)行芯咧,以hello-cli的命令調用。當然命令行的名字你想寫什么都是你的自由竹揍,比如:
    bin

    +1s
  3. 在當前package.json目錄下敬飒,打開命令行工具,執(zhí)行npm link芬位,將當前的代碼在npm全局目錄下留個快捷方式无拗。
    npm檢測到package.json里面存在一個bin字段,它就同時在全局npm包目錄下生成了一個可執(zhí)行文件:
    image

    當我們在系統(tǒng)命令行直接執(zhí)行hello-cli的時候昧碉,實際上就是執(zhí)行這里的腳本英染。
    因為安裝node的時候,npm將這個目錄配置為系統(tǒng)變量環(huán)境了被饿,當你執(zhí)行命令的時候税迷,系統(tǒng)會先找系統(tǒng)命令和系統(tǒng)變量,然后到變量環(huán)境里面去查找這個命令名锹漱,然后找到這個目錄后,發(fā)現(xiàn)匹配上了該命令名的可執(zhí)行文件慕嚷,接著就直接執(zhí)行它哥牍。vue-cli也好,webpack-cli也好喝检,都是這樣執(zhí)行的嗅辣。

這樣,你的第一個cli腳本就成功安裝了挠说,可以在命令行里面澡谭,直接敲你的cli名字,看看結果輸出吧损俭。

另外蛙奖,如果你僅希望你的cli腳本僅在項目里執(zhí)行潘酗,則需要在你項目里面新建一個目錄,重復上述的操作雁仲,只是在第三步的時候仔夺,不要llink到全局里面去,而是使用npm i -D file:<你的腳本cli目錄路徑>攒砖,把它當成項目的依賴安裝到node_modules里面去缸兔,如果安裝成功,那么在項目的package.json你會看到多了一條依賴吹艇,這條依賴的值不是版本號惰蜜,而是你腳本的路徑。然后在node_modules里面會有一個.bin目錄受神,里面就存放著你的可執(zhí)行文件抛猖。

局部安裝建議用npm i -D file:xxx,這樣它會在package.json留條記錄路克,方便其他小伙伴看到樟结。自然,你的腳本最好也是放進項目目錄里面精算。

當然瓢宦,這樣安裝的cli腳本,必須在項目的package.json的scripts字段上聲明腳本命令灰羽,然后通過npm run的方式執(zhí)行驮履。

image

哦?這樣子使用的話不就回到最最最開始的時候那種原始的npm run hello一樣么廉嚼。

是的玫镐,但是有質的區(qū)別。使用node index.js這種方式調用的話固然簡單靈活怠噪,但是嚴重依賴腳本路徑恐似,一旦目錄結構發(fā)生變動,寫在scripts的命令就要更改一次傍念;但是使用npm安裝之后矫夷,本地的cli腳本就被拉到node_modules里面,目錄結構變動對其影響不大憋槐。其次是不利于分享與發(fā)布双藕,如果你想把你的cli腳本發(fā)布出去贴唇,那么有一個好聽響亮的名字纠屋,比起在說明文檔里面告訴使用者如何找到你的腳本路徑再用node執(zhí)行它,簡直好上那么一萬倍不是么浆洗?

這里也給我們提供了一個cli開發(fā)流程思路:

  • 初期開發(fā)可以通過node index.js來看效果。
  • 測試的時候可以通過npm link的方式進行安裝測試嘶摊。
  • 發(fā)布

0x3 參數(shù)讀取:process.argv

名字有了延蟹,輸出也有了,看看我們跟那些大名鼎鼎的cli工具更卒,在形式上還差點啥等孵?對了,人家可以支持不同參數(shù)選項的蹂空,還可以根據(jù)輸入的不同俯萌,產(chǎn)生不同的結果。

這樣吧上枕,我們給這個cli加一個功能咐熙,既然叫hello-cli,那不能只會hello world吧辨萍,必須要見誰就說hello才行:

> hello-cli older
## 輸出
> hello older

雖然這個功能很簡單棋恼,但是至少也是實現(xiàn)了“根據(jù)輸入的不同,產(chǎn)生不同結果”的效果锈玉。

命令行上的參數(shù)爪飘,可以通過process這個變量獲取,process是一個全局對象而不是一個包拉背,不需要通過require引入师崎。通過process這個對象我們可以拿到當前腳本執(zhí)行環(huán)境等一系列信息,其中就包括命令行的輸入情況椅棺,這個信息犁罩,保存在process.argv這個屬性里。我們可以打印一下:

//index.js
console.log(process.argv);

打印結果:

image

可以看出两疚,argv是個數(shù)組床估,前兩位是固定的,分別是node程序的路徑和腳本存放的位置诱渤,從第三位開始才是額外輸入的內(nèi)容丐巫。那么實現(xiàn)上面的功能就很簡單了,只要讀取argv數(shù)組的第三位勺美,然后輸出出來就可以了鞋吉。

//index.js
console.log(`hello ${process.argv[2]||'world'}`)

npm社區(qū)中也有一些優(yōu)秀的命令行參數(shù)解析包,比如yargs,tj的commander.js等等

如果你想使用比較復雜的參數(shù)或者命令励烦,建議還是用第三方包比較好,手寫解析太耗精力了泼诱。

0x4 子進程

現(xiàn)在坛掠,你可以自由自在的寫你自己的cli腳本了。
如果你希望寫一個項目打完包自動推上git的cli,或者自動從git倉庫里面拉取項目啟動模板屉栓,那么舷蒲,你需要通過node的child_process模塊開啟子進程,在子進程內(nèi)調用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)命令對文件目錄進行操作纵柿,效率比fs高到不知道哪里去了。

社區(qū)上也有一些不錯的包启绰,比如阮一峰老師推薦的shelljs

0x5 美化輸出

如果你不那么希望你的cli用起來那么“硬核”昂儒,希望更人性化一點,比如提供一些友好的輸入委可、提示啊渊跋,給你的輸出加點顏色區(qū)分重點啊,寫個簡單的進度條啊等等着倾,那么你就需要美化一下你的輸出了拾酝。


image

除了顏色這部分,不使用第三方包實現(xiàn)起來非常繁瑣復雜卡者,其他的功能蒿囤,都可以試試自己寫。
顏色部分使用了第三方包colors虎眨,這里就不演示了蟋软。
其他都是由nodejs自帶的readline模塊實現(xiàn)的。

//index.js
const readline = require('readline');
const unloadChar='-';
const loadedChar='=';
const rl=readline.createInterface({
    input: process.stdin,
    output: process.stdout
});

rl.question('你想對誰說聲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}%】`)
}
  1. 首先岳守,通過readline.createInterface方法創(chuàng)建一個interface,這個類下面有一個方法.question碌冶,用這個方法在命令行上拋出一個問題湿痢,在第二個參數(shù)傳入一個函數(shù)進行監(jiān)聽。一旦用戶輸入完畢敲下回車扑庞,就會觸發(fā)回調函數(shù)譬重。
  2. 然后我們在回調函數(shù)里面寫了個計時器,假裝我們在處理某些事務罐氨。
  3. 使用readline.cursorTo這個方法臀规,可以改變命令行上的光標的位置。
    readline.cursorTo(process.stdout, 0, 0);是移動到第1列第1行上栅隐,
    readline.cursorTo(process.stdout, 0, 1);是移動到第1列第2行上塔嬉。
  4. 使用readline.clearScreenDown這個方法玩徊,是讓命令行從當前行開始,到最后一行結束谨究,將這兩行之間所有內(nèi)容清除恩袱。
  5. renderProgress是自己封裝的一個方法,通過process.stdout.write方法輸出一行看起來像是進度條的字符串到命令行上胶哲。
  6. 所以在計時器里面畔塔,當計數(shù)小于10的時候,我們讓光標移到第一行上鸯屿,然后清除所有輸出澈吨,輸出進度條字符串;當計數(shù)大于10的時候碾盟,我們關掉計時器棚辽,清除輸出,打印結果冰肴。
  7. 最后不要忘記關掉進程屈藐,可以使用interface這個類的.close方法關掉readline進程,也可以直接調用process.exit退出熙尉。

繪制的思路跟canvas繪制動畫一樣联逻,只不過canvas是清除畫布,而命令行這里是通過readline.clearScreenDown清除輸出检痰。

這樣包归,一個簡易的,人性化的铅歼,帶點點進度條動畫的命令行cli工具就寫好了公壤,你也可以發(fā)揮你的想象力,去寫一些更有趣的效果出來椎椰。

畢竟我們前端厦幅,有瀏覽器我們可以寫動畫,沒了瀏覽器我們一樣可以寫動畫慨飘。

0x6 參考

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末确憨,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子瓤的,更是在濱河造成了極大的恐慌休弃,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件圈膏,死亡現(xiàn)場離奇詭異塔猾,居然都是意外死亡,警方通過查閱死者的電腦和手機稽坤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門桥帆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來医增,“玉大人,你說我怎么就攤上這事老虫。” “怎么了茫多?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵祈匙,是天一觀的道長。 經(jīng)常有香客問我天揖,道長夺欲,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任今膊,我火速辦了婚禮些阅,結果婚禮上,老公的妹妹穿的比我還像新娘斑唬。我一直安慰自己市埋,他們只是感情好,可當我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布恕刘。 她就那樣靜靜地躺著缤谎,像睡著了一般。 火紅的嫁衣襯著肌膚如雪褐着。 梳的紋絲不亂的頭發(fā)上坷澡,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天,我揣著相機與錄音含蓉,去河邊找鬼频敛。 笑死,一個胖子當著我的面吹牛馅扣,可吹牛的內(nèi)容都是我干的斟赚。 我是一名探鬼主播,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼岂嗓,長吁一口氣:“原來是場噩夢啊……” “哼汁展!你這毒婦竟也來了?” 一聲冷哼從身側響起厌殉,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤食绿,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后公罕,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體器紧,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年楼眷,在試婚紗的時候發(fā)現(xiàn)自己被綠了铲汪。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片熊尉。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖掌腰,靈堂內(nèi)的尸體忽然破棺而出狰住,到底是詐尸還是另有隱情,我是刑警寧澤齿梁,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布催植,位于F島的核電站,受9級特大地震影響勺择,放射性物質發(fā)生泄漏创南。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一省核、第九天 我趴在偏房一處隱蔽的房頂上張望稿辙。 院中可真熱鬧,春花似錦气忠、人聲如沸邻储。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至舌菜,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間日月,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工爱咬, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留尺借,地道東北人。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓燎斩,卻偏偏與公主長得像蜂绎,于是被迫代替她去往敵國和親栅表。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容

  • Ubuntu的發(fā)音 Ubuntu,源于非洲祖魯人和科薩人的語言许布,發(fā)作 oo-boon-too 的音绎晃。了解發(fā)音是有意...
    螢火蟲de夢閱讀 99,260評論 9 467
  • 官網(wǎng) 中文版本 好的網(wǎng)站 Content-type: text/htmlBASH Section: User ...
    不排版閱讀 4,381評論 0 5
  • 近來有好一段時間停筆了,原因是年前剛跳槽箕昭,然后新公司剛入手解阅,沒有時間,實在太忙货抄,煩請各位讀者見諒。 上周的工作中积暖,...
    landy8530閱讀 2,976評論 0 2
  • My girl: 一件刻骨銘心的事怪与,在當時的翻折覆回后,仍然縈繞在內(nèi)心的深處分别。很久后,當這件小事被人不經(jīng)意的提起沼填,...
    逆風不解閱讀 840評論 10 10
  • 有位大德說我們修行不成功很大原因是怨親債主的原因括授。人生在世我們難免會和形形色色的有情打交道,無緣不相聚荚虚,這些有情很...
    輕言煦語閱讀 236評論 0 0