前端項目通常會包括多個 npm script,對多個命令進行編排是很自然的需求,有時候需要將多個命令串行,即腳本遵循嚴格的執行順序;有時候則需要讓它們并行來提高速度,比如不相互阻塞的 npm script。社區中也有比 npm 內置的多命令運行機制更好用的解決方案:npm-run-all。
## 哪來那么多命令?
通常來說,前端項目會包含 js、css、less、scss、json、markdown 等格式的文件,為保障代碼質量,給不同的代碼添加檢查是很有必要的,代碼檢查不僅保障代碼沒有低級的語法錯誤,還可確保代碼都遵守社區的最佳實踐和一致的編碼風格,在團隊協作中尤其有用,即使是個人項目,加上代碼檢查,也會提高你的效率和質量。
我通常會給前端項目加上下面 4 種代碼檢查:
* [eslint](https://eslint.org),可定制的 js 代碼檢查,1.1 中有詳細的配置步驟;
* [stylelint](https://stylelint.io),可定制的樣式文件檢查,支持 css、less、scss;
* [jsonlint](https://github.com/zaach/jsonlint),json 文件語法檢查,踩過坑的同學會清楚,json 文件語法錯誤會知道導致各種失敗;
* [markdownlint-cli](https://github.com/igorshubovych/markdownlint-cli),Markdown 文件最佳實踐檢查,個人偏好;
需要注意的是,html 代碼也應該檢查,但是工具支持薄弱,就略過不表。此外,為代碼添加必要的單元測試也是質量保障的重要手段,常用的單測技術棧是:
* [mocha](https://mochajs.org),測試用例組織,測試用例運行和結果收集的框架;
* [chai](http://chaijs.com),測試斷言庫,必要的時候可以結合 [sinon](http://sinonjs.org) 使用;
> **TIP#4**:測試工具如 [tap](http://www.node-tap.org)、[ava](https://github.com/avajs/ava) 也都提供了命令行接口,能很好的集成到 npm script 中,原理是相通的。
包含了基本的代碼檢查、單元測試命令的 package.json 如下:
```
{
"name": "hello-npm-script",
"version": "0.1.0",
"main": "index.js",
"scripts": {
"lint:js": "eslint *.js",
"lint:css": "stylelint *.less",
"lint:json": "jsonlint --quiet *.json",
"lint:markdown": "markdownlint --config .markdownlint.json *.md",
"test": "mocha tests/"
},
"devDependencies": {
"chai": "^4.1.2",
"eslint": "^4.11.0",
"jsonlint": "^1.6.2",
"markdownlint-cli": "^0.5.0",
"mocha": "^4.0.1",
"stylelint": "^8.2.0",
"stylelint-config-standard": "^17.0.0"
}
}
```
## 讓多個 npm script 串行?
在我們運行測試之前確保我們的代碼都通過代碼檢查會是比較不錯的實踐,這也是讓多個 npm script 串行的典型用例,實現方式也比較簡單,只需要用 `&&` 符號把多條 npm script 按先后順序串起來即可,具體到我們的項目,修改如下圖所示:
```
diff --git a/package.json b/package.json
index c904250..023d71e 100644
--- a/package.json
+++ b/package.json
@@ -8,7 +8,7 @@
- "test": "mocha tests/"
+ "test": "npm run lint:js && npm run lint:css && npm run lint:json && npm run lint:markdown && mocha tests/"
},
```
然后直接執行 `npm test` 或 `npm t`,從輸出可以看到子命令的執行順序是嚴格按照我們在 scripts 中聲明的先后順序來的:
`eslint ==> stylelint ==> jsonlint ==> markdownlint ==> mocha`

需要注意的是,串行執行的時候如果前序命令失敗(通常進程退出碼非0),后續全部命令都會終止,我們可以嘗試在 index.js 中引入錯誤(刪掉行末的分號):
```
diff --git a/index.js b/index.js
index ab8bd0e..b817ea4 100644
--- a/index.js
+++ b/index.js
@@ -4,7 +4,7 @@ const add = (a, b) => {
}
return NaN;
-};
+}
module.exports = { add };
```
然后重新運行 npm t,結果如下,npm run lint:js 失敗之后,后續命令都沒有執行:

## 讓多個 npm script 并行?
在嚴格串行的情況下,我們必須要確保代碼中沒有編碼規范問題才能運行測試,在某些時候可能并不是我們想要的,因為我們真正需要的是,代碼變更時同時給出測試結果和測試運行結果。這就需要把子命令的運行從串行改成并行,實現方式更簡單,把連接多條命令的 `&&` 符號替換成 `&` 即可。
代碼變更如下:
```
diff --git a/package.json b/package.json
index 023d71e..2d9bd6f 100644
--- a/package.json
+++ b/package.json
@@ -8,7 +8,7 @@
- "test": "npm run lint:js && npm run lint:css && npm run lint:json && npm run lint:markdown && mocha tests/"
+ "test": "npm run lint:js & npm run lint:css & npm run lint:json & npm run lint:markdown & mocha tests/"
},
```
重新運行 npm t,我們得到如下結果:

細心的同學可能已經發現上圖中哪里不對,npm run lint:js 的結果在進程退出之后才輸出,如果你自己運行,不一定能穩定復現這個問題,但 npm 內置支持的多條命令并行跟 js 里面同時發起多個異步請求非常類似,它只負責觸發多條命令,而不管結果的收集,如果并行的命令執行時間差異非常大,上面的問題就會穩定復現。怎么解決這個問題呢?
答案也很簡單,在命令的增加 `& wait` 即可,這樣我們的 test 命令長這樣:
```
npm run lint:js & npm run lint:css & npm run lint:json & npm run lint:markdown & mocha tests/ & wait
```
加上 wait 的額外好處是,如果我們在任何子命令中啟動了長時間運行的進程,比如啟用了 mocha 的 `--watch` 配置,可以使用 `ctrl + c` 來結束進程,如果沒加的話,你就沒辦法直接結束啟動到后臺的進程。
## 有沒有更好的管理方式?
有強迫癥的同學可能會覺得像上面這樣用原生方式來運行多條命令很臃腫,幸運的是,我們可以使用 `npm-run-all` 實現更輕量和簡潔的多命令運行。
用如下命令將 `npm-run-all` 添加到項目依賴中:
```
npm i npm-run-all -D
```
然后修改 package.json 實現多命令的串行執行:
```
diff --git a/package.json b/package.json
index b3b1272..83974d6 100644
--- a/package.json
+++ b/package.json
@@ -8,7 +8,8 @@
- "test": "npm run lint:js & npm run lint:css & npm run lint:json & npm run lint:markdown & mocha tests/ & wait"
+ "mocha": "mocha tests/",
+ "test": "npm-run-all lint:js lint:css lint:json lint:markdown mocha"
},
```
npm-run-all 還支持通配符匹配分組的 npm script,上面的腳本可以進一步簡化成:
```
diff --git a/package.json b/package.json
index 83974d6..7b327cd 100644
--- a/package.json
+++ b/package.json
@@ -9,7 +9,7 @@
- "test": "npm-run-all lint:js lint:css lint:json lint:markdown mocha"
+ "test": "npm-run-all lint:* mocha"
},
```
如何讓多個 npm script 并行執行?也很簡單:
```
diff --git a/package.json b/package.json
index 7b327cd..c32da1c 100644
--- a/package.json
+++ b/package.json
@@ -9,7 +9,7 @@
- "test": "npm-run-all lint:* mocha"
+ "test": "npm-run-all --parallel lint:* mocha"
},
```
并行執行的時候,我們并不需要在后面增加 `& wait`,因為 npm-run-all 已經幫我們做了。
> **TIP#5**:npm-run-all 還提供了很多配置項支持更復雜的命令編排,比如多個命令并行之后接串行的命令,感興趣的同學請閱讀[文檔](https://github.com/mysticatea/npm-run-all/blob/HEAD/docs/npm-run-all.md),自己玩兒。
* * *
> 本節用到的代碼見 [GitHub](https://github.com/wangshijun/automated-workflow-with-npm-script/tree/02-run-multiple-npm-scripts),想邊看邊動手練習的同學可以拉下來自己改,注意切換到正確的分支 `02-run-multiple-npm-scripts`。**運行命令前別忘了安裝 node\_modules,??**
* * *
- 為什么選擇 npm script
- 入門篇 01:創建并運行 npm script 命令
- 入門篇 02:運行多個 npm script 的各種姿勢
- 入門篇 03:給 npm script 傳遞參數和添加注釋
- 進階篇 01:使用 npm script 的鉤子
- 進階篇 02:在 npm script 中使用環境變量
- 進階篇 03:實現 npm script 命令自動補全
- 高階篇 01:實現 npm script 跨平臺兼容
- 高階篇 02:把龐大的 npm script 拆到單獨文件中
- 高階篇 03:用 node.js 腳本替代復雜的 npm script
- 實戰篇 01:監聽文件變化并自動運行 npm script
- 實戰篇 02:結合 live-reload 實現自動刷新
- 實戰篇 03:在 git hooks 中運行 npm script
- 實戰篇 04:用 npm script 實現構建流水線
- 實戰篇 05:用 npm script 實現服務自動化運維