Update (2017.8.27) : 關於 output.publicPath、 devServer.contentBase、 devServer.publicPath的區別。如下:
output.publicPath: 對於這個選項,我們無需關注什麼絕對相對路徑,因為兩種路徑都可以。我們只需要知道一點:這個選項是指定 HTML 文件中資源文件 (字體、圖片、JS文件等) 的 文件名的公共 URL 部分的。在實際情況中,我們首先會通過 output.filename或有些 loader 如 file-loader的 name屬性設置 文件名的原始部分,webpack 將 文件名的原始部分和公共部分結合之後,HTML 文件就能獲取到資源文件了。
devServer.contentBase: 設置靜態資源的根目錄, html-webpack-plugin生成的 html 不是靜態資源。當用 html 文件裡的地址無法找到靜態資源文件時就會去這個目錄下去找。
devServer.publicPath: 指定瀏覽器上訪問所有 打包(bundled)文件 (在 dist裡生成的所有文件) 的根目錄,這個根目錄是相對伺服器地址及埠的,比 devServer.contentBase和 output.publicPath優先。
前言如果你用過 webpack 且一直用的是 webpack 1,請參考 從v1遷移到v2 (v2 和 v3 差異不大) 對版本變更的內容進行適當的了解,然後再選擇性地閱讀本文。
首先,這篇文章是根據當前最新的 webpack 版本 (即 v3.4.1) 撰寫,較長一段時間內無需擔心過時的問題。其次,這應該會是一篇極長的文章,涵蓋了基本的使用方法,有更高級功能的需求可以參考官方文檔繼續學習。再次,即使是基本的功能,也內容繁多,我儘可能地解釋通俗易懂,將我學習過程中的疑惑和坑一一解釋,如有紕漏,敬請雅正。再次,為了清晰有效地講解,我會演示從零編寫 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),能夠將任何資源如 JavaScript 文件、CSS 文件、圖片等打包成一個或少數文件。
為什麼要用介個 Webpack?首先,定義已經說明了 webpack 能將多個資源模塊打包成一個或少數文件,這意味著與以往的發起多個 HTTP 請求來獲得資源相比,現在只需要發起少量的 HTTP 請求。
想了解合併 HTTP 請求的意義,請見這裡:https://www.zhihu.com/question/34401250?from=profilequestioncard
其次,webpack 能將你的資源轉換為最適合瀏覽器的「格式」,提升應用性能。比如只引用被應用使用的資源 (剔除未被使用的代碼),懶加載資源 (只在需要的時候才加載相應的資源)。再次,對於開發階段,webpack 也提供了實時加載和熱加載的功能,大大地節省了開發時間。除此之外,還有許多優秀之處之處值得去挖掘。不過,webpack 最核心的還是打包的功能。
webpack,gulp/grunt,npm,它們有什麼區別?webpack 是模塊打包器(module bundler),把所有的模塊打包成一個或少量文件,使你只需加載少量文件即可運行整個應用,而無需像之前那樣加載大量的圖片,css文件,js文件,字體文件等等。而gulp/grunt 是自動化構建工具,或者叫任務運行器(task runner),是把你所有重複的手動操作讓代碼來做,例如壓縮JS代碼、CSS代碼,代碼檢查、代碼編譯等等,自動化構建工具並不能把所有模塊打包到一起,也不能構建不同模塊之間的依賴圖。兩者來比較的話,gulp/grunt 無法做模塊打包的事,webpack 雖然有 loader 和 plugin可以做一部分 gulp/grunt 能做的事,但是終究 webpack 的插件還是不如 gulp/grunt 的插件豐富,能做的事比較有限。於是有人兩者結合著用,將 webpack 放到 gulp/grunt 中用。然而,更好的方法是用 npm scripts 取代 gulp/grunt,npm 是 node 的包管理器 (node package manager),用於管理 node 的第三方軟體包,npm 對於任務命令的良好支持讓你最終省卻了編寫任務代碼的必要,取而代之的,是老祖宗的幾個命令行,僅靠幾句命令行就足以完成你的模塊打包和自動化構建的所有需求。
準備開始先來看看一個 webpack 的一個完備的配置文件,是 介樣 的,當然啦,這裡面有很多配置項是即使到這個軟體被廢棄你也用不上的:),所以無需擔心。
基本配置開始之前,請確定你已經安裝了當前 Node 的較新版本。
然後執行以下命令以新建我們的 demo 目錄:
$ mkdir webpack-demo && cd webpack-demo && npm init -y
$ npm i --save-dev webpack
$ mkdir src && cd src && touch index.js
我們使用工具函數庫 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());
import 和 export 已經是 ES6 的標準,但是仍未得到大多數瀏覽器的支持 (可喜的是, Chrome 61 已經開始默認支持了,見 ES6 modules),不過 webpack 提供了對這個特性的支持,但是除了這個特性,其他的 ES6 特性並不會得到 webpack 的特別支持,如有需要,須藉助 Babel 進行轉譯 (transpile)。
然後新建發布版本目錄:
$ 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>
現在,我們運行 webpack 來打包 index.js 為 bundle.js,本地安裝了 webpack 後可以通過 node_modules/.bin/webpack 來訪問 webpack 的二進位版本。
$ cd ..
$ ./node_modules/.bin/webpack src/index.js dist/bundle.js # 第一個參數是打包的入口文件,第二個參數是打包的出口文件
咻咻咻,大致如下輸出一波:
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
現在,你已經得到了你的第一個打包文件 (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') // 輸出文件所在的目錄
}
};
執行:
$ ./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"
},
...
}
在 npm scripts 中我們可以通過包名直接引用本地安裝的 npm 包的二進位版本,而無需編寫包的整個路徑。
執行:
$ npm run build
一波輸出後便得到了打包文件。
bulid 並不是 npm scripts 的內置屬性,需要使用 npm run 來執行腳本,詳情見 npm run。
打包其他類型的文件因為其他文件和 JS 文件類型不同,要把他們加載到 JS 文件中就需要經過加載器 (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: [ // 用於規定在不同模塊被創建時如何處理模塊的規則數組
{
test: /\.css$/, // 匹配特定文件的正則表達式或正則表達式數組
use: [ // 應用於模塊的 loader 使用列表
'style-loader',
'css-loader'
]
}
]
}
};
我們來創建一個 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());
執行 npm run build,然後打開 index.html,就可以看到紅色的字體了。CSS 文件此時已經被打包到 bundle.js 中。再打開瀏覽器控制臺,就可以看到 webpack 做了些什麼。
加載圖片$ npm install --save-dev file-loader
file-loader 指示 webpack 以文件格式發出所需對象並返回文件的公共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'
]
},
{ // 增加加載圖片的規則
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之。現在你可以看到單獨的圖片和以圖片為基礎的背景圖了。
加載字體加載字體用的也是 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'
]
},
{ // 增加加載字體的規則
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;
}
運行打包命令之後便可以看到打包好的文件和發生改變的頁面。
加載 JSON 文件因為 webpack 對 JSON 文件的支持是內置的,所以可以直接添加。
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。
輸出管理前面我們只有一個輸入文件,但現實是我們往往有不止一個輸入文件,這時我們就需要輸入多個入口文件並管理輸出文件。我們在 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', // 根據入口起點名動態生成 bundle 名,可以使用像 "js/[name]/bundle.js" 這樣的文件夾結構
path: path.resolve(__dirname, 'dist')
},
// ...
};
filename:'[name].bundle.js'中的 [name]會替換為對應的入口起點名,其他可用的替換請參見 output.filename。
現在可以打包文件了。但是如果我們修改了入口文件名或增加了入口文件, index.html是不會自動引用新文件的,而手動修改實在太挫。是時候使用插件 (plugin) 來完成這一任務了。我們使用 HtmlWebpackPlugin 自動生成 html 文件。
loader 和 plugin,有什麼區別?loader (加載器),重在「加載」二字,是用於預處理文件的,只用於在加載不同類型的文件時對不同類型的文件做相應的處理。而 plugin (插件),顧名思義,是用來增加 webpack 的功能的,作用於整個 webpack 的構建過程。在 webpack 這個大公司中,loader 是保安大叔,負責對進入公司的不同人員的處理,而 plugin 則是公司裡不同職位的職員,負責公司裡的各種不同業務,每增加一種新型的業務需求,我們就需要增加一種 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: [ // 插件屬性,是插件的實例數組
new HtmlWebpackPlugin({
title: 'webpack demo', // 生成 HTML 文檔的標題
filename: 'index.html' // 寫入 HTML 文件的文件名,默認 `index.html`
})
],
// ...
};
你可以先把 dist 文件夾的 index.html文件刪除,然後執行打包命令。咻咻咻,我們看到 dist 目錄下已經自動生成了一個 index.html文件,但即使不刪除原先的 index.html,該插件默認生成的 index.html也會替換原本的 index.html。
此刻,當你細細觀察 dist 目錄時,雖然現在生成了新的打包文件,但原本的打包文件 bundle.js及其他不用的文件仍然存在在 dist 目錄中,所以在每次構建前我們需要晴空 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']) // 第一個參數是要清理的目錄的字符串數組
],
// ...
};
打包之,現在,dist 中只存在打包生成的文件。
開發環境webpack 提供了很多便於開發時使用的功能,來一一看看吧。
使用代碼映射 (source map)當你的代碼被打包後,如果打包後的代碼發生了錯誤,你很難追蹤到錯誤發生的原始位置,這個時候,我們就需要代碼映射 (source map) 這種工具,它能將編譯後的代碼映射回原始的源碼,你的錯誤是起源於打包前的 b.js的某個位置,代碼映射就能告訴你錯誤是那個模塊的那個位置。webpack 默認提供了 10 種風格的代碼映射,使用它們會明顯影響到構建 (build) 和重構建 (rebuild,每次修改後需要重新構建) 的速度,十種風格的差異可以參看 devtool。關於如何選擇映射風格可以參看 Webpack devtool source map。這裡,我們為了準確顯示錯誤位置,選擇速度較慢的 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'
},
// ...
};
現在來手動製造一些錯誤:
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)
現在,我們很清楚哪裡發生了錯誤,然後輕鬆地改正之。
使用 webpack-dev-server你一定有這樣的體驗,開發時每次修改代碼保存後都需要重新手動構建代碼並手動刷新瀏覽器以觀察修改效果,這是很麻煩的,所以,我們要實時加載代碼。可喜的是,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') // 設置靜態資源的根目錄
},
// entry: './src/index.js',
entry: {
app: './src/index.js',
print: './src/print.js'
},
// ...
};
package.json
{
...
"scripts": {
"build": "webpack",
"start": "webpack-dev-server --open"
},
...
}
使用 webpack-dev-server 時,webpack 並沒有將所有生成的文件寫入磁碟,而是放在內存中,提供更快的內存內訪問,便於實時更新。
現在,可以直接運行 npm start ( start是 npm scripts 的內置屬性,可直接運行),然後瀏覽器自動加載應用的頁面,默認在 localhost:8080顯示。
模塊熱替換 (HMR, Hot Module Replacement)webpack 提供了對模塊熱替換 (或者叫熱加載) 的支持。這一特性能夠讓應用運行的時候替換、增加或刪除模塊,而無需進行完全的重載。想進一步地了解其工作機理,可以參見 Hot Module Replacement,但這並不是必需的,你可以選擇跳過機理部分繼續往下閱讀。
模塊熱替換(HMR)只更新發生變更(替換、添加、刪除)的模塊,而無需重新加載整個頁面(實時加載,LiveReload),這樣可以顯著加快開發速度,一旦打開了 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 便於調用其內置插件
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 默認使用模塊的數字 ID 指代模塊,不便於 debug,這個插件可以將其替換為模塊的真實路徑
],
// ...
};
webpack-dev-server 會為每個入口文件創建一個客戶端腳本,這個腳本會監控該入口文件的依賴模塊的更新,如果該入口文件編寫了 HMR 處理函數,它就能接收依賴模塊的更新,反之,更新會向上冒泡,直到客戶端腳本仍沒有處理函數的話,webpack-dev-server 會重新加載整個頁面。如果入口文件本身發生了更新,因為向上會冒泡到客戶端腳本,並且不存在 HMR 處理函數,所以會導致頁面重載。
我們已經開啟了 HMR 的功能,HMR 的接口已經暴露在 module.hot屬性之下,我們只需要調用 HMR API即可實現熱加載。當「被加載模塊」發生改變時,依賴該模塊的模塊便能檢測到改變並接收改變之後的模塊。
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() { // 接受給定依賴模塊的更新,並觸發一個回調函數來對這些更新做出響應
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.
webpack-dev-server 在 inline mode (此為默認模式) 時,會為每個入口起點 (entry) 創建一個客戶端腳本,所以你會在上面的輸出中看到有些信息重複輸出兩次。
但是當你點擊頁面的按鈕時,你會發現控制臺輸出的是舊的 printMe函數輸出的信息,因為 onclick事件綁定的仍是原始的 printMe函數。我們需要在 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);
});
}
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;
}
可以發現在不重載頁面的前提下我們對樣式的修改進行了熱加載,棒!
生產環境自動方式我們只需要運行 webpack-p (相當於 webpack--optimize-minimize--define process.env.NODE_ENV="'production'")這個命令,便可以自動構建生產版本的應用,這個命令會完成以下步驟:
使用 UglifyJsPlugin (webpack.optimize.UglifyJsPlugin) 壓縮 JS 文件 (此插件和 uglifyjs-webpack-plugin 相同)
運行 LoaderOptionsPlugin 插件,這個插件是用來遷移的,見 document
設置 NodeJS 的環境變量,觸發某些 package 包以不同方式編譯
值得一提的是, webpack-p設置的 process.env.NODE_ENV環境變量,是用於編譯後的代碼的,只有在打包後的代碼中,這一環境變量才是有效的。如果在 webpack 配置文件中引用此環境變量,得到的是 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"
},
...
}
現在可以在配置文件中使用 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()
],
// ...
};
[chunkhash]不能和 HMR 一起使用,換句話說,不應該在開發環境中使用 [chunkhash] (或者 [hash]),這會導致許多問題。詳情見 #2393 和 #377。
build 之,我們得到了生產版本的壓縮好的打包文件。
多配置文件配置有時我們會需要為不同的環境配置不同的配置文件,可以選擇 簡易方法,這裡我們採用較為先進的方法。先準備一個基本的配置文件,包含了所有環境都包含的配置,然後用 webpack-merge 將它和特定環境的配置文件合併並導出,這樣就減少了基本配置的重複。
$ 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"
},
...
}
現在只需執行 npm run build:dev或 npm run build:prod便可以得到開發版或者生產版了!
webpack 命令行選項見 Command Line Interface。
代碼分離入口分離我們先創建一個新文件:
$ 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之,我們發現用入口分離的代碼得到了兩個大文件,這是因為兩個入口文件都引入了 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 之,可以看到結果中包含以下部分:
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分離出來了。
動態引入我們還可以選擇以動態引入的方式來實現代碼分離,藉助 import() 實現之。
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() {
// 此函數原來的內容全部注釋掉...
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);
});
注意上面中的 /* webpackChunkName: "lodash" */這段注釋,它並不是可有可無的,它能幫助我們結合 output.chunkFilename把分離出的模塊最終命名為 lodash.bundle.js而非 [id].bundle.js。
現在 build 之看看吧。
結尾的一點廢話終於寫完了 :),也感謝你能耐心看到這裡。webpack 這個工具的配置還是有些麻煩的。但是呢,某人說這個東東前期會花比較多時間,後期會大大提高你的效率。所以呢,還是拿下這個東東吧。有其他需求的話可以繼續看官方的文檔。遇到困難可以找:
Stack Overflow
Gitter
Webpack Issues
我寫好的 demo 文件放在了這裡:https://github.com/yangkean/webpack-demo
Reference相關文章推薦
從 0 到 1 搭建 webpack2+vue2 自定義模板詳細教程
webpack多頁應用架構系列(一):一步一步解決架構痛點
入門 Webpack,看這篇就夠了