下一篇:webpack代碼分離詳解
本文基于 webpack 4.44.1,用到的所有依賴及版本如下圖

一. 基本概念
1. 什么是webpack
Webpack是一個(gè)模塊化的打包工具,它根據(jù)模塊的依賴關(guān)系進(jìn)行靜態(tài)分析,然后將這些模塊按照指定的規(guī)則生成對應(yīng)的靜態(tài)資源

2. 理解module、chunk、bundle
在 webpack 中,一切皆module,任何一個(gè)文件都可以看成是module。js、css、圖片等都是module
webpack 會將入口文件以及它的依賴引入到一個(gè) chunk 中,然后進(jìn)過一系列處理打包成bundle

3. webpack的五大核心
(1) entry
webpack打包的入口,其取值可以是字符串,數(shù)組或者一個(gè)對象
// 單入口單文件
entry: "./src/index.js"
// 單入口多文件
entry: ["./ src/index.js", "./src/common.js"] // 若index.js與common.js沒有依賴關(guān)系,可以通過此方式將它們打包在一起
// 多入口
entry: {
page1: "./src/page1.js",
page2: "./src/page2.js"
}
(2) output
webpack打包的輸出,常用配置如下
output: {
path: path.resolve(__dirname, "./dist"),
// 單入口時(shí)(默認(rèn))
// filename: "main.js",
// filename: "js/main.js", // filename也可以寫路徑,表示輸出到 dist/js 目錄下
// 多入口時(shí),由于會有多個(gè)輸出,因此文件名不能寫死
filename: "[name].js", // name表示chunk的名稱,此處為entry中的key值
chunkFilename: "[name].js", // 按需加載的模塊打包后的名稱
publicPath: "/" // 項(xiàng)目部署在服務(wù)器上的路徑,如果在根路徑則為 /
}
(3) mode
webpack打包分為兩種模式,開發(fā)模式(development)與生產(chǎn)模式(production),默認(rèn)為生產(chǎn)模式
| 選項(xiàng) ?????? ?????????????????????????????????????????????????????????????????????????????????????? | 描述 |
|---|---|
| development | 會將 process.env.NODE_ENV 的值設(shè)為 development。啟用 NamedChunksPlugin 和 NamedModulesPlugin。 |
| production | 會將 process.env.NODE_ENV 的值設(shè)為 production。啟用 FlagDependencyUsagePlugin, FlagIncludedChunksPlugin, ModuleConcatenationPlugin, NoEmitOnErrorsPlugin, OccurrenceOrderPlugin, SideEffectsFlagPlugin 和 UglifyJsPlugin. |
(4) loader
webpack默認(rèn)只能處理js、json格式的文件,而loader的作用則是將其他格式的文件,轉(zhuǎn)換成webpack能夠處理的文件
使用loader需要在webpack配置文件的module.rules中配置
module.exports = {
entry: ...,
output: ...,
module: {
noParse: /node_modules/, //忽略解析 node_modules 中的文件
rules: [
{
test: /\.xxx$/, // 匹配后綴名為xxx的文件
// 單個(gè)loader
// loader: "xxx-loader",
// options: {},
// 多個(gè)loader,loader的處理順序?yàn)閺暮笸?,因此需要?yōu)先處理的loader放在數(shù)組最后面
// use: ["xxxx-loader", "xxx-loader"],
// 如果某個(gè)loader需要配置,寫成下面的格式
use: [
{
loader: "xxxx-loader",
options: {}
},
"xxx-loader"
],
include: [path.resolve(__dirname, "./src")], // 只解析src中的文件,可以是正則
exclude: [path.resolve(__dirname, "./library")], // 忽略library中的文件,可以是正則
// 當(dāng)多個(gè)規(guī)則同時(shí)匹配某類文件時(shí),可以使用enforce參數(shù)指定優(yōu)先級
enforce: "pre" // 優(yōu)先執(zhí)行該規(guī)則里的loader,post 最后執(zhí)行該規(guī)則里的loader
},
{
// 當(dāng)規(guī)則匹配時(shí),不再匹配后面的規(guī)則。例如某個(gè)文件匹配到了第一個(gè)規(guī)則,不再匹配后面規(guī)則
oneOf: [
{
test: /\.xxx$/,
use: "xxx-loader"
},
{
test: /\.xxx$/,
use: "xxx-loader"
}
]
}
]
}
}
(5) plugin
webpack插件,每一個(gè)插件都有一個(gè)特定的功能,它能處理loader無法處理的事情
插件的使用非常簡單,在plugins數(shù)組中添加插件的實(shí)例化對象即可
const xxxWebpackPlugin = require("xxx-webpack-plugin");
module.exports = {
entry: ...,
output: ...,
plugins: [
new xxxWebpackPlugin(),
new xxxxWebpackPlugin({
// 插件的配置項(xiàng)
})
]
}
二. webpack的簡單使用
1. 新建util.js,index.js,index.html,寫入如下內(nèi)容
// util.js
export const print = (str) => {
console.log(str);
};
// index.js
import {print} from "./util";
print("hello webpack");
// index.html
...
<!-- 在body中引入index.js -->
<body>
<script src="./index.js"></script>
</body>
...
2. 瀏覽器中打開index.html,發(fā)現(xiàn)控制臺報(bào)錯(cuò)

