延續前幾天的內容,今天要介紹本次專案使用的 Webpack 設定。雖然 Webpack 擁有功能強大、擴充性極高的優點,反過來也帶來不容易 debug、難以上手的缺點。他的功能強大的程度,其實足以直接取代 Gulp 的使用;但 Webpack 最重要的意義,作者認為是在它對 require
語法的擴充能力。
在 Webpack 的概念中,所有的 require()
呼叫(或是 import
語法,因為那會被 transpile 回 require
call)都會透過 loader 進行解析與載入。使用者可以根據不同的副檔名設定不同的 loader,讓任何類型的檔案都能被 require
到 JS 內。經過 loader 載入的資源,能以不同的形式存在於 JS 程式碼內,例如 CSS 變成 inline <style>
tag、圖片變成 base64 編碼資料等。
這樣的概念能夠帶來幾個開發上的意義改變:
require
去引入資源,能讓程式碼本身隱含著外部套件的 dependency 與 ordering想像早期的網頁開發,我們會在 HTML 放入許多諸如 <script src=“/jquery.js”></script>
的資源導入,然後再行撰寫我們自己的程式邏輯。這樣的特性讓 JS 程式碼的相依性難以被追蹤、也容易因搞錯順序而搞壞程式碼(例如將 bootstrap.js
的宣告放在 jquery
上方);相較之下,require
本身就能代表一套完整的 module 系統,讓程式碼去管理自身的 dependency issue。回到第一天提到的 Angular.js,以前的 JavaScript 套件通常會自行實作自己的 module 系統(參考 module design pattern),就是基於傳統的 <script>
引入特性而設計的。
除了 JavaScript code,CSS 更能受益於 Webpack 的特性。在 CSS Specificity 概念中,當碰到 specificity 數值相等的 selector 時,後者會覆寫(override)前者的 rule,因此撰寫 CSS 時引入套件的順序、撰寫的順序就相當重要。另外,Webpack 也能直接整合不同的 CSS preprocessor 或加入 PostCSS 轉換,能夠更方便的撰寫 CSS。
不過,反過來說 Webpack 的特性也讓程式碼使用 require
的行為相依於 Webpack loader,例如撰寫 React component 時有 require CSS 的話,這個 component 的檔案便不能於 server 執行(i.e. 無法用於 server-side rendering)。因此使用 Webpack 管理 CSS 等非 JS 資源時,建議對專案結構做適度調整,降低程式碼和 Webpack 之間的耦合度。最簡單的作法是隔離出 Webpack 專用的 require 非 JS 資源檔用的敘述,並將這些敘述放入 Webpack 的 entry file。
Webpack 包含了 loader、plugin、entry、output 等各種可以調整的設定,以下以本專案使用的設定做介紹。
Webpack 在 module resolving 採用 Node.js 的預設規則進行,而 resolve
可以用來覆寫這些規則:
resolve: {
root: projectRoot,
extensions: [
'',
'.js',
'.json',
'.jsx'
],
modulesDirectories: ['node_modules']
},
值得一提的是可以在 extensions
設定副檔名的辨識規則,例如放入 .jsx
就能在 require
時省略檔案的 .jsx
副檔名。同理,若專案的檔案命名規則是以句號串接(例如 MEAN 命名 Angular 檔案用的 app.client.controller.js 命名哥則),可以直接於 extensions
放入 .client.controller.js
以省略這些後綴命名。
在 modulesDirectories
可以指定 module 資料夾,因此若專案有使用 Bower,可以在這邊加入 .bowerrc
中定義的套件安裝路徑。
作為 Webpack 開始 compile 的進入點,entry
其實可以用 key-value 指定多組數值(此專案只有一個 app
entry),適合一個專案需要多個 JS bundle 的狀況(例如登入前/後各自的 HTML5 web app bundle)。傳入陣列的話,陣列內的每個 module 都會被載入,不過輸出的是陣列的最後一個項目。Webpack hot middleware 利用這個特性,可以在開發階段加入 HMR 的程式碼設定。
output
可以指定 bundle 後的檔案名稱、輸出位置等。publicPath
會暴露給 Webpack compile 結束後的輸出物件,供 build 程序後續使用。
Webpack 最重要的部份,由 regex test
指定不同類型的檔案要交由哪個 loader 做載入,並以額外傳入的 query
物件做 loader 設定。這個 query
物件實際上會以 query string 的形式傳入,所以也可以在程式碼內呼叫 require
的時候寫死在 module 命名內,例如 require(‘icon.png?mimetype=image/png’)
對應的 loader 設定可以寫成:
{
test: /\.png$/,
loader: 'url-loader',
query: {
mimetype: 'image/png'
}
}
不過由於寫死在 require
的方式容易造成管理不便以及加重對 Webpack 和特定 loader 的耦合度,建議斟酌使用。
Loader 的想法是將來源檔案載入後再輸出成 JS 程式碼,因此可以用 !
串接,只要格式相仿就能達到多次轉換的效果,例如 LESS 的轉換就能藉由 css!less
的 loader 設定,將 LESS 轉換成 CSS 再載入 JS 程式碼中。
JS 程式碼本身使用 babel-loader
並載入 Babel plugin 進行 ES6 程式碼轉換。
{
test: /\.js(x?)$/,
exclude: /node_modules/,
loader: 'babel',
query: {
presets: ['react', 'es2015', 'stage-0'],
plugins: [
'transform-object-rest-spread',
['transform-runtime', {
helpers: false,
polyfill: false,
regenerator: true
}]
]
}
},
由於大部分從 node_modules
引入的套件都是以 ES5 的形式安裝到本地環境,我們可以將 node_modules
來的 module 用 exclude
忽略掉,以加速 build 過程。
在 loader 運作結束後,Webpack 會套用 plugin 的功能在輸出的程式碼上。在此介紹一些作者常用的 plugin:
webpack.optimize.OccurrenceOrderPlugin
(內建):這個 plugin 主要是用來確保 bundle 的 module 排列順序會遵循被 require 的順序,除了能確保 CSS 的引入順序,也能維持 build hash 的一致性,增加 HMR 效能webpack.DefinePlugin
(內建):將 compile 出來的程式碼內的命名「抽換」掉,適合用來在 compile 時期注入 server 的環境變數webpack.BannerPlugin
(內建):在 bundle 出來的檔案頂端加上包在註解內的 bannerExtractTextPlugin
:用來將 require 到 JS 裡面的 CSS 抽取出來變成獨立的 .css
檔案,需要搭配 loader 使用webpack.optimize.DedupePlugin
(內建):進行靜態分析,把重複的程式碼刪減掉重複的部份(deduplication)webpack.optimize.UglifyJsPlugin
(內建):應該不會有人在 production 環境不用這個 plugin 吧?webpack.HotModuleReplacementPlugin
(內建):搭配 Webpack hot server 使用webpack.NoErrorsPlugin
(內建):減少 compile 時期的 error message 輸出,增加開發速度