原文首發(fā)于:Webpack 3洽瞬,從入門到放棄
Update (2017.8.27) : 關于
output.publicPath
本涕、devServer.contentBase
、devServer.publicPath
的區(qū)別伙窃。如下:
- output.publicPath: 對于這個選項菩颖,我們無需關注什么絕對相對路徑,因為兩種路徑都可以为障。我們只需要知道一點:這個選項是指定 HTML 文件中資源文件 (字體晦闰、圖片放祟、JS文件等) 的
文件名
的公共 URL 部分的。在實際情況中呻右,我們首先會通過output.filename
或有些 loader 如file-loader
的name
屬性設置文件名
的原始部分跪妥,webpack 將文件名
的原始部分和公共部分結(jié)合之后,HTML 文件就能獲取到資源文件了声滥。- devServer.contentBase: 設置靜態(tài)資源的根目錄眉撵,
html-webpack-plugin
生成的 html 不是靜態(tài)資源。當用 html 文件里的地址無法找到靜態(tài)資源文件時就會去這個目錄下去找落塑。- devServer.publicPath: 指定瀏覽器上訪問所有 打包(bundled)文件 (在
dist
里生成的所有文件) 的根目錄纽疟,這個根目錄是相對服務器地址及端口的,比devServer.contentBase
和output.publicPath
優(yōu)先憾赁。
前言
Tips
如果你用過 webpack 且一直用的是 webpack 1污朽,請參考 從v1遷移到v2 (v2 和 v3 差異不大) 對版本變更的內(nèi)容進行適當?shù)牧私猓缓笤龠x擇性地閱讀本文缠沈。
首先膘壶,這篇文章是根據(jù)當前最新的 webpack 版本 (即 v3.4.1) 撰寫,較長一段時間內(nèi)無需擔心過時的問題洲愤。其次颓芭,這應該會是一篇極長的文章,涵蓋了基本的使用方法柬赐,有更高級功能的需求可以參考官方文檔繼續(xù)學習亡问。再次,即使是基本的功能肛宋,也內(nèi)容繁多州藕,我盡可能地解釋通俗易懂,將我學習過程中的疑惑和坑一一解釋酝陈,如有紕漏床玻,敬請雅正。再次沉帮,為了清晰有效地講解锈死,我會演示從零編寫 demo,只要一步步跟著做穆壕,就會清晰許多待牵。最后,官方文檔也是個坑爹貨喇勋!
Webpack缨该,何許人也?
借用官方的說法:
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.
簡言之川背,webpack 是一個模塊打包器 (module bundler)贰拿,能夠?qū)⑷魏钨Y源如 JavaScript 文件蛤袒、CSS 文件、圖片等打包成一個或少數(shù)文件壮不。
為什么要用介個 Webpack?
首先汗盘,定義已經(jīng)說明了 webpack 能將多個資源模塊打包成一個或少數(shù)文件皱碘,這意味著與以往的發(fā)起多個 HTTP 請求來獲得資源相比询一,現(xiàn)在只需要發(fā)起少量的 HTTP 請求。
Tips
想了解合并 HTTP 請求的意義癌椿,請見 這里健蕊。
其次,webpack 能將你的資源轉(zhuǎn)換為最適合瀏覽器的“格式”踢俄,提升應用性能缩功。比如只引用被應用使用的資源 (剔除未被使用的代碼),懶加載資源 (只在需要的時候才加載相應的資源)都办。再次嫡锌,對于開發(fā)階段,webpack 也提供了實時加載和熱加載的功能琳钉,大大地節(jié)省了開發(fā)時間势木。除此之外,還有許多優(yōu)秀之處之處值得去挖掘歌懒。不過啦桌,webpack 最核心的還是打包的功能。
webpack及皂,gulp/grunt甫男,npm,它們有什么區(qū)別?
webpack 是模塊打包器(module bundler)验烧,把所有的模塊打包成一個或少量文件板驳,使你只需加載少量文件即可運行整個應用,而無需像之前那樣加載大量的圖片碍拆,css文件若治,js文件,字體文件等等倔监。而gulp/grunt 是自動化構(gòu)建工具直砂,或者叫任務運行器(task runner),是把你所有重復的手動操作讓代碼來做浩习,例如壓縮JS代碼静暂、CSS代碼,代碼檢查谱秽、代碼編譯等等洽蛀,自動化構(gòu)建工具并不能把所有模塊打包到一起摹迷,也不能構(gòu)建不同模塊之間的依賴圖。兩者來比較的話郊供,gulp/grunt 無法做模塊打包的事峡碉,webpack 雖然有 loader 和 plugin可以做一部分 gulp/grunt 能做的事,但是終究 webpack 的插件還是不如 gulp/grunt 的插件豐富驮审,能做的事比較有限鲫寄。于是有人兩者結(jié)合著用,將 webpack 放到 gulp/grunt 中用疯淫。然而地来,更好的方法是用 npm scripts 取代 gulp/grunt,npm 是 node 的包管理器 (node package manager)熙掺,用于管理 node 的第三方軟件包未斑,npm 對于任務命令的良好支持讓你最終省卻了編寫任務代碼的必要,取而代之的币绩,是老祖宗的幾個命令行蜡秽,僅靠幾句命令行就足以完成你的模塊打包和自動化構(gòu)建的所有需求。
準備開始
先來看看一個 webpack 的一個完備的配置文件缆镣,是 介樣 的芽突,當然啦,這里面有很多配置項是即使到這個軟件被廢棄你也用不上的:)费就,所以無需擔心诉瓦。
基本配置
開始之前,請確定你已經(jī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ù)庫 lodash 來演示我們的 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 的標準,但是仍未得到大多數(shù)瀏覽器的支持 (可喜的是眠蚂, Chrome 61 已經(jīng)開始默認支持了煞聪,見 ES6 modules),不過 webpack 提供了對這個特性的支持逝慧,但是除了這個特性昔脯,其他的 ES6 特性并不會得到 webpack 的特別支持,如有需要笛臣,須借助 Babel 進行轉(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)在,我們運行 webpack 來打包 index.js
為 bundle.js
沈堡,本地安裝了 webpack 后可以通過 node_modules/.bin/webpack
來訪問 webpack 的二進制版本静陈。
$ cd ..
$ ./node_modules/.bin/webpack src/index.js dist/bundle.js # 第一個參數(shù)是打包的入口文件,第二個參數(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)得到了你的第一個打包文件 (bundle.js) 了矢洲。
使用配置文件
像上面這樣使用 webpack 應該是最挫的姿勢了贷掖,所以我們要使用 webpack 的配置文件來提高我們的姿勢水平聋溜。
$ touch webpack.config.js
webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js', // 入口起點酥郭,可以指定多個入口起點
output: { // 輸出,只可指定一個輸出配置
filename: 'bundle.js', // 輸出文件名
path: path.resolve(__dirname, 'dist') // 輸出文件所在的目錄
}
};
執(zhí)行:
$ ./node_modules/.bin/webpack --config webpack.config.js # `--config` 制定 webpack 的配置文件撞叨,默認是 `webpack.config.js`
所以這里可以省卻 --config webpack.config.js
金踪。但是每次都要寫 ./node_modules/.bin/webpack
實在讓人不爽,所以我們要動用 NPM Scripts谒所。
package.json
{
...
"scripts": {
"build": "webpack"
},
...
}
Tips
在npm scripts
中我們可以通過包名直接引用本地安裝的 npm 包的二進制版本热康,而無需編寫包的整個路徑沛申。
執(zhí)行:
$ npm run build
一波輸出后便得到了打包文件劣领。
Tips
bulid
并不是npm scripts
的內(nèi)置屬性,需要使用npm run
來執(zhí)行腳本铁材,詳情見 npm run尖淘。
打包其他類型的文件
因為其他文件和 JS 文件類型不同,要把他們加載到 JS 文件中就需要經(jīng)過加載器 (loader) 的處理著觉。
加載 CSS
我們需要安裝兩個 loader 來處理 CSS 文件:
$ npm i --save-dev style-loader css-loader
style-loader 通過插入 <style> 標簽將 CSS 加入到 DOM 中村生,css-loader 會像解釋 import/require() 一樣解釋 @import 和 url()。
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: { // 如何處理項目中不同類型的模塊
rules: [ // 用于規(guī)定在不同模塊被創(chuàng)建時如何處理模塊的規(guī)則數(shù)組
{
test: /\.css$/, // 匹配特定文件的正則表達式或正則表達式數(shù)組
use: [ // 應用于模塊的 loader 使用列表
'style-loader',
'css-loader'
]
}
]
}
};
我們來創(chuàng)建一個 CSS 文件:
$ cd src && touch style.css
src/style.css
.hello {
color: red;
}
src/index.js
import _ from 'lodash';
import './style.css'; // 通過`import`引入 CSS 文件
function component() {
const element = document.createElement('div');
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
element.classList.add('hello'); // 在相應元素上添加類名
return element;
}
document.body.appendChild(component());
執(zhí)行npm run build
饼丘,然后打開index.html
趁桃,就可以看到紅色的字體了。CSS 文件此時已經(jīng)被打包到 bundle.js 中肄鸽。再打開瀏覽器控制臺卫病,就可以看到 webpack 做了些什么。
加載圖片
$ npm install --save-dev file-loader
file-loader 指示 webpack 以文件格式發(fā)出所需對象并返回文件的公共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'
]
}
]
}
};
我們在當前項目的目錄中如下增加圖片:
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
之。現(xiàn)在你可以看到單獨的圖片和以圖片為基礎的背景圖了逮诲。
加載字體
加載字體用的也是 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'
]
}
]
}
};
在當前項目的目錄中如下增加字體:
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;
}
運行打包命令之后便可以看到打包好的文件和發(fā)生改變的頁面。
加載 JSON 文件
因為 webpack 對 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 解析得到的對象
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());
關于其他文件的加載,可以尋求相應的 loader齐唆。
輸出管理
前面我們只有一個輸入文件嗤栓,但現(xiàn)實是我們往往有不止一個輸入文件,這時我們就需要輸入多個入口文件并管理輸出文件蝶念。我們在 src 目錄下增加一個 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ù)入口起點名動態(tài)生成 bundle 名芋绸,可以使用像 "js/[name]/bundle.js" 這樣的文件夾結(jié)構(gòu)
path: path.resolve(__dirname, 'dist')
},
// ...
};
Tips
filename: '[name].bundle.js'
中的[name]
會替換為對應的入口起點名,其他可用的替換請參見 output.filename担敌。
現(xiàn)在可以打包文件了摔敛。但是如果我們修改了入口文件名或增加了入口文件,index.html
是不會自動引用新文件的全封,而手動修改實在太挫马昙。是時候使用插件 (plugin) 來完成這一任務了。我們使用 HtmlWebpackPlugin 自動生成 html 文件刹悴。
loader 和 plugin行楞,有什么區(qū)別?
loader (加載器)土匀,重在“加載”二字子房,是用于預處理文件的,只用于在加載不同類型的文件時對不同類型的文件做相應的處理就轧。而 plugin (插件)证杭,顧名思義,是用來增加 webpack 的功能的妒御,作用于整個 webpack 的構(gòu)建過程解愤。在 webpack 這個大公司中,loader 是保安大叔乎莉,負責對進入公司的不同人員的處理送讲,而 plugin 則是公司里不同職位的職員,負責公司里的各種不同業(yè)務惋啃,每增加一種新型的業(yè)務需求哼鬓,我們就需要增加一種 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ù)組
new HtmlWebpackPlugin({
title: 'webpack demo', // 生成 HTML 文檔的標題
filename: 'index.html' // 寫入 HTML 文件的文件名魄宏,默認 `index.html`
})
],
// ...
};
你可以先把 dist 文件夾的index.html
文件刪除,然后執(zhí)行打包命令存筏。咻咻咻宠互,我們看到 dist 目錄下已經(jīng)自動生成了一個index.html
文件,但即使不刪除原先的index.html
椭坚,該插件默認生成的index.html
也會替換原本的index.html
予跌。
此刻,當你細細觀察 dist 目錄時善茎,雖然現(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']) // 第一個參數(shù)是要清理的目錄的字符串數(shù)組
],
// ...
};
打包之航邢,現(xiàn)在,dist 中只存在打包生成的文件骄蝇。
開發(fā)環(huán)境
webpack 提供了很多便于開發(fā)時使用的功能膳殷,來一一看看吧。
使用代碼映射 (source map)
當你的代碼被打包后九火,如果打包后的代碼發(fā)生了錯誤赚窃,你很難追蹤到錯誤發(fā)生的原始位置,這個時候岔激,我們就需要代碼映射 (source map) 這種工具勒极,它能將編譯后的代碼映射回原始的源碼,你的錯誤是起源于打包前的b.js
的某個位置虑鼎,代碼映射就能告訴你錯誤是那個模塊的那個位置辱匿。webpack 默認提供了 10 種風格的代碼映射,使用它們會明顯影響到構(gòu)建 (build) 和重構(gòu)建 (rebuild震叙,每次修改后需要重新構(gòu)建) 的速度,十種風格的差異可以參看 devtool戚丸。關于如何選擇映射風格可以參看 Webpack devtool source map。這里限府,我們?yōu)榱藴蚀_顯示錯誤位置,選擇速度較慢的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)在來手動制造一些錯誤:
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
再點擊按鈕,你就會看到控制臺顯示如下報錯:
Uncaught ReferenceError: cosnole is not defined
at HTMLButtonElement.printMe (print.js:2)
現(xiàn)在残家,我們很清楚哪里發(fā)生了錯誤烁涌,然后輕松地改正之瑞信。
使用 webpack-dev-server
你一定有這樣的體驗筐眷,開發(fā)時每次修改代碼保存后都需要重新手動構(gòu)建代碼并手動刷新瀏覽器以觀察修改效果,這是很麻煩的垫毙,所以霹疫,我們要實時加載代碼∽劢妫可喜的是丽蝎,webpack 提供了對實時加載代碼的支持。我們需要安裝 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: { // 檢測代碼變化并自動重新編譯并自動刷新瀏覽器
contentBase: path.resolve(__dirname, 'dist') // 設置靜態(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 時征峦,webpack 并沒有將所有生成的文件寫入磁盤,而是放在內(nèi)存中消请,提供更快的內(nèi)存內(nèi)訪問,便于實時更新类腮。
現(xiàn)在臊泰,可以直接運行npm start
(start
是 npm scripts 的內(nèi)置屬性,可直接運行)蚜枢,然后瀏覽器自動加載應用的頁面缸逃,默認在localhost:8080
顯示。
模塊熱替換 (HMR, Hot Module Replacement)
webpack 提供了對模塊熱替換 (或者叫熱加載) 的支持厂抽。這一特性能夠讓應用運行的時候替換需频、增加或刪除模塊,而無需進行完全的重載筷凤。想進一步地了解其工作機理昭殉,可以參見 Hot Module Replacement苞七,但這并不是必需的,你可以選擇跳過機理部分繼續(xù)往下閱讀挪丢。
Tips
模塊熱替換(HMR)只更新發(fā)生變更(替換蹂风、添加、刪除)的模塊乾蓬,而無需重新加載整個頁面(實時加載惠啄,LiveReload),這樣可以顯著加快開發(fā)速度任内,一旦打開了 webpack-dev-server 的 hot 模式撵渡,在試圖重新加載整個頁面之前,熱模式會嘗試使用 HMR 來更新死嗦。
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 我們在用 HMR
hotOnly: true // 指定如果熱加載失敗了禁止刷新頁面 (這是 webpack 的默認行為)趋距,這樣便于我們知道失敗是因為何種錯誤
},
// 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() // 打印日志信息時 webpack 默認使用模塊的數(shù)字 ID 指代模塊,不便于 debug越走,這個插件可以將其替換為模塊的真實路徑
],
// ...
};
Tips
webpack-dev-server 會為每個入口文件創(chuàng)建一個客戶端腳本棚品,這個腳本會監(jiān)控該入口文件的依賴模塊的更新,如果該入口文件編寫了 HMR 處理函數(shù)廊敌,它就能接收依賴模塊的更新铜跑,反之,更新會向上冒泡骡澈,直到客戶端腳本仍沒有處理函數(shù)的話锅纺,webpack-dev-server 會重新加載整個頁面。如果入口文件本身發(fā)生了更新肋殴,因為向上會冒泡到客戶端腳本囤锉,并且不存在 HMR 處理函數(shù),所以會導致頁面重載护锤。
我們已經(jīng)開啟了 HMR 的功能官地,HMR 的接口已經(jīng)暴露在module.hot
屬性之下,我們只需要調(diào)用 HMR API 即可實現(xiàn)熱加載烙懦。當“被加載模塊”發(fā)生改變時驱入,依賴該模塊的模塊便能檢測到改變并接收改變之后的模塊。
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) { // 習慣上我們會檢查是否可以訪問 `module.hot` 屬性
module.hot.accept('./print.js', function() { // 接受給定依賴模塊的更新氯析,并觸發(fā)一個回調(diào)函數(shù)來對這些更新做出響應
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...');
}
我們會看到控制臺打印出的信息中含有以下幾行:
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 (此為默認模式) 時掩缓,會為每個入口起點 (entry) 創(chuàng)建一個客戶端腳本雪情,所以你會在上面的輸出中看到有些信息重復輸出兩次。
但是當你點擊頁面的按鈕時你辣,你會發(fā)現(xiàn)控制臺輸出的是舊的printMe
函數(shù)輸出的信息巡通,因為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 升級到 v0.4.6 時無法正確壓縮 ES6 的代碼扁达,所以上面有些代碼采用 ES5 以暫時方便后面的壓縮正卧,詳見 #49。
模塊熱替換也可以用于樣式的修改跪解,效果跟控制臺修改一樣一樣的炉旷。
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)在不重載頁面的前提下我們對樣式的修改進行了熱加載叉讥,棒窘行!
生產(chǎn)環(huán)境
自動方式
我們只需要運行webpack -p
(相當于 webpack --optimize-minimize --define process.env.NODE_ENV="'production'"
)這個命令,便可以自動構(gòu)建生產(chǎn)版本的應用图仓,這個命令會完成以下步驟:
- 使用
UglifyJsPlugin
(webpack.optimize.UglifyJsPlugin) 壓縮 JS 文件 (此插件和 uglifyjs-webpack-plugin 相同) - 運行
LoaderOptionsPlugin
插件罐盔,這個插件是用來遷移的,見 document - 設置 NodeJS 的環(huán)境變量救崔,觸發(fā)某些 package 包以不同方式編譯
值得一提的是惶看,webpack -p
設置的process.env.NODE_ENV
環(huán)境變量,是用于編譯后的代碼的六孵,只有在打包后的代碼中纬黎,這一環(huán)境變量才是有效的。如果在 webpack 配置文件中引用此環(huán)境變量劫窒,得到的是 undefined本今,可以參見 #2537。但是主巍,有時我們確實需要在 webpack 配置文件中使用 process.env.NODE_ENV
冠息,怎么辦呢?一個方法是運行NODE_ENV='production' webpack -p
命令孕索,不過這個命令在Windows中是會出問題的逛艰。為了解決兼容問題,我們采用 cross-env 解決跨平臺的問題搞旭。
$ 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(), // 關閉 HMR 功能
new webpack.NamedModulesPlugin()
],
// ...
};
Tips
[chunkhash]不能和 HMR 一起使用,換句話說选脊,不應該在開發(fā)環(huán)境中使用 [chunkhash] (或者 [hash]),這會導致許多問題脸甘。詳情見 #2393 和 #377恳啥。
build 之,我們得到了生產(chǎn)版本的壓縮好的打包文件丹诀。
多配置文件配置
有時我們會需要為不同的環(huán)境配置不同的配置文件钝的,可以選擇 簡易方法翁垂,這里我們采用較為先進的方法。先準備一個基本的配置文件硝桩,包含了所有環(huán)境都包含的配置沿猜,然后用 webpack-merge 將它和特定環(huán)境的配置文件合并并導出,這樣就減少了基本配置的重復碗脊。
$ 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') // 在編譯的代碼里設置了`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 命令行選項見 Command Line Interface。
代碼分離
入口分離
我們先創(chuàng)建一個新文件:
$ 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)用入口分離的代碼得到了兩個大文件祈坠,這是因為兩個入口文件都引入了lodash
,這很大程度上造成了冗余矢劲,在同一個頁面中我們只需要引入一個lodash
就可以了赦拘。
抽取相同部分
我們使用 CommonsChunkPlugin 插件來將相同的部分提取出來放到一個單獨的模塊中。
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
分離出來了躺同。
動態(tài)引入
我們還可以選擇以動態(tài)引入的方式來實現(xiàn)代碼分離,借助 import() 實現(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ù)原來的內(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" */
這段注釋蹋艺,它并不是可有可無的,它能幫助我們結(jié)合output.chunkFilename
把分離出的模塊最終命名為lodash.bundle.js
而非[id].bundle.js
椭员。
現(xiàn)在 build 之看看吧车海。
懶加載 (lazy loading)
既然有了import()
,我們可以選擇在需要的時候才加載相應的模塊隘击,減少了應用初始化時加載大量暫不需要的模塊的壓力侍芝,這能讓我們的應用更高效地運行。
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; // 引入模塊的默認函數(shù)
printMe();
});
};
return element;
// 原本的動態(tài)引入注釋...
}
document.body.appendChild(component());
// var element = component();
// document.body.appendChild(element);
// 熱加載部分注釋
// component().then(function(component) {
// document.body.appendChild(component);
// });
構(gòu)建之埋同,控制臺此時并無輸出州叠,點擊按鈕,會看到控制臺如下輸出:
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"!
說明 print 模塊只在我們點擊時才引入了凶赁,すっげえ咧栗!
緩存 (caching)
瀏覽器在初次加載網(wǎng)站時,會下載很多文件虱肄,為了較少下載大量資源的壓力致板,瀏覽器會對資源進行緩存 (caching),這樣瀏覽器便可以更迅速地加載網(wǎng)站咏窿,但是我們需要在文件內(nèi)容發(fā)生改變時更新文件斟或。
我們可以在輸出文件名上下手腳:
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)容相關的,只要內(nèi)容發(fā)生了改變集嵌,構(gòu)建后文件名的 hash 就會發(fā)生改變萝挤。
還有一個要點是提取出第三方庫放到單獨模塊中御毅,因為它們是不太可能頻繁發(fā)生改變的,所以無需多次加載這些模塊怜珍,提取的方法用 CommonsChunkPlugin 插件端蛆,這個插件上文中提到過,指定入口文件名時它會提取改入口文件為單個文件酥泛,不指定則會提取 webpack 的運行時代碼今豆。
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: [ // 第三方庫可以統(tǒng)一放在這個入口一起合并
'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 自身的運行時代碼放在 runtime 模塊
})
// new webpack.HotModuleReplacementPlugin(),
// new webpack.NamedModulesPlugin()
],
// ...
};
Tips
包含 vendor 的 CommonsChunkPlugin 實例必須在包含 runtime 的之前,否則會報錯揭璃。
src/index.js
// import _ from 'lodash';
// ...
// ...
如果我們在 src 下新建一個文件h.js
晚凿,再在index.js
中引入它,保存瘦馍,構(gòu)建之歼秽,我們發(fā)現(xiàn)有些沒改變的模塊的 hash 也發(fā)生了改變,這是因為加入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(), // 替換掉原來的`module.id`
new CleanWebpackPlugin(['dist']),
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor'
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'runtime'
})
// new webpack.HotModuleReplacementPlugin(),
// new webpack.NamedModulesPlugin()
],
// ...
};
再來執(zhí)行剛才那波操作底瓣,就會發(fā)現(xiàn)無關修改的模塊 hash 未變了谢揪。
Shimming
Tips
你可以將 shim 簡單理解為是用于兼容 API 的小型庫。
使用 jQuery 時我們習慣性地使用$
或jQuery
變量捐凭,每次都使用const $ = require(“jquery”)
引入的話太麻煩拨扶,如果能直接把這兩個變量設置為全局變量豈不美滋滋?這樣就可以在每個模塊中直接使用這兩個變量了茁肠。為了兼容這一做法患民,我們使用 ProvidePlugin 插件為我們完成這一任務。
$ 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({ // 設置全局變量
$: '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垦梆,點擊頁面按鈕匹颤,成功了。
另外托猩,如果你需要在某些模塊加載時設置該模塊的全局變量印蓖,請看 這里。
結(jié)尾的一點廢話
終于寫完了 :)京腥,也感謝你能耐心看到這里另伍。webpack 這個工具的配置還是有些麻煩的。但是呢,某人說這個東東前期會花比較多時間摆尝,后期會大大提高你的效率。所以呢因悲,還是拿下這個東東吧堕汞。有其他需求的話可以繼續(xù)看官方的文檔。遇到困難可以找:
我寫好的 demo 文件放在了這里晃琳。
Reference
- 入門 Webpack讯检,看這篇就夠了 - v1
- Webpack Guides
- Webpack: When To Use And Why
- stackoverflow / github issues