因?yàn)闉g覽器是不支持es6的模塊化語法的,這個(gè)時(shí)候webpack就可以發(fā)揮作用了
3. 使用 npm init 初始化一個(gè)項(xiàng)目,并安裝webpack
npm i webpack webpack-cli -g // 全局安裝
npm i webpack webpack-cli -D // 項(xiàng)目中安裝
4.將之前新建的三個(gè)文件拷貝到項(xiàng)目中,結(jié)構(gòu)如下

5. 在命令行中輸入webpack index.js

index.js表示入口文件,由于webpack的默認(rèn)輸出為dist/main.js,所以不用指定出口,也可以使用 webpack [entry] -o [output] 來指定出口
執(zhí)行命令后會發(fā)現(xiàn)左側(cè)生成了一個(gè)dist文件夾以及main.js文件
6. 將index.html中引入的index.js文件改為./dist/main.js文件,在瀏覽器中運(yùn)行

成功運(yùn)行,沒有報(bào)錯(cuò)
打開main.js會發(fā)現(xiàn),里面的代碼是被壓縮過的,如果不希望代碼被壓縮,可以在命令后面加上 --mode development,表示以開發(fā)模式進(jìn)行打包(開發(fā)模式不會壓縮代碼)
使用配置文件
每次打包時(shí)都輸入一長串命令非常繁瑣,可以在package.json中添加腳本,如下圖

然后通過npm run build:dev 或 npm run build:pro 打包(build:dev 與 build:pro 為自定義名稱)
webpack 打包時(shí)會默認(rèn)將項(xiàng)目根目錄下的 webpack.config.js 文件當(dāng)做配置文件,因此可以通過新建 webpack.config.js 文件來更改webpack的默認(rèn)配置
// webpack.config.js
module.exports = {
//mode: "development", // 因?yàn)樵谀_本指定了打包模式,所以無需設(shè)置
entry: "./src/index.js",
output: {
path: path.resolve(__dirname, "./dist"),
filename: "main.js",
}
}
三. HtmlWebpackPlugin 與 CleanWebpackPlugin
將 index.html 復(fù)制一份到 dist 目錄中去,并修改其引入的 main.js 路徑為正確的相對路徑
新建 server.js 文件,添加以下代碼
const express = require("express"); // 先安裝 npm i express -D
const path = require("path");
const app = express();
app.use(express.static(path.resolve(__dirname, "dist"), {maxAge: 3600000}))
app.listen(3000);
命令行輸入 node server.js,然后在瀏覽器打開 127.0.0.1:3000,結(jié)果如下

修改 index.js 中 print 函數(shù)的參數(shù)為 “hello node”,重新打包刷新頁面

打印結(jié)果并沒有更新,因?yàn)闉g覽器強(qiáng)緩存生效,沒有去請求新的文件。而緩存是基于 url 的,只要 url 變了,緩存就會失效,我們只需要修改 js 的文件名便可以修改對應(yīng)的 url,因此我們希望打包后的文件名根據(jù)文件內(nèi)容動態(tài)生成,此時(shí)我們可以修改配置為
module.exports = {
entry: "./src/index.js",
output: {
path: path.resolve(__dirname, "./dist"),
filename: "[chunkhash:10].js", // chunk內(nèi)容的hash值,后面會詳細(xì)介紹
}
}
打包刷新頁面(記得修改 ./dist/index.html 中引入的 js 文件名)

可以發(fā)現(xiàn),文件名發(fā)生了變化,緩存的問題也順利解決了。但是文件名的變化卻引發(fā)了兩個(gè)新的問題
- 每次打包都要手動修改 html 中引入的 js 文件名
- 新的文件不會覆蓋舊的文件,輸出目錄中的文件越來越多
HtmlWebpackPlugin 與 CleanWebpackPlugin 便可以為我們解決這兩個(gè)問題
npm i html-webpack-plugin clean-webpack-plugin -D
CleanWebpackPlugin:每次打包時(shí)自動清除舊的文件,默認(rèn)清除output.path目錄
HtmlWebpackPlugin:生成一個(gè)html文件,自動引入打包后的js文件
在config中添加plugins配置
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
module.export = {
...
plugins: [
new HtmlWebpackPlugin(),
new CleanWebpackPlugin()
]
}
最終打包生成的html如下圖,自動幫我們引入了js文件

