在現代前端項目的交付工作流中,部署前最關鍵的環節就是構建,構建環節要完成的事情通常包括:
* 源代碼預編譯:比如 less、sass、typescript;
* 圖片優化、雪碧圖生成;
* JS、CSS 合并、壓縮;
* 靜態資源加版本號和引用替換;
* 靜態資源傳 CDN 等。
現在大多數同學所接觸的項目構建過程可能都是別人配置好的,但是對于構建過程中的某些考量可能并不是很清楚。
接下來,我們將組合 npm script 和簡單的命令行工具為實際項目添加構建過程,以加深對構建過程的理解,同時也會用到前面很多章節的知識點。
## 項目目錄結構
對之前的示例項目做簡單改造,讓目錄結構包括典型的前端項目資源引用情況:
```
client
├── images
│?? └── schedule.png
├── index.html
├── scripts
│?? └── main.js
└── styles
└── main.css
```
可能的資源依賴關系如下:
* css、html 文件中引用了圖片;
* html 文件中引用了 css、js;
顯而易見,我們的構建過程必須遵循下面的步驟才能不出錯:
1. 壓縮圖片;
2. 編譯 less、壓縮 css;
3. 編譯、壓縮 js;
4. 給圖片加版本號并替換 js、css 中的引用;
5. 給 js、css 加版本號并替換 html 中的引用;
## 添加構建過程
下面介紹如何結合 npm script 正確的給這樣的項目結構加上構建過程。
### 1\. 準備構建目錄
我們約定構建產生的結果代碼,放在 dist 目錄下,與 client 的結構完全相同,每次構建前,清空之前的構建目錄,利用 npm 的鉤子機制添加 prebuild 命令如下:
```
- "client:static-server": "http-server client/"
+ "client:static-server": "http-server client/",
+ "prebuild": "rm -rf dist && mkdir -p dist/{images,styles,scripts}",
```
### 2\. 準備腳本目錄
構建過程需要的命令稍長,我們可以使用 scripty 來把這些腳本剝離到單獨的文件中,為此需要準備單獨的目錄,并且我們的構建過程分為:images、styles、scripts、hash 四個步驟,每個步驟準備單獨的文件。
```
mkdir scripts/build
touch scripts/build.sh
touch scripts/build/{images,styles,scripts}.sh
chmod -R a+x scripts
```
**腳本文件的可執行權限必須添加正確,否則 scripty 會直接報錯**,上面命令執行完之后,scripts 目錄包含如下內容:
```
scripts
├── build
│?? ├── hash.sh
│?? ├── images.sh
│?? ├── scripts.sh
│?? └── styles.sh
├── build.sh
```
### 3\. 圖片構建過程
圖片構建的經典工具是 [imagemin](https://github.com/imagemin/imagemin),它也提供了命令行版本 [imagemin-cli](https://github.com/imagemin/imagemin-cli),首先安裝依賴:
```
npm i imagemin-cli -D
# npm install imagemin-cli --save-dev
# yarn add imagemin-cli -D
```
然后在 scripts/build/images.sh 中添加如下內容:
```
imagemin client/images/* --out-dir=dist/images
```
然后在 package.json 中添加 build:images 命令:
```
+ "build:images": "scripty",
```
嘗試運行 npm run prebuild && npm run build:images,然后觀察 dist 目錄的變化。
### 4\. 樣式構建過程
我們使用 [less](http://lesscss.org/usage/) 編寫樣式,所以需要預編譯樣式代碼,可以使用 less 官方庫自帶的命令行工具 lessc,使用 sass 的同學可以直接使用 [node-sass](https://github.com/sass/node-sass)。此外,樣式預編譯完成之后,我們需要使用 [cssmin](https://www.npmjs.com/package/cssmin) 來完成代碼預壓縮。首先安裝依賴:
```
npm i cssmin -D
# npm install cssmin --save-dev
# yarn add cssmin -D
```
然后在 scripts/build/styles.sh 中添加如下內容,這里我們使用到了 shell 里面的管道操作符 `|` 和輸出重定向 `>`:
```
for file in client/styles/*.css
do
lessc $file | cssmin > dist/styles/$(basename $file)
done
```
然后在 package.json 中添加 build:styles 命令:
```
+ "build:styles": "scripty",
```
嘗試運行 npm run prebuild && npm run build:styles,然后觀察 dist 目錄的變化,應該能看到 less 編譯之后再被壓縮的 css 代碼。
### 4\. JS 構建過程
我們使用 ES6 編寫 JS 代碼,所以需要 [uglify-es](https://github.com/mishoo/UglifyJS2/tree/harmony) 來進行代碼壓縮,如果你不使用 ES6,可以直接使用 [uglify-js](https://github.com/mishoo/UglifyJS2) 來壓縮代碼,首先安裝依賴:
```
npm i uglify-es -D
# npm install uglify-es --save-dev
# yarn add uglify-es -D
```
然后在 scripts/build/scripts.sh 中添加如下內容,**需要額外注意的是,這里我們需要手動指定 uglify-es 目錄下的 bin 文件,否則識別不了 ES6 語法**,因為 uglify-es 在 npm install 過程自動創建的軟鏈是錯誤的。
```
for file in client/scripts/*.js
do
./node_modules/uglify-es/bin/uglifyjs $file --mangle > dist/scripts/$(basename $file)
done
```
然后在 package.json 中添加 build:scripts 命令:
```
+ "build:scripts": "scripty",
```
嘗試運行 npm run prebuild && npm run build:scripts,然后觀察 dist 目錄的變化,應該能看到被 uglify-es 壓縮后的代碼。
> **TIP#19**:uglify-es 支持很多其他的選項,以及 sourcemap,對 JS 代碼做極致的優化,詳細[參考](https://github.com/mishoo/UglifyJS2/tree/harmony#command-line-options)
### 4\. 資源版本號和引用替換
給靜態資源加版本號的原因是線上環境的靜態資源通常都放在 CDN 上,或者設置了很長時間的緩存,或者兩者兼有,如果資源更新了但沒有更新版本號,瀏覽器端是拿不到最新內容的,手動加版本號的過程很繁瑣并且容易出錯,為此自動化這個過程就顯得非常有價值,通常的做法是利用文件內容做哈希,比如 md5,然后以這個哈希值作為版本號,版本號附著在文件名里面,線上環境的資源引用全部是帶版本號的。
為了實現這個過程,我們需要引入兩個小工具:
* [hashmark](https://github.com/keithamus/hashmark),自動添加版本號;
* [replaceinfiles](https://github.com/songkick/replaceinfiles),自動完成引用替換,它需要將版本號過程的輸出作為輸入;
首先安裝依賴:
```
npm i hashmark replaceinfiles -D
# npm install hashmark replaceinfiles --save-dev
# yarn add hashmark replaceinfiles -D
```
然后在 scripts/build/hash.sh 中添加如下內容:
```
# 給圖片資源加上版本號,并且替換引用
hashmark -c dist -r -l 8 '**/*.{png,jpg}' '{dir}/{name}.{hash}{ext}' | replaceinfiles -S -s 'dist/**/*.css' -d '{dir}/{base}'
# 給 js、css 資源加上版本號,并且替換引用
hashmark -c dist -r -l 8 '**/*.{css,js}' '{dir}/{name}.{hash}{ext}' | replaceinfiles -S -s 'client/index.html' -d 'dist/index.html'
```
然后在 package.json 中添加 build:hash 命令:
```
+ "build:hash": "scripty",
```
這個步驟需要依賴前幾個步驟,不能單獨運行,接下來我們需要增加完整的 build 命令把上面幾個步驟串起來。
### 5\. 完整的構建步驟
最后我們在 package.json 中添加 build 命令把所有的步驟串起來,完整的 diff 如下:
```
- "client:static-server": "http-server client/"
+ "client:static-server": "http-server client/",
+ "prebuild": "rm -rf dist && mkdir -p dist/{images,styles,scripts}",
+ "build": "scripty",
+ "build:images": "scripty",
+ "build:scripts": "scripty",
+ "build:styles": "scripty",
+ "build:hash": "scripty"
```
其中 scripts/build.sh 的內容如下:
```
for step in 'images' 'scripts' 'styles' 'hash'
do
npm run build:$step
done
```
然后我們嘗試運行 npm run build,完整的過程輸出如下:

構建完成的 dist 目錄內容如下:

可以看到,所有的靜態資源都加上了版本號。
構建完成的 dist/index.html 內容如下:

可以看到,靜態資源的版本號被正確替換了,為了驗證構建出來的頁面是否正常運行,可以運行 `./node_modules/.bin/http-server dist`,然后瀏覽器打開:`http://127.0.0.1:8080`,不出意外的話,瀏覽器顯示如下:

> **好了,到這里,我們給簡單但是五臟俱全的前端項目加上了構建過程,這些環節你是否都清楚?你覺得還缺失些什么環節?歡迎留言交流**
* * *
> 本節用到的代碼見 [GitHub](https://github.com/wangshijun/automated-workflow-with-npm-script/tree/12-use-npm-script-as-build-pipeline),想邊看邊動手練習的同學可以拉下來自己改,注意切換到正確的分支 `12-use-npm-script-as-build-pipeline`。
* * *
- 為什么選擇 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 實現服務自動化運維