原文首發(fā)于:Webpack 3,從入門到放棄
Update (2017.8.27) : 關(guān)于
output.publicPath、devServer.contentBase、devServer.publicPath的區(qū)別。如下:
- output.publicPath: 對(duì)于這個(gè)選項(xiàng),我們無(wú)需關(guān)注什么絕對(duì)相對(duì)路徑,因?yàn)閮煞N路徑都可以。我們只需要知道一點(diǎn):這個(gè)選項(xiàng)是指定 HTML 文件中資源文件 (字體、圖片、JS文件等) 的
文件名的公共 URL 部分的。在實(shí)際情況中,我們首先會(huì)通過(guò)output.filename或有些 loader 如file-loader的name屬性設(shè)置文件名的原始部分,webpack 將文件名的原始部分和公共部分結(jié)合之后,HTML 文件就能獲取到資源文件了。- devServer.contentBase: 設(shè)置靜態(tài)資源的根目錄,
html-webpack-plugin生成的 html 不是靜態(tài)資源。當(dāng)用 html 文件里的地址無(wú)法找到靜態(tài)資源文件時(shí)就會(huì)去這個(gè)目錄下去找。- devServer.publicPath: 指定瀏覽器上訪問(wèn)所有 打包(bundled)文件 (在
dist里生成的所有文件) 的根目錄,這個(gè)根目錄是相對(duì)服務(wù)器地址及端口的,比devServer.contentBase和output.publicPath優(yōu)先。
前言
Tips
如果你用過(guò) webpack 且一直用的是 webpack 1,請(qǐng)參考 從v1遷移到v2 (v2 和 v3 差異不大) 對(duì)版本變更的內(nèi)容進(jìn)行適當(dāng)?shù)牧私?,然后再選擇性地閱讀本文。
首先,這篇文章是根據(jù)當(dāng)前最新的 webpack 版本 (即 v3.4.1) 撰寫,較長(zhǎng)一段時(shí)間內(nèi)無(wú)需擔(dān)心過(guò)時(shí)的問(wèn)題。其次,這應(yīng)該會(huì)是一篇極長(zhǎng)的文章,涵蓋了基本的使用方法,有更高級(jí)功能的需求可以參考官方文檔繼續(xù)學(xué)習(xí)。再次,即使是基本的功能,也內(nèi)容繁多,我盡可能地解釋通俗易懂,將我學(xué)習(xí)過(guò)程中的疑惑和坑一一解釋,如有紕漏,敬請(qǐng)雅正。再次,為了清晰有效地講解,我會(huì)演示從零編寫 demo,只要一步步跟著做,就會(huì)清晰許多。最后,官方文檔也是個(gè)坑爹貨!
Webpack,何許人也?
借用官方的說(shuō)法:
webpack is a module bundler. Its main purpose is to bundle JavaScript files for usage in a browser, yet it is also capable of transforming, bundling, or packaging just about any resource or asset.
簡(jiǎn)言之,webpack 是一個(gè)模塊打包器 (module bundler),能夠?qū)⑷魏钨Y源如 JavaScript 文件、CSS 文件、圖片等打包成一個(gè)或少數(shù)文件。
為什么要用介個(gè) Webpack?
首先,定義已經(jīng)說(shuō)明了 webpack 能將多個(gè)資源模塊打包成一個(gè)或少數(shù)文件,這意味著與以往的發(fā)起多個(gè) HTTP 請(qǐng)求來(lái)獲得資源相比,現(xiàn)在只需要發(fā)起少量的 HTTP 請(qǐng)求。
Tips
想了解合并 HTTP 請(qǐng)求的意義,請(qǐng)見 這里。
其次,webpack 能將你的資源轉(zhuǎn)換為最適合瀏覽器的“格式”,提升應(yīng)用性能。比如只引用被應(yīng)用使用的資源 (剔除未被使用的代碼),懶加載資源 (只在需要的時(shí)候才加載相應(yīng)的資源)。再次,對(duì)于開發(fā)階段,webpack 也提供了實(shí)時(shí)加載和熱加載的功能,大大地節(jié)省了開發(fā)時(shí)間。除此之外,還有許多優(yōu)秀之處之處值得去挖掘。不過(guò),webpack 最核心的還是打包的功能。
webpack,gulp/grunt,npm,它們有什么區(qū)別?
webpack 是模塊打包器(module bundler),把所有的模塊打包成一個(gè)或少量文件,使你只需加載少量文件即可運(yùn)行整個(gè)應(yīng)用,而無(wú)需像之前那樣加載大量的圖片,css文件,js文件,字體文件等等。而gulp/grunt 是自動(dòng)化構(gòu)建工具,或者叫任務(wù)運(yùn)行器(task runner),是把你所有重復(fù)的手動(dòng)操作讓代碼來(lái)做,例如壓縮JS代碼、CSS代碼,代碼檢查、代碼編譯等等,自動(dòng)化構(gòu)建工具并不能把所有模塊打包到一起,也不能構(gòu)建不同模塊之間的依賴圖。兩者來(lái)比較的話,gulp/grunt 無(wú)法做模塊打包的事,webpack 雖然有 loader 和 plugin可以做一部分 gulp/grunt 能做的事,但是終究 webpack 的插件還是不如 gulp/grunt 的插件豐富,能做的事比較有限。于是有人兩者結(jié)合著用,將 webpack 放到 gulp/grunt 中用。然而,更好的方法是用 npm scripts 取代 gulp/grunt,npm 是 node 的包管理器 (node package manager),用于管理 node 的第三方軟件包,npm 對(duì)于任務(wù)命令的良好支持讓你最終省卻了編寫任務(wù)代碼的必要,取而代之的,是老祖宗的幾個(gè)命令行,僅靠幾句命令行就足以完成你的模塊打包和自動(dòng)化構(gòu)建的所有需求。
準(zhǔn)備開始
先來(lái)看看一個(gè) webpack 的一個(gè)完備的配置文件,是 介樣 的,當(dāng)然啦,這里面有很多配置項(xiàng)是即使到這個(gè)軟件被廢棄你也用不上的:),所以無(wú)需擔(dān)心。
基本配置
開始之前,請(qǐng)確定你已經(jīng)安裝了當(dāng)前 Node 的較新版本。
然后執(zhí)行以下命令以新建我們的 demo 目錄:
$ mkdir webpack-demo && cd webpack-demo && npm init -y
$ npm i --save-dev webpack
$ mkdir src && cd src && touch index.js
我們使用工具函數(shù)庫(kù) lodash 來(lái)演示我們的 demo。先安裝之:
$ npm i --save lodash
src/index.js
import _ from 'lodash';
function component() {
const element = document.createElement('div');
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
return element;
}
document.body.appendChild(component());
Tips
import和export已經(jīng)是 ES6 的標(biāo)準(zhǔn),但是仍未得到大多數(shù)瀏覽器的支持 (可喜的是, Chrome 61 已經(jīng)開始默認(rèn)支持了,見 ES6 modules),不過(guò) webpack 提供了對(duì)這個(gè)特性的支持,但是除了這個(gè)特性,其他的 ES6 特性并不會(huì)得到 webpack 的特別支持,如有需要,須借助 Babel 進(jìn)行轉(zhuǎn)譯 (transpile)。
然后新建發(fā)布版本目錄:
$ cd .. && mkdir dist && cd dist && touch index.html
dist/index.html
<!DOCTYPE html>
<html>
<head>
<title>webpack demo</title>
</head>
<body>
<script src="bundle.js"></script>
</body>
</html>
現(xiàn)在,我們運(yùn)行 webpack 來(lái)打包 index.js 為 bundle.js,本地安裝了 webpack 后可以通過(guò) node_modules/.bin/webpack 來(lái)訪問(wèn) webpack 的二進(jìn)制版本。
$ cd ..
$ ./node_modules/.bin/webpack src/index.js dist/bundle.js # 第一個(gè)參數(shù)是打包的入口文件,第二個(gè)參數(shù)是打包的出口文件
咻咻咻,大致如下輸出一波:
Hash: de8ed072e2c7b3892179
Version: webpack 3.4.1
Time: 390ms
Asset Size Chunks Chunk Names
bundle.js 544 kB 0 [emitted] [big] main
[0] ./src/index.js 225 bytes {0} [built]
[2] (webpack)/buildin/global.js 509 bytes {0} [built]
[3] (webpack)/buildin/module.js 517 bytes {0} [built]
+ 1 hidden module
現(xiàn)在,你已經(jīng)得到了你的第一個(gè)打包文件 (bundle.js) 了。
使用配置文件
像上面這樣使用 webpack 應(yīng)該是最挫的姿勢(shì)了,所以我們要使用 webpack 的配置文件來(lái)提高我們的姿勢(shì)水平。
$ touch webpack.config.js
webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js', // 入口起點(diǎn),可以指定多個(gè)入口起點(diǎn)
output: { // 輸出,只可指定一個(gè)輸出配置
filename: 'bundle.js', // 輸出文件名
path: path.resolve(__dirname, 'dist') // 輸出文件所在的目錄
}
};
執(zhí)行:
$ ./node_modules/.bin/webpack --config webpack.config.js # `--config` 制定 webpack 的配置文件,默認(rèn)是 `webpack.config.js`
所以這里可以省卻 --config webpack.config.js。但是每次都要寫 ./node_modules/.bin/webpack 實(shí)在讓人不爽,所以我們要?jiǎng)佑?NPM Scripts。
package.json
{
...
"scripts": {
"build": "webpack"
},
...
}
Tips
在npm scripts中我們可以通過(guò)包名直接引用本地安裝的 npm 包的二進(jìn)制版本,而無(wú)需編寫包的整個(gè)路徑。
執(zhí)行:
$ npm run build
一波輸出后便得到了打包文件。
Tips
bulid并不是npm scripts的內(nèi)置屬性,需要使用npm run來(lái)執(zhí)行腳本,詳情見 npm run。
打包其他類型的文件
因?yàn)槠渌募?JS 文件類型不同,要把他們加載到 JS 文件中就需要經(jīng)過(guò)加載器 (loader) 的處理。
加載 CSS
我們需要安裝兩個(gè) loader 來(lái)處理 CSS 文件:
$ npm i --save-dev style-loader css-loader
style-loader 通過(guò)插入 <style> 標(biāo)簽將 CSS 加入到 DOM 中,css-loader 會(huì)像解釋 import/require() 一樣解釋 @import 和 url()。
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: { // 如何處理項(xiàng)目中不同類型的模塊
rules: [ // 用于規(guī)定在不同模塊被創(chuàng)建時(shí)如何處理模塊的規(guī)則數(shù)組
{
test: /\.css$/, // 匹配特定文件的正則表達(dá)式或正則表達(dá)式數(shù)組
use: [ // 應(yīng)用于模塊的 loader 使用列表
'style-loader',
'css-loader'
]
}
]
}
};
我們來(lái)創(chuàng)建一個(gè) CSS 文件:
$ cd src && touch style.css
src/style.css
.hello {
color: red;
}
src/index.js
import _ from 'lodash';
import './style.css'; // 通過(guò)`import`引入 CSS 文件
function component() {
const element = document.createElement('div');
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
element.classList.add('hello'); // 在相應(yīng)元素上添加類名
return element;
}
document.body.appendChild(component());
執(zhí)行npm run build,然后打開index.html,就可以看到紅色的字體了。CSS 文件此時(shí)已經(jīng)被打包到 bundle.js 中。再打開瀏覽器控制臺(tái),就可以看到 webpack 做了些什么。
加載圖片
$ npm install --save-dev file-loader
file-loader 指示 webpack 以文件格式發(fā)出所需對(duì)象并返回文件的公共URL,可用于任何文件的加載。
webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
},
{ // 增加加載圖片的規(guī)則
test: /\.(png|svg|jpg|gif)$/,
use: [
'file-loader'
]
}
]
}
};
我們?cè)诋?dāng)前項(xiàng)目的目錄中如下增加圖片:
webpack-demo
|- package.json
|- webpack.config.js
|- /dist
|- bundle.js
|- index.html
|- /src
+ |- icon.jpg
|- style.css
|- index.js
|- /node_modules
src/index.js
import _ from 'lodash';
import './style.css';
import Icon from './icon.jpg'; // Icon 是圖片的 URL
function component() {
const element = document.createElement('div');
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
element.classList.add('hello');
const myIcon = new Image();
myIcon.src = Icon;
element.appendChild(myIcon);
return element;
}
document.body.appendChild(component());
src/style.css
.hello {
color: red;
background: url(./icon.jpg);
}
再npm run build之?,F(xiàn)在你可以看到單獨(dú)的圖片和以圖片為基礎(chǔ)的背景圖了。
加載字體
加載字體用的也是 file-loader。
webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /\.(png|svg|jpg|gif)$/,
use: [
'file-loader'
]
},
{ // 增加加載字體的規(guī)則
test: /\.(woff|woff2|eot|ttf|otf)$/,
use: [
'file-loader'
]
}
]
}
};
在當(dāng)前項(xiàng)目的目錄中如下增加字體:
webpack-demo
|- package.json
|- webpack.config.js
|- /dist
|- bundle.js
|- index.html
|- /src
+ |- my-font.ttf
|- icon.jpg
|- style.css
|- index.js
|- /node_modules
src/style.css
@font-face {
font-family: MyFont;
src: url(./my-font.ttf);
}
.hello {
color: red;
background: url(./icon.jpg);
font-family: MyFont;
}
運(yùn)行打包命令之后便可以看到打包好的文件和發(fā)生改變的頁(yè)面。
加載 JSON 文件
因?yàn)?webpack 對(duì) JSON 文件的支持是內(nèi)置的,所以可以直接添加。
src/data.json
{
"name": "webpack-demo",
"version": "1.0.0",
"author": "Sam Yang"
}
src/index.js
import _ from 'lodash';
import './style.css';
import Icon from './icon.jpg';
import Data from './data.json'; // Data 變量包含可直接使用的 JSON 解析得到的對(duì)象
function component() {
const element = document.createElement('div');
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
element.classList.add('hello');
const myIcon = new Image();
myIcon.src = Icon;
element.appendChild(myIcon);
console.log(Data);
return element;
}
document.body.appendChild(component());
關(guān)于其他文件的加載,可以尋求相應(yīng)的 loader。
輸出管理
前面我們只有一個(gè)輸入文件,但現(xiàn)實(shí)是我們往往有不止一個(gè)輸入文件,這時(shí)我們就需要輸入多個(gè)入口文件并管理輸出文件。我們?cè)?src 目錄下增加一個(gè) print.js 文件。
src/print.js
export default function printMe() {
console.log('I get called from print.js!');
}
src/index.js
import _ from 'lodash';
import printMe from './print.js';
// import './style.css';
// import Icon from './icon.jpg';
// import Data from './data.json';
function component() {
const element = document.createElement('div');
const btn = document.createElement('button');
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
// element.classList.add('hello');
// const myIcon = new Image();
// myIcon.src = Icon;
// element.appendChild(myIcon);
// console.log(Data);
btn.innerHTML = 'Click me and check the console!';
btn.onclick = printMe;
element.appendChild(btn);
return element;
}
document.body.appendChild(component());
dist/index.html
<!DOCTYPE html>
<html>
<head>
<title>webpack demo</title>
<script src="./print.bundle.js"></script>
</head>
<body>
<!-- <script src="bundle.js"></script> -->
<script src="./app.bundle.js"></script>
</body>
</html>
webpack.config.js
const path = require('path');
module.exports = {
// entry: './src/index.js',
entry: {
app: './src/index.js',
print: './src/print.js'
},
output: {
// filename: 'bundle.js',
filename: '[name].bundle.js', // 根據(jù)入口起點(diǎn)名動(dòng)態(tài)生成 bundle 名,可以使用像 "js/[name]/bundle.js" 這樣的文件夾結(jié)構(gòu)
path: path.resolve(__dirname, 'dist')
},
// ...
};
Tips
filename: '[name].bundle.js'中的[name]會(huì)替換為對(duì)應(yīng)的入口起點(diǎn)名,其他可用的替換請(qǐng)參見 output.filename。
現(xiàn)在可以打包文件了。但是如果我們修改了入口文件名或增加了入口文件,index.html是不會(huì)自動(dòng)引用新文件的,而手動(dòng)修改實(shí)在太挫。是時(shí)候使用插件 (plugin) 來(lái)完成這一任務(wù)了。我們使用 HtmlWebpackPlugin 自動(dòng)生成 html 文件。
loader 和 plugin,有什么區(qū)別?
loader (加載器),重在“加載”二字,是用于預(yù)處理文件的,只用于在加載不同類型的文件時(shí)對(duì)不同類型的文件做相應(yīng)的處理。而 plugin (插件),顧名思義,是用來(lái)增加 webpack 的功能的,作用于整個(gè) webpack 的構(gòu)建過(guò)程。在 webpack 這個(gè)大公司中,loader 是保安大叔,負(fù)責(zé)對(duì)進(jìn)入公司的不同人員的處理,而 plugin 則是公司里不同職位的職員,負(fù)責(zé)公司里的各種不同業(yè)務(wù),每增加一種新型的業(yè)務(wù)需求,我們就需要增加一種 plugin。
安裝插件:
$ npm i --save-dev html-webpack-plugin
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
// entry: './src/index.js',
entry: {
app: './src/index.js',
print: './src/print.js'
},
output: {
// filename: 'bundle.js',
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
},
plugins: [ // 插件屬性,是插件的實(shí)例數(shù)組
new HtmlWebpackPlugin({
title: 'webpack demo', // 生成 HTML 文檔的標(biāo)題
filename: 'index.html' // 寫入 HTML 文件的文件名,默認(rèn) `index.html`
})
],
// ...
};
你可以先把 dist 文件夾的index.html文件刪除,然后執(zhí)行打包命令。咻咻咻,我們看到 dist 目錄下已經(jīng)自動(dòng)生成了一個(gè)index.html文件,但即使不刪除原先的index.html,該插件默認(rèn)生成的index.html也會(huì)替換原本的index.html。
此刻,當(dāng)你細(xì)細(xì)觀察 dist 目錄時(shí),雖然現(xiàn)在生成了新的打包文件,但原本的打包文件bundle.js及其他不用的文件仍然存在在 dist 目錄中,所以在每次構(gòu)建前我們需要晴空 dist 目錄,我們使用 CleanWebpackPlugin 插件。
$ npm i clean-webpack-plugin --save-dev
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
module.exports = {
// entry: './src/index.js',
entry: {
app: './src/index.js',
print: './src/print.js'
},
output: {
// filename: 'bundle.js',
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
},
plugins: [
new HtmlWebpackPlugin({
title: 'webpack demo',
filename: 'index.html'
}),
new CleanWebpackPlugin(['dist']) // 第一個(gè)參數(shù)是要清理的目錄的字符串?dāng)?shù)組
],
// ...
};
打包之,現(xiàn)在,dist 中只存在打包生成的文件。
開發(fā)環(huán)境
webpack 提供了很多便于開發(fā)時(shí)使用的功能,來(lái)一一看看吧。
使用代碼映射 (source map)
當(dāng)你的代碼被打包后,如果打包后的代碼發(fā)生了錯(cuò)誤,你很難追蹤到錯(cuò)誤發(fā)生的原始位置,這個(gè)時(shí)候,我們就需要代碼映射 (source map) 這種工具,它能將編譯后的代碼映射回原始的源碼,你的錯(cuò)誤是起源于打包前的b.js的某個(gè)位置,代碼映射就能告訴你錯(cuò)誤是那個(gè)模塊的那個(gè)位置。webpack 默認(rèn)提供了 10 種風(fēng)格的代碼映射,使用它們會(huì)明顯影響到構(gòu)建 (build) 和重構(gòu)建 (rebuild,每次修改后需要重新構(gòu)建) 的速度,十種風(fēng)格的差異可以參看 devtool。關(guān)于如何選擇映射風(fēng)格可以參看 Webpack devtool source map。這里,我們?yōu)榱藴?zhǔn)確顯示錯(cuò)誤位置,選擇速度較慢的inline-source-map。
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
module.exports = {
devtool: 'inline-source-map', // 控制是否生成以及如何生成 source map
// entry: './src/index.js',
entry: {
app: './src/index.js',
print: './src/print.js'
},
// ...
};
現(xiàn)在來(lái)手動(dòng)制造一些錯(cuò)誤:
src/print.js
export default function printMe() {
- console.log('I get called from print.js!');
+ cosnole.log('I get called from print.js!');
}
打包之后打開index.html再點(diǎn)擊按鈕,你就會(huì)看到控制臺(tái)顯示如下報(bào)錯(cuò):
Uncaught ReferenceError: cosnole is not defined
at HTMLButtonElement.printMe (print.js:2)
現(xiàn)在,我們很清楚哪里發(fā)生了錯(cuò)誤,然后輕松地改正之。
使用 webpack-dev-server
你一定有這樣的體驗(yàn),開發(fā)時(shí)每次修改代碼保存后都需要重新手動(dòng)構(gòu)建代碼并手動(dòng)刷新瀏覽器以觀察修改效果,這是很麻煩的,所以,我們要實(shí)時(shí)加載代碼。可喜的是,webpack 提供了對(duì)實(shí)時(shí)加載代碼的支持。我們需要安裝 webpack-dev-server 以獲得支持。
$ npm i --save-dev webpack-dev-server
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
module.exports = {
devtool: 'inline-source-map',
devServer: { // 檢測(cè)代碼變化并自動(dòng)重新編譯并自動(dòng)刷新瀏覽器
contentBase: path.resolve(__dirname, 'dist') // 設(shè)置靜態(tài)資源的根目錄
},
// entry: './src/index.js',
entry: {
app: './src/index.js',
print: './src/print.js'
},
// ...
};
package.json
{
...
"scripts": {
"build": "webpack",
"start": "webpack-dev-server --open"
},
...
}
Tips
使用 webpack-dev-server 時(shí),webpack 并沒(méi)有將所有生成的文件寫入磁盤,而是放在內(nèi)存中,提供更快的內(nèi)存內(nèi)訪問(wèn),便于實(shí)時(shí)更新。
現(xiàn)在,可以直接運(yùn)行npm start (start是 npm scripts 的內(nèi)置屬性,可直接運(yùn)行),然后瀏覽器自動(dòng)加載應(yīng)用的頁(yè)面,默認(rèn)在localhost:8080顯示。
模塊熱替換 (HMR, Hot Module Replacement)
webpack 提供了對(duì)模塊熱替換 (或者叫熱加載) 的支持。這一特性能夠讓應(yīng)用運(yùn)行的時(shí)候替換、增加或刪除模塊,而無(wú)需進(jìn)行完全的重載。想進(jìn)一步地了解其工作機(jī)理,可以參見 Hot Module Replacement,但這并不是必需的,你可以選擇跳過(guò)機(jī)理部分繼續(xù)往下閱讀。
Tips
模塊熱替換(HMR)只更新發(fā)生變更(替換、添加、刪除)的模塊,而無(wú)需重新加載整個(gè)頁(yè)面(實(shí)時(shí)加載,LiveReload),這樣可以顯著加快開發(fā)速度,一旦打開了 webpack-dev-server 的 hot 模式,在試圖重新加載整個(gè)頁(yè)面之前,熱模式會(huì)嘗試使用 HMR 來(lái)更新。
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const webpack = require('webpack'); // 引入 webpack 便于調(diào)用其內(nèi)置插件
module.exports = {
devtool: 'inline-source-map',
devServer: {
contentBase: path.resolve(__dirname, 'dist'),
hot: true, // 告訴 dev-server 我們?cè)谟?HMR
hotOnly: true // 指定如果熱加載失敗了禁止刷新頁(yè)面 (這是 webpack 的默認(rèn)行為),這樣便于我們知道失敗是因?yàn)楹畏N錯(cuò)誤
},
// entry: './src/index.js',
entry: {
app: './src/index.js',
// print: './src/print.js'
},
// ...
plugins: [
new HtmlWebpackPlugin({
title: 'webpack demo',
filename: 'index.html'
}),
new CleanWebpackPlugin(['dist']),
new webpack.HotModuleReplacementPlugin(), // 啟用 HMR
new webpack.NamedModulesPlugin() // 打印日志信息時(shí) webpack 默認(rèn)使用模塊的數(shù)字 ID 指代模塊,不便于 debug,這個(gè)插件可以將其替換為模塊的真實(shí)路徑
],
// ...
};
Tips
webpack-dev-server 會(huì)為每個(gè)入口文件創(chuàng)建一個(gè)客戶端腳本,這個(gè)腳本會(huì)監(jiān)控該入口文件的依賴模塊的更新,如果該入口文件編寫了 HMR 處理函數(shù),它就能接收依賴模塊的更新,反之,更新會(huì)向上冒泡,直到客戶端腳本仍沒(méi)有處理函數(shù)的話,webpack-dev-server 會(huì)重新加載整個(gè)頁(yè)面。如果入口文件本身發(fā)生了更新,因?yàn)橄蛏蠒?huì)冒泡到客戶端腳本,并且不存在 HMR 處理函數(shù),所以會(huì)導(dǎo)致頁(yè)面重載。
我們已經(jīng)開啟了 HMR 的功能,HMR 的接口已經(jīng)暴露在module.hot屬性之下,我們只需要調(diào)用 HMR API 即可實(shí)現(xiàn)熱加載。當(dāng)“被加載模塊”發(fā)生改變時(shí),依賴該模塊的模塊便能檢測(cè)到改變并接收改變之后的模塊。
src/index.js
import _ from 'lodash';
import printMe from './print.js';
// import './style.css';
// import Icon from './icon.jpg';
// import Data from './data.json';
function component() {
const element = document.createElement('div');
const btn = document.createElement('button');
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
// element.classList.add('hello');
// const myIcon = new Image();
// myIcon.src = Icon;
// element.appendChild(myIcon);
// console.log(Data);
btn.innerHTML = 'Click me and check the console!';
btn.onclick = printMe;
element.appendChild(btn);
return element;
}
document.body.appendChild(component());
if(module.hot) { // 習(xí)慣上我們會(huì)檢查是否可以訪問(wèn) `module.hot` 屬性
module.hot.accept('./print.js', function() { // 接受給定依賴模塊的更新,并觸發(fā)一個(gè)回調(diào)函數(shù)來(lái)對(duì)這些更新做出響應(yīng)
console.log('Accepting the updated printMe module!');
printMe();
});
}
npm start之。為了演示效果,我們做如下修改:
src/print.js
export default function printMe() {
- console.log('I get called from print.js!');
+ console.log('Updating print.js...');
}
我們會(huì)看到控制臺(tái)打印出的信息中含有以下幾行:
index.js:33 Accepting the updated printMe module!
print.js:2 Updating print.js...
log.js:23 [HMR] Updated modules:
log.js:23 [HMR] - ./src/print.js
log.js:23 [HMR] App is up to date.
Tips
webpack-dev-server 在 inline mode (此為默認(rèn)模式) 時(shí),會(huì)為每個(gè)入口起點(diǎn) (entry) 創(chuàng)建一個(gè)客戶端腳本,所以你會(huì)在上面的輸出中看到有些信息重復(fù)輸出兩次。
但是當(dāng)你點(diǎn)擊頁(yè)面的按鈕時(shí),你會(huì)發(fā)現(xiàn)控制臺(tái)輸出的是舊的printMe函數(shù)輸出的信息,因?yàn)?code>onclick事件綁定的仍是原始的printMe函數(shù)。我們需要在module.hot.accept里更新綁定。
src/index.js
import _ from 'lodash';
import printMe from './print.js';
// import './style.css';
// import Icon from './icon.jpg';
// import Data from './data.json';
// ...
// document.body.appendChild(component());
var element = component();
document.body.appendChild(element);
if(module.hot) {
module.hot.accept('./print.js', function() {
console.log('Accepting the updated printMe module!');
// printMe();
document.body.removeChild(element);
element = component();
document.body.appendChild(element);
});
}
Tips
uglifyjs-webpack-plugin 升級(jí)到 v0.4.6 時(shí)無(wú)法正確壓縮 ES6 的代碼,所以上面有些代碼采用 ES5 以暫時(shí)方便后面的壓縮,詳見 #49。
模塊熱替換也可以用于樣式的修改,效果跟控制臺(tái)修改一樣一樣的。
src/index.js
import _ from 'lodash';
import printMe from './print.js';
import './style.css';
// import Icon from './icon.jpg';
// import Data from './data.json';
// ...
npm start之,做如下修改:
/* ... */
body {
background-color: yellow;
}
可以發(fā)現(xiàn)在不重載頁(yè)面的前提下我們對(duì)樣式的修改進(jìn)行了熱加載,棒!
生產(chǎn)環(huán)境
自動(dòng)方式
我們只需要運(yùn)行webpack -p (相當(dāng)于 webpack --optimize-minimize --define process.env.NODE_ENV="'production'")這個(gè)命令,便可以自動(dòng)構(gòu)建生產(chǎn)版本的應(yīng)用,這個(gè)命令會(huì)完成以下步驟:
- 使用
UglifyJsPlugin(webpack.optimize.UglifyJsPlugin) 壓縮 JS 文件 (此插件和 uglifyjs-webpack-plugin 相同) - 運(yùn)行
LoaderOptionsPlugin插件,這個(gè)插件是用來(lái)遷移的,見 document - 設(shè)置 NodeJS 的環(huán)境變量,觸發(fā)某些 package 包以不同方式編譯
值得一提的是,webpack -p設(shè)置的process.env.NODE_ENV環(huán)境變量,是用于編譯后的代碼的,只有在打包后的代碼中,這一環(huán)境變量才是有效的。如果在 webpack 配置文件中引用此環(huán)境變量,得到的是 undefined,可以參見 #2537。但是,有時(shí)我們確實(shí)需要在 webpack 配置文件中使用 process.env.NODE_ENV,怎么辦呢?一個(gè)方法是運(yùn)行NODE_ENV='production' webpack -p命令,不過(guò)這個(gè)命令在Windows中是會(huì)出問(wèn)題的。為了解決兼容問(wèn)題,我們采用 cross-env 解決跨平臺(tái)的問(wèn)題。
$ npm i --save-dev cross-env
package.json
{
...
"scripts": {
"build": "cross-env NODE_ENV=production webpack -p",
"start": "webpack-dev-server --open"
},
...
}
現(xiàn)在可以在配置文件中使用process.env.NODE_ENV了。
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const webpack = require('webpack');
module.exports = {
// ...
output: {
// filename: 'bundle.js',
// filename: '[name].bundle.js',
filename: process.env.NODE_ENV === 'production' ? '[name].[chunkhash].js' : '[name].bundle.js', // 在配置文件中使用`process.env.NODE_ENV`
path: path.resolve(__dirname, 'dist')
},
plugins: [
new HtmlWebpackPlugin({
title: 'webpack demo',
filename: 'index.html'
}),
new CleanWebpackPlugin(['dist']),
// new webpack.HotModuleReplacementPlugin(), // 關(guān)閉 HMR 功能
new webpack.NamedModulesPlugin()
],
// ...
};
Tips
[chunkhash]不能和 HMR 一起使用,換句話說(shuō),不應(yīng)該在開發(fā)環(huán)境中使用 [chunkhash] (或者 [hash]),這會(huì)導(dǎo)致許多問(wèn)題。詳情見 #2393 和 #377。
build 之,我們得到了生產(chǎn)版本的壓縮好的打包文件。
多配置文件配置
有時(shí)我們會(huì)需要為不同的環(huán)境配置不同的配置文件,可以選擇 簡(jiǎn)易方法,這里我們采用較為先進(jìn)的方法。先準(zhǔn)備一個(gè)基本的配置文件,包含了所有環(huán)境都包含的配置,然后用 webpack-merge 將它和特定環(huán)境的配置文件合并并導(dǎo)出,這樣就減少了基本配置的重復(fù)。
$ npm i --save-dev webpack-merge
webpack.common.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
module.exports = {
entry: {
app: './src/index.js',
print: './src/print.js'
},
output: {
path: path.resolve(__dirname, 'dist')
},
plugins: [
new HtmlWebpackPlugin({
title: 'webpack demo',
filename: 'index.html'
}),
new CleanWebpackPlugin(['dist'])
],
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /\.(png|svg|jpg|gif)$/,
use: [
'file-loader'
]
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
use: [
'file-loader'
]
}
]
}
};
webpack.dev.js
const path = require('path');
const webpack = require('webpack');
const Merge = require('webpack-merge');
const CommonConfig = require('./webpack.common.js');
module.exports = Merge(CommonConfig, {
devtool: 'cheap-module-eval-source-map',
devServer: {
contentBase: path.resolve(__dirname, 'dist'),
hot: true,
hotOnly: true
},
output: {
filename: '[name].bundle.js'
},
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('development') // 在編譯的代碼里設(shè)置了`process.env.NODE_ENV`變量
}),
new webpack.HotModuleReplacementPlugin(),
new webpack.NamedModulesPlugin()
]
});
webpack.prod.js
const path = require('path');
const webpack = require('webpack');
const Merge = require('webpack-merge');
const CommonConfig = require('./webpack.common.js');
module.exports = Merge(CommonConfig, {
devtool: 'cheap-module-source-map',
output: {
filename: '[name].[chunkhash].js'
},
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production')
}),
new webpack.optimize.UglifyJsPlugin()
]
});
package.json
{
...
"scripts": {
"build": "cross-env NODE_ENV=production webpack -p",
"start": "webpack-dev-server --open",
"build:dev": "webpack-dev-server --open --config webpack.dev.js",
"build:prod": "webpack --progress --config webpack.prod.js"
},
...
}
現(xiàn)在只需執(zhí)行npm run build:dev或npm run build:prod便可以得到開發(fā)版或者生產(chǎn)版了!
Tips
webpack 命令行選項(xiàng)見 Command Line Interface。
代碼分離
入口分離
我們先創(chuàng)建一個(gè)新文件:
$ cd src && touch another.js
src/another.js
import _ from 'lodash';
console.log(_.join(['Another', 'module', 'loaded!'], ' '));
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const webpack = require('webpack');
module.exports = {
// ...
entry: {
app: './src/index.js',
// print: './src/print.js'
another: './src/another.js'
},
// ...
};
cd .. && npm run build之,我們發(fā)現(xiàn)用入口分離的代碼得到了兩個(gè)大文件,這是因?yàn)閮蓚€(gè)入口文件都引入了lodash,這很大程度上造成了冗余,在同一個(gè)頁(yè)面中我們只需要引入一個(gè)lodash就可以了。
抽取相同部分
我們使用 CommonsChunkPlugin 插件來(lái)將相同的部分提取出來(lái)放到一個(gè)單獨(dú)的模塊中。
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const webpack = require('webpack');
module.exports = {
// devtool: 'inline-source-map',
// ...
output: {
// filename: 'bundle.js',
filename: '[name].bundle.js',
// filename: process.env.NODE_ENV === 'production' ? '[name].[chunkhash].js' : '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
},
plugins: [
new HtmlWebpackPlugin({
title: 'webpack demo',
filename: 'index.html'
}),
new CleanWebpackPlugin(['dist']),
new webpack.optimize.CommonsChunkPlugin({
name: 'common' // 抽取出的模塊的模塊名
}),
// new webpack.HotModuleReplacementPlugin(),
// new webpack.NamedModulesPlugin()
],
// ...
};
build 之,可以看到結(jié)果中包含以下部分:
app.bundle.js 6.14 kB 0 [emitted] app
another.bundle.js 185 bytes 1 [emitted] another
common.bundle.js 73.2 kB 2 [emitted] common
index.html 314 bytes [emitted]
我們把lodash分離出來(lái)了。
動(dòng)態(tài)引入
我們還可以選擇以動(dòng)態(tài)引入的方式來(lái)實(shí)現(xiàn)代碼分離,借助 import() 實(shí)現(xiàn)之。
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
// const webpack = require('webpack');
module.exports = {
// ...
entry: {
app: './src/index.js',
// print: './src/print.js'
// another: './src/another.js'
},
output: {
// filename: 'bundle.js',
filename: '[name].bundle.js',
chunkFilename: '[name].bundle.js', // 指定非入口塊文件輸出的名字
// filename: process.env.NODE_ENV === 'production' ? '[name].[chunkhash].js' : '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
},
plugins: [
new HtmlWebpackPlugin({
title: 'webpack demo',
filename: 'index.html'
}),
new CleanWebpackPlugin(['dist'])
// new webpack.optimize.CommonsChunkPlugin({
// name: 'common'
// }),
// new webpack.HotModuleReplacementPlugin(),
// new webpack.NamedModulesPlugin()
],
// ...
};
src/index.js
// import _ from 'lodash';
import printMe from './print.js';
// import './style.css';
// import Icon from './icon.jpg';
// import Data from './data.json';
function component() {
// 此函數(shù)原來(lái)的內(nèi)容全部注釋掉...
return import(/* webpackChunkName: "lodash" */ 'lodash').then(function(_) {
const element = document.createElement('div');
const btn = document.createElement('button');
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
btn.innerHTML = 'Click me and check the console!';
btn.onclick = printMe;
element.appendChild(btn);
return element;
}).catch(function(error) {
console.log('An error occurred while loading the component')
});
}
// document.body.appendChild(component());
// var element = component();
// document.body.appendChild(element);
// 原本熱加載的部分全部注釋掉...
component().then(function(component) {
document.body.appendChild(component);
});
Tips
注意上面中的/* webpackChunkName: "lodash" */這段注釋,它并不是可有可無(wú)的,它能幫助我們結(jié)合output.chunkFilename把分離出的模塊最終命名為lodash.bundle.js而非[id].bundle.js。
現(xiàn)在 build 之看看吧。
懶加載 (lazy loading)
既然有了import(),我們可以選擇在需要的時(shí)候才加載相應(yīng)的模塊,減少了應(yīng)用初始化時(shí)加載大量暫不需要的模塊的壓力,這能讓我們的應(yīng)用更高效地運(yùn)行。
src/print.js
console.log('The print.js module has loaded! See the network tab in dev tools...');
export default function printMe() {
// console.log('Updating print.js...');
console.log('Button Clicked: Here\'s "some text"!');
}
src/index.js
import _ from 'lodash';
// 其他引入注釋...
function component() {
const element = document.createElement('div');
const btn = document.createElement('button');
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
// element.classList.add('hello');
// const myIcon = new Image();
// myIcon.src = Icon;
// element.appendChild(myIcon);
// console.log(Data);
btn.innerHTML = 'Click me and check the console!';
// btn.onclick = printMe;
element.appendChild(btn);
btn.onclick = function() {
import(/* webpackChunkName: "print" */ './print')
.then(function(module) {
const printMe = module.default; // 引入模塊的默認(rèn)函數(shù)
printMe();
});
};
return element;
// 原本的動(dòng)態(tài)引入注釋...
}
document.body.appendChild(component());
// var element = component();
// document.body.appendChild(element);
// 熱加載部分注釋
// component().then(function(component) {
// document.body.appendChild(component);
// });
構(gòu)建之,控制臺(tái)此時(shí)并無(wú)輸出,點(diǎn)擊按鈕,會(huì)看到控制臺(tái)如下輸出:
print.bundle.js:1 The print.js module has loaded! See the network tab in dev tools...
print.bundle.js:1 Button Clicked: Here's "some text"!
說(shuō)明 print 模塊只在我們點(diǎn)擊時(shí)才引入了,すっげえ!
緩存 (caching)
瀏覽器在初次加載網(wǎng)站時(shí),會(huì)下載很多文件,為了較少下載大量資源的壓力,瀏覽器會(huì)對(duì)資源進(jìn)行緩存 (caching),這樣瀏覽器便可以更迅速地加載網(wǎng)站,但是我們需要在文件內(nèi)容發(fā)生改變時(shí)更新文件。
我們可以在輸出文件名上下手腳:
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
// const webpack = require('webpack');
module.exports = {
// ...
output: {
// filename: 'bundle.js',
filename: '[name].[chunkhash].js',
// chunkFilename: '[name].bundle.js',
// filename: process.env.NODE_ENV === 'production' ? '[name].[chunkhash].js' : '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
},
// ...
};
Tips
[chunkhash] 是內(nèi)容相關(guān)的,只要內(nèi)容發(fā)生了改變,構(gòu)建后文件名的 hash 就會(huì)發(fā)生改變。
還有一個(gè)要點(diǎn)是提取出第三方庫(kù)放到單獨(dú)模塊中,因?yàn)樗鼈兪遣惶赡茴l繁發(fā)生改變的,所以無(wú)需多次加載這些模塊,提取的方法用 CommonsChunkPlugin 插件,這個(gè)插件上文中提到過(guò),指定入口文件名時(shí)它會(huì)提取改入口文件為單個(gè)文件,不指定則會(huì)提取 webpack 的運(yùn)行時(shí)代碼。
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const webpack = require('webpack');
module.exports = {
// ...
entry: {
app: './src/index.js',
vendor: [ // 第三方庫(kù)可以統(tǒng)一放在這個(gè)入口一起合并
'lodash'
]
// print: './src/print.js'
// another: './src/another.js'
},
output: {
// filename: 'bundle.js',
filename: '[name].[chunkhash].js',
chunkFilename: '[name].bundle.js',
// filename: process.env.NODE_ENV === 'production' ? '[name].[chunkhash].js' : '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
},
plugins: [
new HtmlWebpackPlugin({
title: 'webpack demo',
filename: 'index.html'
}),
new CleanWebpackPlugin(['dist']),
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor' // 將 vendor 入口處的代碼放入 vendor 模塊
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'runtime' // 將 webpack 自身的運(yùn)行時(shí)代碼放在 runtime 模塊
})
// new webpack.HotModuleReplacementPlugin(),
// new webpack.NamedModulesPlugin()
],
// ...
};
Tips
包含 vendor 的 CommonsChunkPlugin 實(shí)例必須在包含 runtime 的之前,否則會(huì)報(bào)錯(cuò)。
src/index.js
// import _ from 'lodash';
// ...
// ...
如果我們?cè)?src 下新建一個(gè)文件h.js,再在index.js中引入它,保存,構(gòu)建之,我們發(fā)現(xiàn)有些沒(méi)改變的模塊的 hash 也發(fā)生了改變,這是因?yàn)榧尤?code>h.js后它們的module.id變了,但這明顯是不合理的。在開發(fā)環(huán)境,我們可以用 NamedModulesPlugin 將 id 換成具體路徑名。而在生產(chǎn)環(huán)境,我們可以使用 HashedModuleIdsPlugin。
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const webpack = require('webpack');
module.exports = {
// ...
plugins: [
new HtmlWebpackPlugin({
title: 'webpack demo',
filename: 'index.html'
}),
new webpack.HashedModuleIdsPlugin(), // 替換掉原來(lái)的`module.id`
new CleanWebpackPlugin(['dist']),
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor'
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'runtime'
})
// new webpack.HotModuleReplacementPlugin(),
// new webpack.NamedModulesPlugin()
],
// ...
};
再來(lái)執(zhí)行剛才那波操作,就會(huì)發(fā)現(xiàn)無(wú)關(guān)修改的模塊 hash 未變了。
Shimming
Tips
你可以將 shim 簡(jiǎn)單理解為是用于兼容 API 的小型庫(kù)。
使用 jQuery 時(shí)我們習(xí)慣性地使用$或jQuery變量,每次都使用const $ = require(“jquery”)引入的話太麻煩,如果能直接把這兩個(gè)變量設(shè)置為全局變量豈不美滋滋?這樣就可以在每個(gè)模塊中直接使用這兩個(gè)變量了。為了兼容這一做法,我們使用 ProvidePlugin 插件為我們完成這一任務(wù)。
$ npm i --save jquery
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const webpack = require('webpack');
module.exports = {
// ...
plugins: [
new HtmlWebpackPlugin({
title: 'webpack demo',
filename: 'index.html'
}),
new webpack.ProvidePlugin({ // 設(shè)置全局變量
$: 'jquery',
jQuery: 'jquery'
}),
new webpack.HashedModuleIdsPlugin(),
new CleanWebpackPlugin(['dist']),
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor'
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'runtime'
})
// new webpack.HotModuleReplacementPlugin(),
// new webpack.NamedModulesPlugin()
],
// ...
};
src/print.js
console.log('The print.js module has loaded! See the network tab in dev tools...');
console.log($('title').text()); // 使用 jQuery
export default function printMe() {
// console.log('Updating print.js...');
console.log('Button Clicked: Here\'s "some text"!');
}
build,點(diǎn)擊頁(yè)面按鈕,成功了。
另外,如果你需要在某些模塊加載時(shí)設(shè)置該模塊的全局變量,請(qǐng)看 這里。
結(jié)尾的一點(diǎn)廢話
終于寫完了 :),也感謝你能耐心看到這里。webpack 這個(gè)工具的配置還是有些麻煩的。但是呢,某人說(shuō)這個(gè)東東前期會(huì)花比較多時(shí)間,后期會(huì)大大提高你的效率。所以呢,還是拿下這個(gè)東東吧。有其他需求的話可以繼續(xù)看官方的文檔。遇到困難可以找:
我寫好的 demo 文件放在了這里。
Reference
- 入門 Webpack,看這篇就夠了 - v1
- Webpack Guides
- Webpack: When To Use And Why
- stackoverflow / github issues