HtmlWebpackPlugin 自動生成的html沒有任何內(nèi)容,如果需要按照已有的 html 來生成,可以給該插件指定模板,常用參數(shù)如下
module.exports = {
// 這里三個(gè)入口是為了解釋 HtmlWebpackPlugin 的 chunks 及 excludeChunks
entry: {
page1: "./src/page1.js",
page2: "./src/page2.js",
common: "./src/common.js"
},
plugins: [
new HtmlWebpackPlugin({
filename: "index.html", // 生成的 html 的文件名
template: "index.template.html", // 指定模板
title: "hello webpack", // 設(shè)置 html 的 title,可以在 html 中通過 ejs 語法引入
inject: true, // 默認(rèn)值,script標(biāo)簽位于 body 底部,可選值 body、header、false(表示不自動引入js)
hash: false, // true 表示引入的js文件后面添加 hash 值作為參數(shù),src="main.js?78ccc964740f25e35fca"
chunks: [page1, common], // 多入口打包會有多個(gè)文件,默認(rèn)引入全部,此配置表示只引入 page1, common
minify: {
collapseWhitespace: true, // 去除空格
minifyCSS: true, // 壓縮 html 內(nèi)聯(lián)的css
minifyJS: true, // 壓縮 html 內(nèi)聯(lián)的js
removeComments: true, // 移除注釋
}
}),
// 多頁面需要 new 多個(gè)對象
new HtmlWebpackPlugin({
...
excludeChunk: [page1], // 不需要引入page1,即只引入 page2 與
})
]
}
// index.html
...
<head>
<!-- 打包后會被替換成插件參數(shù)中的title — hello webpack -->
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
...

四.處理項(xiàng)目中的樣式文件
webpack 默認(rèn)是無法處理 css 文件的,因此需要使用 css-loader 來處理項(xiàng)目中的 css 文件
新建 css 文件,在 js 中引入,一定要在 js 中引入(無需在 html 中引入)
// index.css
body {
background-color: plum;
}
// index.js
...
import "./index.css"
安裝 css-loader,并在配置文件中添加配置
npm i css-loader -D
module.exports = {
...
module: {
rules: [
{
test: /\.css$/,
use: "css-loader"
}
]
}
}
執(zhí)行打包命令,在瀏覽器中打開 index.html 文件

如上圖所示,頁面背景色沒有改變,html 中也沒有任何樣式
css 雖然被打包到了 js 文件中,但并沒有作用到對應(yīng)的元素上,此時(shí),就需要 style-loader 來處理了
安裝 style-loader,繼續(xù)修改配置為
npm i style-loader -D
module: {
rules: [
{
test: /\.css$/,
use: ["style-loader", "css-loader"] // 從后往前,先執(zhí)行css-loader,后執(zhí)行style-loader
}
]
}
重新打包,刷新頁面,最終效果如下圖

由此可知,style-loader 的作用是將 css 插入到 head 里的 style 標(biāo)簽中
在實(shí)際的項(xiàng)目開發(fā)中,我們可能需要將 css 抽離成一個(gè)單獨(dú)的文件,然后通過 link 的方式引入,因此我們可以使用 MiniCssExtractPlugin 插件來完成此事
安裝 MiniCssExtractPlugin,并使用 MiniCssExtractPlugin.loader 替換 style-loader,修改配置如下
npm i mini-css-extract-plugin -D
output: {
path: path.resolve(__dirname, "./dist"),
filename: "[name].js" // 為了方便理解,我們把 [chunkhash:10].js 改為 [name].js,表示chunk 的名稱
},
module: {
rules: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, "css-loader"]
}
]
},
plugins: [
new MiniCssExtractPlugin(),
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: "index.html",
title: "hello webpack"
})
]
打包后生成的文件及頁面效果如下圖

css 被單獨(dú)打包到了 main.css(因?yàn)?index.css 與 index.js 屬于同一個(gè) chunk ,因此它們的chunkname 都是 main)中,并且被生成的 html 文件使用 link 引入(HtmlWebpackPlugin 插件幫我們做了此事)
為了減少文件體積,一般我們會對 css 文件進(jìn)行壓縮,使用 OptimizeCssAssetsWebpackPlugin 插件即可
安裝 OptimizeCssAssetsWebpackPlugin ,在配置中添加
npm i optimize-css-assets-webpack-plugin -D
plugins: [
...
new OptimizeCssAssetsWebpackPlugin()
]
為了演示效果,在 index.css 中隨便多加點(diǎn)樣式,然后打包,發(fā)現(xiàn)輸出的 css 文件內(nèi)容被壓縮到了一行

