前端日常開發中,會遇見各種各樣的cli,比如一行命令幫你打包的webpack,一行命令幫你生成vue項目模板的vue-cli,還有創建react項目的create-react-app等等等等。這些工具極大地方便了我們的日常工作,讓計算機自己去干繁瑣的工作,而我們,就可以節省出大量的時間用於學習、交流、開發、 逛steam 。
但是有時候一些十分特別的需求,我們是找不到適合的cli工具去做的。比如說,你的項目十分龐大,你給項目添加一個新的路由,要經過 創建目錄 -> 創建.vue文件 -> 更新vue-router的路由列表
這一趟流程,就算快捷鍵創建目錄文件用得再熟悉,也比不過你一行命令來得快,特別是路由目錄嵌套深,.vue文件初始化模板復雜的時候。
所以呢,何不為自己項目寫一個cli?就專門做這些繁瑣的活?
0x1 hello world
nodejs的cli,本質就是跑node腳本嘛,基本上每位前端er都會:
1
2
|
// index.js
console.log(
'hello world'
)
|
然后命令行調用
1
2
3
4
|
> node index.js
## 輸出:
> hello world
|
可以做得更逼真一點,我們在package.json里面的scripts字段上添加一下腳本名:
1
2
3
4
5
|
{
"scripts"
:{
"hello"
:
"node index.js"
}
}
|
然后命令行調用:
1
|
> npm run hello
|
但是,看到這里你肯定會說,人家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
,就是:
這里,我們需要做幾步操作:
1、index.js文件頂部聲明執行環境:
1
2
3
|
// 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
。
2、添加package.json的bin字段。
可以在index.js當前的目錄下執行 npm init
創建一個package.json,然后在package.json里面,添加一個bin字段:
1
2
3
4
5
6
7
|
{
"name"
:
"hello-test"
,
"version"
:
"1.0.0"
,
"bin"
:{
"hello-cli"
:
"index.js"
}
}
|
bin字段里面寫上這個命令行的名字,也就是 hello-cli
,它告訴npm,里面的js腳本可以通過命令行的方式執行,以 hello-cli
的命令調用。當然命令行的名字你想寫什么都是你的自由,比如:
3、 在當前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
才行:
1
2
3
|
> hello-cli older
## 輸出
> hello older
|
雖然這個功能很簡單,但是至少也是實現了“根據輸入的不同,產生不同結果”的效果。
命令行上的參數,可以通過 process
這個變量獲取, process
是一個全局對象而不是一個包,不需要通過 require
引入。通過 process
這個對象我們可以拿到當前腳本執行環境等一系列信息,其中就包括命令行的輸入情況,這個信息,保存在 process.argv
這個屬性里。我們可以打印一下:
1
2
|
//index.js
console.log(process.argv);
|
打印結果:
可以看出,argv是個數組,前兩位是固定的,分別是node程序的路徑和腳本存放的位置,從第三位開始才是額外輸入的內容。那么實現上面的功能就很簡單了,只要讀取argv數組的第三位,然后輸出出來就可以了。
1
2
|
//index.js
console.log(`hello ${process.argv[2]||
'world'
}`)
|
npm社區中也有一些優秀的命令行參數解析包,比如yargs ,tj的commander.js 等等
如果你想使用比較復雜的參數或者命令,建議還是用第三方包比較好,手寫解析太耗精力了。
0x4 子進程
現在,你可以自由自在的寫你自己的cli腳本了。
如果你希望寫一個項目打完包自動推上git的cli,或者自動從git倉庫里面拉取項目啟動模板,那么,你需要通過node的 child_process
模塊開啟子進程,在子進程內調用git命令:
1
2
3
4
5
6
7
8
|
//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
0x5 美化輸出
如果你不那么希望你的cli用起來那么“硬核”,希望更人性化一點,比如提供一些友好的輸入、提示啊,給你的輸出加點顏色區分重點啊,寫個簡單的進度條啊等等,那么你就需要美化一下你的輸出了。
除了顏色這部分,不使用第三方包實現起來非常繁瑣復雜,其他的功能,都可以試試自己寫。
顏色部分使用了第三方包 colors ,這里就不演示了。
其他都是由nodejs自帶的readline模塊實現的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
//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
方法輸出一行看起來像是進度條的字符串到命令行上。
所以在計時器里面,當計數小於10的時候,我們讓光標移到第一行上,然后清除所有輸出,輸出進度條字符串;當計數大於10的時候,我們關掉計時器,清除輸出,打印結果。
最后不要忘記關掉進程,可以使用 interface
這個類的 .close
方法關掉readline進程,也可以直接調用 process.exit
退出。
繪制的思路跟canvas繪制動畫一樣,只不過canvas是清除畫布,而命令行這里是通過 readline.clearScreenDown
清除輸出。
這樣,一個簡易的,人性化的,帶點點進度條動畫的命令行cli工具就寫好了,你也可以發揮你的想象力,去寫一些更有趣的效果出來。
畢竟我們前端,有瀏覽器我們可以寫動畫,沒了瀏覽器我們一樣可以寫動畫