首頁>技術>

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

前端日常開發中,會遇見各種各樣的cli,比如一行命令幫你打包的webpack,一行命令幫你生成vue專案模板的vue-cli,還有建立react專案的create-react-app等等等等。這些工具極大地方便了我們的日常工作,讓計算機自己去幹繁瑣的工作,而我們,就可以節省出大量的時間用於學習、交流、開發、逛steam

但是有時候一些十分特別的需求,我們是找不到適合的cli工具去做的。比如說,你的專案十分龐大,你給專案新增一個新的路由,要經過建立目錄 -> 建立.vue檔案 -> 更新vue-router的路由列表這一趟流程,就算快捷鍵建立目錄檔案用得再熟悉,也比不過你一行命令來得快,特別是路由目錄巢狀深,.vue檔案初始化模板複雜的時候。

所以呢,何不為自己專案寫一個cli?就專門做這些繁瑣的活?

0x1 hello world

nodejs的cli,本質就是跑node指令碼嘛,基本上每位前端er都會:

// index.jsconsole.log('hello world')複製程式碼

然後命令列呼叫

> node index.js## 輸出:> hello world複製程式碼

可以做得更逼真一點,我們在package.json裡面的scripts欄位上新增一下指令碼名:

{ "scripts":{ "hello":"node index.js" }}複製程式碼

然後命令列呼叫:

但是,看到這裡你肯定會說,人家webpack還有vue-cli都是“有名字”的!什麼vue-cli init app、webpack -p的,多漂亮,看看這個命令列,node index.js,還npm run hello,誰不會啊,醜不拉幾的,怕又不是來水文章的哦?差評!!

別急啊各位大人,接下來就說說,如何給這個node指令碼起個名字。

0x2 起名字

姑且,先把這個cli的名字命名為hello-cli,就是我們能夠在命令列裡面,輸入hello-cli,然後它就列印一句hello world,沒有node也沒有npm,就是:

在當前package.json目錄下,開啟命令列工具,執行npm link,將當前的程式碼在npm全域性目錄下留個快捷方式。npm檢測到package.json裡面存在一個bin欄位,它就同時在全域性npm包目錄下生成了一個可執行檔案:

當我們在系統命令列直接執行hello-cli的時候,實際上就是執行這裡的指令碼。因為安裝node的時候,npm將這個目錄配置為系統變數環境了,當你執行命令的時候,系統會先找系統命令和系統變數,然後到變數環境裡面去查詢這個命令名,然後找到這個目錄後,發現匹配上了該命令名的可執行檔案,接著就直接執行它。vue-cli也好,webpack-cli也好,都是這樣執行的。

這樣,你的第一個cli指令碼就成功安裝了,可以在命令列裡面,直接敲你的cli名字,看看結果輸出吧。

另外,如果你僅希望你的cli指令碼僅在專案裡執行,則需要在你專案裡面新建一個目錄,重複上述的操作,只是在第三步的時候,不要llink到全局裡面去,而是使用npm i -D file:<你的指令碼cli目錄路徑>,把它當成專案的依賴安裝到node_modules裡面去,如果安裝成功,那麼在專案的package.json你會看到多了一條依賴,這條依賴的值不是版本號,而是你指令碼的路徑。然後在node_modules裡面會有一個.bin目錄,裡面就存放著你的可執行檔案。

區域性安裝建議用npm i -D file:xxx,這樣它會在package.json留條記錄,方便其他小夥伴看到。自然,你的指令碼最好也是放進專案目錄裡面。

當然,這樣安裝的cli指令碼,必須在專案的package.json的scripts欄位上宣告指令碼命令,然後通過npm run的方式執行。

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

是的,但是有質的區別。使用node index.js這種方式呼叫的話固然簡單靈活,但是嚴重依賴指令碼路徑,一旦目錄結構發生變動,寫在scripts的命令就要更改一次;但是使用npm安裝之後,本地的cli指令碼就被拉到node_modules裡面,目錄結構變動對其影響不大。其次是不利於分享與釋出,如果你想把你的cli指令碼釋出出去,那麼有一個好聽響亮的名字,比起在說明文件裡面告訴使用者如何找到你的指令碼路徑再用node執行它,簡直好上那麼一萬倍不是麼?

這裡也給我們提供了一個cli開發流程思路:

初期開發可以通過node index.js來看效果。測試的時候可以通過npm link的方式進行安裝測試。釋出

0x3 引數讀取:process.argv

名字有了,輸出也有了,看看我們跟那些大名鼎鼎的cli工具,在形式上還差點啥?對了,人家可以支援不同引數選項的,還可以根據輸入的不同,產生不同的結果。

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

> hello-cli older## 輸出> hello older複製程式碼

雖然這個功能很簡單,但是至少也是實現了“根據輸入的不同,產生不同結果”的效果。

命令列上的引數,可以通過process這個變數獲取,process是一個全域性物件而不是一個包,不需要通過require引入。通過process這個物件我們可以拿到當前指令碼執行環境等一系列資訊,其中就包括命令列的輸入情況,這個資訊,儲存在process.argv這個屬性裡。我們可以列印一下:

//index.jsconsole.log(process.argv);複製程式碼

列印結果:

可以看出,argv是個陣列,前兩位是固定的,分別是node程式的路徑和指令碼存放的位置,從第三位開始才是額外輸入的內容。那麼實現上面的功能就很簡單了,只要讀取argv陣列的第三位,然後輸出出來就可以了。//index.jsconsole.log(`hello ${process.argv[2]||'world'}`)複製程式碼

npm社群中也有一些優秀的命令列引數解析包,比如yargs,tj的commander.js等等

如果你想使用比較複雜的引數或者命令,建議還是用第三方包比較好,手寫解析太耗精力了。

0x4 子程序

現在,你可以自由自在的寫你自己的cli指令碼了。

如果你希望寫一個專案打完包自動推上git的cli,或者自動從git倉庫裡面拉取專案啟動模板,那麼,你需要通過node的child_process模組開啟子程序,在子程序內呼叫git命令:

//test.jsconst 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命令,包括系統命令、其他cli命令都可以在這裡執行。特別是系統命令,使用系統命令對檔案目錄進行操作,效率比fs高到不知道哪裡去了。

社群上也有一些不錯的包,比如阮一峰老師推薦的shelljs

0x5 美化輸出

如果你不那麼希望你的cli用起來那麼“硬核”,希望更人性化一點,比如提供一些友好的輸入、提示啊,給你的輸出加點顏色區分重點啊,寫個簡單的進度條啊等等,那麼你就需要美化一下你的輸出了。

除了顏色這部分,不使用第三方包實現起來非常繁瑣複雜,其他的功能,都可以試試自己寫。

顏色部分使用了第三方包colors,這裡就不演示了。

其他都是由nodejs自帶的readline模組實現的。

//index.jsconst 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}%】`)}複製程式碼
首先,通過readline.createInterface方法建立一個interface類,這個類下面有一個方法.question,用這個方法在命令列上丟擲一個問題,在第二個引數傳入一個函式進行監聽。一旦使用者輸入完畢敲下回車,就會觸發回撥函式。然後我們在回撥函式裡面寫了個計時器,假裝我們在處理某些事務。使用readline.cursorTo這個方法,可以改變命令列上的游標的位置。readline.cursorTo(process.stdout, 0, 0);是移動到第1列第1行上,readline.cursorTo(process.stdout, 0, 1);是移動到第1列第2行上。使用readline.clearScreenDown這個方法,是讓命令列從當前行開始,到最後一行結束,將這兩行之間所有內容清除。renderProgress是自己封裝的一個方法,通過process.stdout.write方法輸出一行看起來像是進度條的字串到命令列上。所以在計時器裡面,當計數小於10的時候,我們讓游標移到第一行上,然後清除所有輸出,輸出進度條字串;當計數大於10的時候,我們關掉計時器,清除輸出,列印結果。最後不要忘記關掉程序,可以使用interface這個類的.close方法關掉readline程序,也可以直接呼叫process.exit退出。

繪製的思路跟canvas繪製動畫一樣,只不過canvas是清除畫布,而命令列這裡是通過readline.clearScreenDown清除輸出。

這樣,一個簡易的,人性化的,帶點點進度條動畫的命令列cli工具就寫好了,你也可以發揮你的想象力,去寫一些更有趣的效果出來。

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

0x6 參考

www.ruanyifeng.com/blog/2015/0…nodejs.cn/api/readlin…github.com/Marak/color…github.com/shelljs/she…

最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 教你三步爬取前端優質文章