對于 .less、.scss 等樣式文件,只需要安裝對應(yīng)的 loader 并添加配置即可,以 less 為例
{
test: /\.less$/,
use: [MiniCssExtractPlugin.loader, "css-loader", "less-loader"]
}
五.處理項(xiàng)目中的圖片
與樣式文件一樣,webpack 默認(rèn)也無法處理圖片,因此需要對應(yīng)的 loader 來處理
安裝 file-loader,添加對應(yīng)配置
npm i file-loader -D
// 在module.rules中新增
{
test: /\.(png|jpe?g|gif|svg)$/,
use: {
loader: "file-loader",
options: {
name: "[name]-[contenthash:10].[ext]" // ext表示文件的后綴名
}
}
}
修改 index.html、index.css、index.js 文件并打包,內(nèi)容如下圖

預(yù)覽結(jié)果

從上面兩張圖可以看出,只有 css 引入的 bg.jpeg 被打包輸出,并且引入的圖片路徑也被替換成了打包后的路徑,js 及 html 直接引入的圖片皆存在問題
對于通過 js 動態(tài)添加的圖片,我們可以使用 require() 方法來引入
photo.src = require("./images/vue.jpeg");
打包預(yù)覽,結(jié)果如下

圖片被打包輸出了,但是 src 的路徑卻不正確,變成了一個(gè) Module 對象,那是因?yàn)?file-loader 處理后的圖片默認(rèn)是使用 ESModule 導(dǎo)出的,而我們卻是使用 commonJS 來引入的。有兩種方式解決這個(gè)問題
方式一: 修改 file-loader 參數(shù)(推薦,因?yàn)?html 中的處理也是使用 commonJS 引入)

方式二: 修改js中的代碼如下
photo.src = require("./images/vue.jpeg").default;
// 或者
import img from "./images/vue.jpeg"
photo.src = img;
對于 html 中的圖片,我們可以使用 html-loader 或 html-withimg-loader 來處理,以 html-loader 為例
安裝html-loader,并添加配置重新打包
npm i html-loader -D
{
test: /\.html$/,
use: "html-loader"
}

至此,css、js、html 中的圖片都能被正確打包處理,但是注意看 head 中的 title,并沒有被 htmlWebpackPlugin 解析
因?yàn)槭褂?html-loader 處理之后,htmlWebpackPlugin 無法解析 html 中的 ejs 語法,而是當(dāng)做 string 來輸出。所以當(dāng) html 中存在 ejs 語法時(shí),我們不能使用 html-loader 來處理圖片,而是直接使用 ejs 語法來引入
<img src="<%= require('./src/images/webpack.jpg') %>">
目前為止,我們所有的文件都是輸出到 dist 目錄下的,不便于之后的發(fā)布上線,因此,我們希望不同類型的文件輸出到不同的目錄中,例如 css 文件輸出到 style 目錄,圖片輸出到 images 目錄,js 輸出到 js 目錄...
修改 webpack 配置重新打包
// 修改 output
filename: "js/[name].js" // 修改前為 "[name].js"
// file-loader options 添加參數(shù)
outputPath: "images" // 輸出到 dist 目錄下的 images 中
// MiniCssExtractPlugin 添加參數(shù)
new MiniCssExtractPlugin({
filename: "style/[name].css"
})

可以看到,文件確實(shí)被打包到了對應(yīng)的目錄中,但是 css 引入的圖片卻沒有顯示,分析一下原因
圖片放在了 images 目錄,css 引入的圖片路徑前面也自動拼上了 images/,但是由于 css 放在了 style 目錄,導(dǎo)致 css 與 圖片的相對路徑發(fā)生了變化,因此 css 引入的圖片路徑錯(cuò)誤無法顯示
給 MiniCssExtractPlugin.loader 添加一個(gè)參數(shù)便可解決此問題
{
test: /\.css$/,
use: [{
loader: MiniCssExtractPlugin.loader,
options: {
publicPath: "../" // css 引入的資源路徑前面拼上../
}
}, "css-loader"]
}
再次打包看效果

