前端平常開發中,會碰見各類各樣的cli,好比一行命令幫你打包的webpack,一行命令幫你生成vue項目模板的vue-cli,還有建立react項目的create-react-app等等等等。這些工具極大地方便了咱們的平常工做,讓計算機本身去幹繁瑣的工做,而咱們,就能夠節省出大量的時間用於學習、交流、開發、逛steam。javascript
可是有時候一些十分特別的需求,咱們是找不到適合的cli工具去作的。好比說,你的項目十分龐大,你給項目添加一個新的路由,要通過建立目錄 -> 建立.vue文件 -> 更新vue-router的路由列表
這一趟流程,就算快捷鍵建立目錄文件用得再熟悉,也比不過你一行命令來得快,特別是路由目錄嵌套深,.vue文件初始化模板複雜的時候。html
因此呢,何不爲本身項目寫一個cli?就專門作這些繁瑣的活?前端
nodejs的cli,本質就是跑node腳本嘛,基本上每位前端er都會:vue
// index.js
console.log('hello world')
複製代碼
而後命令行調用java
> node index.js
## 輸出:
> hello world
複製代碼
能夠作得更逼真一點,咱們在package.json裏面的scripts字段上添加一下腳本名:node
{
"scripts":{
"hello":"node index.js"
}
}
複製代碼
而後命令行調用:react
> npm run hello
複製代碼
可是,看到這裏你確定會說,人家webpack還有vue-cli都是「有名字」的!什麼vue-cli init app
、webpack -p
的,多漂亮,看看這個命令行,node index.js
,還npm run hello
,誰不會啊,醜不拉幾的,怕又不是來水文章的哦?差評!!webpack
別急啊各位大人,接下來就說說,如何給這個node腳本起個名字。git
姑且,先把這個cli的名字命名爲hello-cli
,就是咱們可以在命令行裏面,輸入hello-cli
,而後它就打印一句hello world
,沒有node
也沒有npm
,就是: github
// index.js
#!/usr/bin/env node
console.log('hello world')
複製代碼
添加#!/usr/bin/env node
或者#!/usr/bin/node
,這是告訴系統,下面這個腳本,使用nodejs來執行。固然,這個系統不包括windows,由於windows下有個JScript的歷史遺留物在,會讓你的腳本跑不起來。#!/usr/bin/env node
的意思是讓系統本身去找node的執行程序。#!/usr/bin/node
的意思是,明確告訴系統,node的執行程序在路徑爲/usr/bin/node
。npm init
建立一個package.json,而後在package.json裏面,添加一個bin字段:{
"name": "hello-test",
"version": "1.0.0",
"bin":{
"hello-cli":"index.js"
}
}
複製代碼
bin字段裏面寫上這個命令行的名字,也就是hello-cli
,它告訴npm,裏面的js腳本能夠經過命令行的方式執行,以hello-cli
的命令調用。固然命令行的名字你想寫什麼都是你的自由,好比:
npm link
,將當前的代碼在npm全局目錄下留個快捷方式。hello-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
的方式進行安裝測試。名字有了,輸出也有了,看看咱們跟那些大名鼎鼎的cli工具,在形式上還差點啥?對了,人家能夠支持不一樣參數選項的,還能夠根據輸入的不一樣,產生不一樣的結果。
這樣吧,咱們給這個cli加一個功能,既然叫hello-cli
,那不能只會hello world
吧,必需要見誰就說hello
才行:
> hello-cli older
## 輸出
> hello older
複製代碼
雖然這個功能很簡單,可是至少也是實現了「根據輸入的不一樣,產生不一樣結果」的效果。
命令行上的參數,能夠經過process
這個變量獲取,process
是一個全局對象而不是一個包,不須要經過require
引入。經過process
這個對象咱們能夠拿到當前腳本執行環境等一系列信息,其中就包括命令行的輸入狀況,這個信息,保存在process.argv
這個屬性裏。咱們能夠打印一下:
//index.js
console.log(process.argv);
複製代碼
打印結果:
//index.js
console.log(`hello ${process.argv[2]||'world'}`)
複製代碼
npm社區中也有一些優秀的命令行參數解析包,好比yargs,tj的commander.js等等
若是你想使用比較複雜的參數或者命令,建議仍是用第三方包比較好,手寫解析太耗精力了。
如今,你能夠自由自在的寫你本身的cli腳本了。
若是你但願寫一個項目打完包自動推上git的cli,或者自動從git倉庫裏面拉取項目啓動模板,那麼,你須要經過node的child_process
模塊開啓子進程,在子進程內調用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命令,包括系統命令、其餘cli命令均可以在這裏執行。特別是系統命令,使用系統命令對文件目錄進行操做,效率比fs高到不知道哪裏去了。
社區上也有一些不錯的包,好比阮一峯老師推薦的shelljs
若是你不那麼但願你的cli用起來那麼「硬核」,但願更人性化一點,好比提供一些友好的輸入、提示啊,給你的輸出加點顏色區分重點啊,寫個簡單的進度條啊等等,那麼你就須要美化一下你的輸出了。
除了顏色這部分,不使用第三方包實現起來很是繁瑣複雜,其餘的功能,均可以試試本身寫。
顏色部分使用了第三方包colors,這裏就不演示了。
其餘都是由nodejs自帶的readline模塊實現的。
//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}%】`)
}
複製代碼
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
方法輸出一行看起來像是進度條的字符串到命令行上。interface
這個類的.close
方法關掉readline進程,也能夠直接調用process.exit
退出。繪製的思路跟canvas繪製動畫同樣,只不過canvas是清除畫布,而命令行這裏是經過readline.clearScreenDown
清除輸出。
這樣,一個簡易的,人性化的,帶點點進度條動畫的命令行cli工具就寫好了,你也能夠發揮你的想象力,去寫一些更有趣的效果出來。
畢竟咱們前端,有瀏覽器咱們能夠寫動畫,沒了瀏覽器咱們同樣能夠寫動畫。