ok!圖片展示的問題已經(jīng)完美解決了,接下來進(jìn)行些小優(yōu)化
- 使用 url-loader 代替 file-loader,url-loader 會將體積較小的圖片進(jìn)行 base64 編碼打包進(jìn)文件中,減少網(wǎng)絡(luò)請求次數(shù)
- 使用 image-webpack-loader 對圖片進(jìn)行壓縮
npm i url-loader image-webpack-loader -D
{
test: /\.(jpe?g|png|gif|svg)$/,
use: [{
loader: "url-loader",
options: {
name: "[name]-[contenthash:10].[ext]",
outputPath: "images",
limit: 10 * 1024, // 表示 小于10k 的圖片會被 base64 編碼
fallback: "file-loader", // 大于 10k 的圖片由 file-loader 處理,默認(rèn)值,可不設(shè)置
esModule: false
}
}, {
loader: "image-webpack-loader",
options: {
disabled: true // 在開發(fā)或使用webpack-dev-server時(shí),使用它可以加快初始編譯速度,并在較小程度上加快后續(xù)編譯速度(來自官方文檔)
}
}]
}

可以看到,只有 bg.jpeg(21k 被壓縮到了 19k) 被打包輸出,而 webpack.jpg(9.9k) 與 vue.jpeg(7k) 都被 base64 編碼直接打包進(jìn)了文件中,關(guān)于圖片的處理就到此為止
六.理解name、hash、chunkhash、contenthash
-
name:chunk name,chunk的名稱
多入口 chunkname 為其key值,單入口不指定key則默認(rèn)為main,異步加載的模塊默認(rèn)為數(shù)字

hash:每次打包生成的hash,見上圖
項(xiàng)目中任意與打包相關(guān)的文件內(nèi)容改變(包括配置文件),此hash就會改變chunkhash:根據(jù) chunk 的內(nèi)容生成的 hash
修改前面例子中的 js 及 css 輸出名稱為 [chunkhash:10],然后打包
output: {
filename: "js/[chunkhash:10].js", // :10 表示取前面10 位
path: path.resolve(__dirname, "./dist")
},
plugins: [
// ...
new MiniCssExtractPlugin({
filename: "style/[chunkhash:10].css"
})
]

index.js 中引入了 util.js 及 index.css,它們屬于同一個(gè) chunk,所以它們的 chunkhash 是一樣的,最終打包出來的文件名稱相同 ,只要其中一個(gè)文件內(nèi)容改變,其 chunkhash 就會改變。隨意修改 util.js 中的內(nèi)容重新打包,css 文件內(nèi)容并沒有變化,其文件名依然發(fā)生了改變
// util.js
export const print = str => {
console.log(str);
}
// 修改后
export const print = str => {
console.log(str);
console.log(123);
}

- contenthash:單個(gè)輸出文件內(nèi)容的 hash
我們將 js 及 css 的文件名從 chunkhash 改為 contenthash,打包結(jié)果如下

可以發(fā)現(xiàn),js 的文件名 與 css 的文件名并不一致,接下來我們還原 util.js 中的內(nèi)容再打包一次

最終,js 文件的名稱發(fā)生了變化,而 css 文件名并沒有改變,繼續(xù)修改 index.css 中的內(nèi)容
body {
/* background-color: plum; 修改前 */
background-color: peru; /* 修改后 */
}

較之上次,css 文件名發(fā)生了改變,而 js 文件名卻沒有變化
所以,contenthash 可以認(rèn)為是單個(gè)輸出文件內(nèi)容的 hash,或者說 bundle 的 hash
七. devServer 的使用
就目前來說,每次我們修改完代碼,都需要重新打包,然后刷新頁面才能看到最新的效果,這極大的影響了我們的開發(fā)效率。那有沒有辦法可以在每次修改后,自動打包并刷新頁面呢?答案肯定是有的,就是接下來我們要用到的devServer
webpack-dev-server 是 webpack 官方提供的一個(gè)小型 Express 服務(wù)器。使用它可以為 webpack 打包生成的資源文件提供web服務(wù)。
首先需要安裝 webpack-dev-server,接著在 package.json 中添加一個(gè)腳本
npm i webpack-dev-server -D
"scripts": {
...
"start": "webpack-dev-server"
}
在命令行執(zhí)行 npm start 即可(start 是一個(gè)特殊的命令,不同于其他命令,直接 npm start 就行,中間不需要 run),如下圖所示,啟動成功,打開 localhost:8081 便可訪問

并且我們發(fā)現(xiàn)dist目錄中沒有任何文件,那是因?yàn)?webpack-dev-server 將我們的代碼打包到了內(nèi)存中,并沒有輸出到指定目錄
修改項(xiàng)目中的 js、 css 或 html 文件然后保存,可以看到頁面會自動刷新展示最新效果(無法貼圖演示,可自行嘗試)
每次啟動項(xiàng)目時(shí)都需要手動打開瀏覽器,很不方便,其實(shí) devServer 是可以自動打開瀏覽器的,只需要一個(gè)簡單的配置
// webpack.config.js
module.exports = {
// ...
devServer: {
open: true, // 自動打開瀏覽器,下面三項(xiàng)不是必須的
host: "0.0.0.0", // 如果希望被局域網(wǎng)訪問,設(shè)為 0.0.0.0,默認(rèn) localhost
port: "8888", // 端口,默認(rèn) 8080
useLocalIp: true // 使用本地ip,如果 host 設(shè)置 0.0.0.0,請將此參數(shù)設(shè)為 true,否則結(jié)果就會像下面這樣
}
}

每次修改完內(nèi)容保存自動刷新頁面,看起來沒有什么問題,但是當(dāng)我們的項(xiàng)目很龐大的時(shí)候,就會顯得特別的慢,因此我們需要使用 devServer 提供的 HMR (Hot Module Replacement,熱模塊替換)技術(shù),在 devServer 中添加一個(gè)參數(shù)即可開啟熱更新
hot: true // webpack 會自動引入 HotModuleReplacementPlugin 插件
修改完配置需要重新啟動

熱更新已經(jīng)開啟,修改 js 文件,內(nèi)容會自動更新。但是修改 css,比如 body 的背景色

頁面并沒有熱更新,因?yàn)?MiniCssExtractPlugin.loader 默認(rèn)是不支持熱更新的,在開發(fā)中,我們可以將其修改為之前的 style-loader(默認(rèn)支持熱更新),或者修改其參數(shù)
{
loader: MiniCssExtractPlugin.loader,
options: {
publicPath: "../",
hmr: true // 開啟熱更新
}
}
重啟并修改 css 中 body 的背景色,會發(fā)現(xiàn)頁面自動更新了,并且只請求了與 css 相關(guān)的文件

html 一般不做熱更新,因?yàn)?html 變了,肯定整個(gè)頁面都需要刷新
devServer的其他常用配置
devServer: {
contentBase: path.resolve(__dirname, "public"), // 告訴服務(wù)器從哪里提供內(nèi)容,默認(rèn)為當(dāng)前工作目錄
wacthContentBase: true, // 監(jiān)視 contentBase 里面的內(nèi)容,一旦變化就 reload,
watchOptions: {
ignore: "", // 忽略哪些文件的變化
},
historyApiFallback: true, // 請求的資源不存在時(shí)返回 index.html,比如 vue-router 的 history 模式
clientLogLevel: "none", // 不要顯示啟動日志信息
overlay: false, // 如果出錯(cuò)了,不要全屏提示
progress: true, // 控制臺輸出運(yùn)行進(jìn)度
compress: true, // 啟用 gzip 壓縮
proxy: { // 設(shè)置代理
"/api": { // 當(dāng) url 中含有 /api 時(shí)就會使用這里設(shè)置的代理
target: "http://xxxx.com", // 目標(biāo)服務(wù)器地址
changeOrigin: true, // 修改請求頭中的host為target
secure: false, // https請求要加上這個(gè)
ws: true, // 代理websocket
pathRewrite: {
"^/api": "" // url 重寫,將 url 里面的 /api 去掉
}
}
}
}
為了更好的演示,后面的例子都不使用 devServer,繼續(xù)使用 webpack 手動打包
八. js兼容性處理
之前的案例都是在 chrome 瀏覽器上運(yùn)行的,接下來我們在某瀏覽器上試試

意料之中的報(bào)錯(cuò),根據(jù)錯(cuò)誤信息,知道是 util.js 文件中報(bào)的錯(cuò),因?yàn)槲覀冊?util.js 中使用了 es6 的 const 及箭頭函數(shù),而某瀏覽器(IE:看我作甚?)并不認(rèn)識這些高階語法
所以呢,我們就需要將這些高階語法,轉(zhuǎn)換成某瀏覽器能夠識別的語法,也就是所謂的 babel
需要安裝 babel-loader、
@babel/core(babel核心庫,核心 api 都在這里)、
@babel/preset-env(babel 預(yù)設(shè),babel 是插件化的,轉(zhuǎn)換不同的語法,需要不同的插件,預(yù)設(shè)的作用就是按需引入插件)
npm i babel-loader @babel/core @babel/preset-env -D
在配置文件中添加loader,重新打包
{
test: /\.js$/,
exclude: /node_modules/, // 不處理 node_modules 中的文件
use: {
loader: "babel-loader",
// 也可以在項(xiàng)目根目錄新建 .babelrc 文件,將配置寫入文件中
options: {
presets: ["@babel/preset-env"]
}
}
}

成功運(yùn)行,并且 const 轉(zhuǎn)化成了 var, 箭頭函數(shù)也被轉(zhuǎn)成了普通函數(shù)
接下來我們在 index.js 中添加一段代碼,打包后再次在ie中預(yù)覽
new Promise(resolve => {
setTimeout(() => {
resolve("promise resolve")
}, 1000);
}).then(res => {
console.log(res);
});

Promise未定義,promise 是 ES6 中新出的 API ,@babel/preset-env 只能轉(zhuǎn)換高階的語法,并不能轉(zhuǎn)換高階的 API ,因此我們就需要使用 @babel/polyfill 來處理這些高階的 API
@babel/polyfill 其實(shí)是 core-js2 與 regenerator-runtime 組成的一個(gè)集成包,使用 core-js2,則安裝 @babel/polyfill,使用 core-js3 則安裝 core-js 與 regenerator-runtime ,以 core-js3 為例
npm i core-js regenerator-runtime -S
修改 babel-loader 配置
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: [
["@babel/preset-env", {
modules: false, // 對ES6的模塊文件不做轉(zhuǎn)化,以便使用 tree shaking
useBuiltIns: "usage", // 取值可以是 false,"entry","usage"
corejs: 3, // corejs 版本號
targets: {} // 需要兼容的瀏覽器,若未配置,取 browserslist 中的值
}]
]
}
}
}
關(guān)于useBuiltIns取值的說明
false:需要在 js 文件頂部引入,不需要指定 corejs 版本號,會將整個(gè)內(nèi)容全部打包
// <--- core-js2 --->
// import "@babel/polyfill";
// <--- core-js3 --->
import "core-js/stable";
import "regenerator-runtime/runtime";
entry:需要在 js 文件頂部引入,需要指定 corejs 版本號,根據(jù)配置的瀏覽器,打包瀏覽器不兼容的內(nèi)容
usage:不需要在 js 文件頂部引入,需要指定corejs 版本號,根據(jù)配置的瀏覽器兼容性,以及代碼中用到的 API 來按需打包
本案例使用 usage,因此不需要在 js 文件頂部引入包,配置完成后直接打包刷新頁面,結(jié)果如下,沒有報(bào)錯(cuò),成功打印

當(dāng)項(xiàng)目里的 js 文件越來越多時(shí),babel 轉(zhuǎn)換耗時(shí)會越來越長,可以使用 babel 緩存及多進(jìn)程打包來提高速度,安裝 thread-loader,修改 babel 配置
{
test: /\.js$/,
exclude: /node_modules/,
use: [
{
loader: "thread-loader", // 耗時(shí)比較長的 loader 才需要多進(jìn)程,否則只會更慢
options: {
workers: 2 // 進(jìn)程數(shù) 2
}
},
{
loader: "babel-loader",
options: {
cacheDirectory: true, // 開啟babel 緩存,未修改的 js 文件直接取緩存
presets: [...]
}
}
]
}
九. tree shaking
移除文件中未被引用使用的代碼,它依賴于 ES6 模塊語法的靜態(tài)結(jié)構(gòu)特性,例如 import 和 export
滿足以下兩個(gè)條件即可 tree shaking
- 使用 es6 模塊化語法(使用 babel 時(shí)記得設(shè)置參數(shù) modules 為 false)
- 生產(chǎn)模式打包(默認(rèn)開啟 tree shaking)
為了演示效果,我們對代碼做如下修改
// util.js 由原本的 ESModule 導(dǎo)出改為 commonJS 導(dǎo)出,并隨便添加一個(gè)函數(shù)
exports.print = str => {
console.log(str);
}
exports.test = () => {
console.log("tree shaking");
}
// index.js 修改 print 函數(shù)的引入方式
const print = require("./util").print;
print("hello webpack")
util.js 中新增的 test 函數(shù)并未引入使用,然后使用生產(chǎn)模式打包,在打包生成的 main.js 中搜索 "tree shaking"

可以看到,雖然 test 函數(shù)并未引入使用,但是依然被打包進(jìn)來了。將模塊化語法改為 ESModule,重新打包并搜索 "tree shaking"
// util.js
export const print = str => {...}
export const test = () => {...}
// index.js
import {print} from "./util"; // 只引入 print
print("hello webpack")
結(jié)果如下圖,main.js 中搜索不到 "tree shaking",說明 test 函數(shù)并未被打包進(jìn)來

如果只是引入,并未使用,依然會被移除,例如下面的代碼,test 未被打包進(jìn)文件
import {print, test} from "./util"; // 兩個(gè)都引入,但是不使用 test
sideEffects:哪些文件具有副作用,配合 tree shaking 使用
所謂副作用是指在導(dǎo)入時(shí)會執(zhí)行特殊行為的代碼,就比如 css 文件,只要導(dǎo)入,勢必影響樣式
tree shaking 只會對沒有副作用的文件生效,例如我們將所有文件都設(shè)置為無副作用
// package.json
{
"sideEffects": false // 表示所有文件都沒有副作用
}
生產(chǎn)模式打包然后刷新頁面,會發(fā)現(xiàn)所有的樣式都沒有了,即 css 被 tree shaking 了

項(xiàng)目開發(fā)時(shí),如果代碼確實(shí)有一些副作用,將其文件路徑放入 sideEffects 數(shù)組中,以防被 tree shaking,可以是絕對路徑、相對路徑或通配符,例如:
"sideEffects": [
"*.css",
"./src/someSideEffectfulFile.js"
]
網(wǎng)絡(luò)資料顯示 sideEffects 默認(rèn)值為 true,即所有文件都有副作用,但是上面的例子中,明顯 util.js 被認(rèn)為沒有副作用,所以默認(rèn)值是啥暫不清楚,而且就算手動設(shè)置為 true,util.js 還是會被 tree shaking(不知道是否與 webpack 版本有關(guān))
十. 其他配置
1.resolve:設(shè)置模塊如何被解析
alias:為路徑設(shè)置別名,讓引入變得更簡單
// webpack.config.js
module.exports = {
resolve: {
alias: {
"@": path.resolve(__dirname, "src"),
// $ 匹配結(jié)尾,不會影響 vue/xxx 的路徑,表示項(xiàng)目中引入的 vue 為運(yùn)行時(shí)版本
"vue$": "vue/dist/vue.runtime.js"
}
}
}
// 例如某 .vue 文件中引入的一個(gè)組件為
import xxx from "../../components/xxx";
// 則可寫為
import xxx from "@/components/xxx";
extensions:引入哪些類型的文件時(shí)可以省略后綴名
resolve: {
// 默認(rèn)值為 [".js", ".json"]
extensions: [".js", ".json", ".vue"] // 引入 js、json、vue 文件時(shí)不需要寫后綴名
}
modules:解析模塊時(shí)應(yīng)該搜索的目錄
resolve: {
// 默認(rèn)值:["node_modules"],表示從當(dāng)前目錄的 node_modules 中查找,不存在時(shí)則去上級目錄的 node_modules 中查找,一直到根目錄為止。
// 若設(shè)置為絕對路徑,則只在指定的目錄中查找
modules: [path.resolve(__dirname, "src"), "node_modules"]
}
2. externals:設(shè)置某些庫不被打包,而是運(yùn)行時(shí)去外部獲取
// index.js 添加代碼
import $ from "jquery";
$("p").css("color", "red");
在 index.js 中引入 jquery,最終會被打包進(jìn) main.js 中,導(dǎo)致整個(gè)項(xiàng)目體積過大,使用 externals 排除 jquery 打包,并通過 cdn 引入(index.js 中的引入要保留,不能刪除)
// index.html 中 body 底部添加
<body>
<% for (let src of htmlWebpackPlugin.options.cdnList) { %>
<script src="<%= src %>"></script>
<% } %>
<!-- 直接寫死,與上面二選一,推薦上面的方式 -->
<script src="https://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
</body>
// webpack.config.js 添加下面內(nèi)容
module.exports = {
externals: {
jquery: "jQuery" // key 為引入的包名,value 為全局變量名
},
plugins: [
new HtmlWebpackPlugin({
// 用來在 html 中通過 script 引入,cdnList 為自定義變量。若直接寫死則不需要此配置
cdnList: ["https://libs.baidu.com/jquery/2.0.0/jquery.min.js"]
})
]
}
3. watch:初始構(gòu)建之后,繼續(xù)監(jiān)聽任何已解析文件的更改
將此參數(shù)設(shè)置為 true,使用 webpack 打包之后,每當(dāng)有文件內(nèi)容發(fā)生變化,就會自動重新打包(devServer默認(rèn)開啟)
module.exports = {
watch: true,
watchOptions: {
aggregateTimeout: 300, // 延時(shí) 300ms 打包,防抖
ignored: /node_modules/, // 忽略監(jiān)聽,也可以是 anymatch 模式,例: "files/**/*.js"
poll: true // 開啟輪詢模式,如果值為數(shù)字,表示輪詢的間隔,單位毫秒
}
}
4. devtool:控制如何生成 source map
source map 是一個(gè)信息文件,保存源代碼與打包后代碼的映射關(guān)系,幫助我們快速定位報(bào)錯(cuò)的代碼
在 index.js 中添加一行報(bào)錯(cuò)的代碼,比如打印一個(gè)未聲明的變量 a,生產(chǎn)模式打包,報(bào)錯(cuò)信息如下圖,左邊未使用 source map,很明顯,無法準(zhǔn)確定位到報(bào)錯(cuò)的代碼,而右邊直接映射到了打包前的代碼

關(guān)于不同 source map 之間的區(qū)別,請自行百度,不做闡述
下一篇:webpack代碼分離詳解