我們已經(jīng)了解了許多關(guān)于 Webpack 的知識(shí),但要完全熟練掌握它并非易事。一個(gè)很好的學(xué)習(xí)方法是通過實(shí)際項(xiàng)目練習(xí)。當(dāng)我們對(duì) Webpack 的配置有了足夠的理解后,就可以嘗試重構(gòu)一些項(xiàng)目。本次我選擇了一個(gè)純HTML/JS的PC項(xiàng)目進(jìn)行重構(gòu),項(xiàng)目位于 GitHub 上,非常感謝該項(xiàng)目的貢獻(xiàn)者。
重構(gòu)案例選擇了兩個(gè)頁(yè)面:首頁(yè) index.html 和購(gòu)物車頁(yè)面 cart.html。

項(xiàng)目目錄結(jié)構(gòu)清晰,根目錄包含了各個(gè) HTML 頁(yè)面,同一層級(jí)下還有 CSS、JS 和 IMG 文件夾。每個(gè) HTML 頁(yè)面對(duì)應(yīng)各自的 CSS 和業(yè)務(wù) JS 文件。

初始化 npm 項(xiàng)目
首先,創(chuàng)建一個(gè)新的空文件夾,并在其中運(yùn)行 npm init -y 命令來初始化項(xiàng)目。接著,在項(xiàng)目根目錄下創(chuàng)建 src 文件夾,將 index.html 文件復(fù)制到 src 目錄下,以此為基礎(chǔ)進(jìn)行重構(gòu)。打開 index.html 文件,可以看到頁(yè)面中引入了 CSS、圖片和 JS 資源。然后將 CSS、IMG 和 JS 文件夾也移至 src 目錄下。

隨后,我們觀察 index.html 文件中的 <link> 和 <script> 標(biāo)簽,它們分別用于加載外部的 CSS 文件和 JavaScript 文件。為了使項(xiàng)目更好地適應(yīng) Webpack 的模塊化打包機(jī)制,在 index.html 同一目錄級(jí)別的位置創(chuàng)建一個(gè)新的 index.js 文件。在這個(gè)新的 index.js 文件中,我們將使用模塊化的方式導(dǎo)入原本通過 <link> 標(biāo)簽引入的 CSS 文件以及通過 <script> 標(biāo)簽加載的 JavaScript 文件。
對(duì)于那些直接嵌入在 <script> 標(biāo)簽內(nèi)的腳本代碼,例如圖中提到的 flexslider 函數(shù),我們暫且保持其原樣,不做變動(dòng)。
import "./css/public.css";
import "./css/index.css";
import './js/jquery-1.12.4.min.js'
import './js/public.js';
import './js/nav.js';
import './js/jquery.flexslider-min.js';
初始化 webpack
使用命令 npm install webpack --save 來安裝 Webpack,并創(chuàng)建 webpack.config.js 文件來定義基本的配置。由于原項(xiàng)目包含多個(gè) HTML 頁(yè)面,因此這是一個(gè)多入口項(xiàng)目。
const path = require("path");
module.exports = {
mode: "development",
entry: {
index: "./src/index.js",
},
output: {
filename: "[name].[hash:8].js",
path: path.join(__dirname, "./dist"),
},
};
在 package.json 中添加 "build": "webpack" 命令。
處理css、圖片
Webpack 默認(rèn)不支持處理 CSS 和圖片資源。要處理 CSS 資源,可以通過 css-loader 和 style-loader;而圖片資源則可以通過 Webpack 5 的內(nèi)置功能——asset module 來處理。
首先,安裝處理 CSS 所需的依賴項(xiàng):
npm install css-loader style-loader --save
這里我們使用 css-loader 來解析 CSS 文件,并通過 style-loader 將其作為內(nèi)聯(lián)樣式插入到 DOM 中。初期階段,我們可以先這樣創(chuàng)建內(nèi)聯(lián)樣式,之后再考慮將 CSS 資源進(jìn)一步抽離優(yōu)化。
module.exports = {
module: {
rules: [
{ test: /\.css$/, use: ["style-loader", "css-loader"] },
{ test: /\.(jpg|jpeg|png|gif|svg)/i, type: "asset" },
],
},
}
處理 html
使用 html-webpack-plugin 插件根據(jù) index.html 創(chuàng)建壓縮后的 HTML 文件,并將編譯后的 JS 文件引入。
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
plugins: [
new HtmlWebpackPlugin({
template: "./src/index.html",
filename: "index.html",
}),
],
}
圖片資源
asset module 主要用于處理在 CSS 文件中通過背景圖像或其他方式引入的圖片資源。然而,對(duì)于 HTML 頁(yè)面中直接通過標(biāo)簽引入的資源,它則無能為力。

如圖所示,這些圖片的路徑都是 img/xxx.png。由于編譯后的文件位于 dist 文件夾下,而此時(shí) dist 文件夾下沒有 img 目錄。因此,我們可以通過 copy-webpack-plugin 將 src 目錄下的 img 文件夾復(fù)制到 dist 目錄下。
const CopyPlugin = require("copy-webpack-plugin");
module.exports = {
plugins: [
new CopyPlugin({
patterns: [
{
from: "./src/img",
to: "./img",
},
],
}),
],
};
這樣一來,當(dāng)我們執(zhí)行 npm run build 時(shí),dist 文件夾中已經(jīng)生成了 index.html 及其對(duì)應(yīng)的 CSS、JS 和圖片等資源。然而,當(dāng)我們嘗試從 index.html 打開頁(yè)面時(shí),卻發(fā)現(xiàn)頁(yè)面報(bào)錯(cuò)提示 $ 未定義,并且頁(yè)面底部定義的 flexslider 方法并未生效。

ProvidePlugin $ 符號(hào)
我們知道 $ 符號(hào)實(shí)際上是 jQuery 提供的一個(gè)全局變量。提示找不到 $ 符,意味著 jQuery 的全局變量尚未正確暴露。為了解決這個(gè)問題,我們可以按照以下步驟操作:
首先,通過安裝 jQuery:
npm install jquery --save
接著,調(diào)整 index.js 文件中的引入方式:
// 修改前
import './js/jquery-1.12.4.min.js'
// 修改后
import 'jquery';
然后,使用 ProvidePlugin 來定義 $ 的映射關(guān)系:
const webpack = require("webpack");
module.exports = {
plugins: [
new webpack.ProvidePlugin({
$: "jquery",
jQuery: "jquery",
}),
],
};
最后,將 index.html 文件底部通過 <script> 標(biāo)簽調(diào)用的 flexslider 函數(shù)代碼移動(dòng)到需要引入的業(yè)務(wù) JS 文件中。
完成上述步驟后,再次執(zhí)行 npm run build,原有的 index.html 功能就能實(shí)現(xiàn)基本的重構(gòu),接下來就可以進(jìn)行更多的優(yōu)化工作了。
自動(dòng)清空編譯后文件夾
在執(zhí)行 npm run build 時(shí),Webpack 會(huì)根據(jù) webpack.config.js 中的規(guī)則,在 dist 目錄下生成編譯后的文件。為了避免 dist 文件夾中生成的文件混雜在一起,通常我們需要在每次編譯前手動(dòng)清理該目錄。
為了省去這一手動(dòng)操作的麻煩,我們可以使用 clean-webpack-plugin 來自動(dòng)清空 dist 文件夾。這樣可以確保每次構(gòu)建時(shí),dist 目錄都是干凈的,從而避免舊文件的干擾。
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
module.exports = {
plugins: [
new CleanWebpackPlugin()
],
};
抽離css文件
這樣會(huì)導(dǎo)致 JS 文件體積過大,并且 JS 和 CSS 代碼混合在一起,不夠清晰。在開發(fā)環(huán)境中,這種方式是可行的,因?yàn)榫幾g速度快,但在生產(chǎn)環(huán)境中,我們需要將 CSS 資源抽離成單獨(dú)的文件。

為此,我們可以使用 mini-css-extract-plugin 替換掉 style-loader,以實(shí)現(xiàn) CSS 資源的獨(dú)立打包。
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
module: {
rules: [
{ test: /\.css$/, use: [MiniCssExtractPlugin.loader, "css-loader"] },
],
},
plugins: [
new MiniCssExtractPlugin({
filename: "[name].[hash:8].css",
chunkFilename: "[name].[hash:8].css",
}),
],
};
通過這種方式,CSS 資源會(huì)被單獨(dú)打包成一個(gè)文件,從而使得最終的輸出更加規(guī)范和高效。如圖所示,CSS 文件已經(jīng)被獨(dú)立出來。

js和css壓縮
在前面的配置中,mode 被設(shè)置為 development,這在開發(fā)模式下便于調(diào)試。然而,在代碼發(fā)布時(shí),我們需要切換到 production 模式。在這種模式下,Webpack 會(huì)自動(dòng)對(duì)資源文件進(jìn)行壓縮,以減小文件大小。
除了更改 mode 設(shè)置之外,我們還可以利用 terser-webpack-plugin 和 css-minimizer-webpack-plugin 分別對(duì) JavaScript 和 CSS 資源進(jìn)行進(jìn)一步的壓縮。
const TerserPlugin = require("terser-webpack-plugin");
const CssMinimizerWebpackPlugin = require("css-minimizer-webpack-plugin");
module.exports = {
mode: "production",
optimization: {
minimizer: [new TerserPlugin({}), new CssMinimizerWebpackPlugin()],
},
};
如圖所示,我們可以看到不同 mode 設(shè)置下,以及使用插件對(duì)代碼資源進(jìn)行壓縮后的文件體積變化。盡管當(dāng)前項(xiàng)目只有一個(gè)頁(yè)面,包含少量的 HTML、CSS 和 JS 文件,因此代碼壓縮的效果可能不是特別顯著,但隨著項(xiàng)目規(guī)模的擴(kuò)大,這種壓縮策略的效果將更加明顯。

增加開發(fā)模式
以上代碼的修改,我們都通過執(zhí)行 npm run build 來觀察編譯后的產(chǎn)物。然而,當(dāng)需要遷移多個(gè)文件時(shí),使用開發(fā)模式會(huì)更便于實(shí)時(shí)查看所有業(yè)務(wù)場(chǎng)景的使用情況。
為了實(shí)現(xiàn)這一點(diǎn),我們可以使用 webpack-dev-server 來啟動(dòng)一個(gè)開發(fā)服務(wù)器。安裝完成后,在 webpack.config.js 文件中增加 devServer 的配置:
module.exports = {
devServer: {
open: true,
compress: true,
port: 8000,
},
};
接著,在 package.json 文件中配置一個(gè)用于啟動(dòng)開發(fā)服務(wù)器的腳本指令:
"scripts": {
"dev": "webpack serve",
},
這樣一來,通過執(zhí)行 npm run dev 即可啟動(dòng)開發(fā)服務(wù)器,并自動(dòng)打開瀏覽器查看 index.html 頁(yè)面的內(nèi)容。這樣不僅方便調(diào)試,還能實(shí)時(shí)預(yù)覽代碼改動(dòng)的效果。
多入口
到目前為止,我們僅遷移了首頁(yè)的資源?,F(xiàn)在我們將繼續(xù)遷移購(gòu)物車頁(yè)面。與首頁(yè)的遷移類似,首先將 cart.html 文件復(fù)制到 src 目錄下,并查找其中引入的 CSS 和 JS 資源。

接著,創(chuàng)建一個(gè) cart.js 文件,并在其中引入所需的 JS 和 CSS 文件:
// cart.js
import "./css/public.css";
import "./css/proList.css";
import 'jquery';
import './js/public.js';
import './js/pro.js';
import './js/cart.js';
接下來的配置非常關(guān)鍵。我們需要在 webpack.config.js 中定義多入口,并為每個(gè)頁(yè)面生成相應(yīng)的模板 HTML 文件。這里需要注意的是,一定要定義 chunks 屬性,否則生成的 HTML 頁(yè)面會(huì)錯(cuò)誤地引入所有 CSS 和 JS 文件。
module.exports = {
entry: {
index: "./src/index.js",
cart: "./src/cart.js",
},
plugins: [
new HtmlWebpackPlugin({
template: "./src/index.html",
filename: "index.html",
chunks: ["index"],
}),
new HtmlWebpackPlugin({
template: "./src/cart.html",
filename: "cart.html",
chunks: ["cart"],
}),
],
};
完成上述配置后,再次執(zhí)行 npm run build,即可編譯出兩個(gè)頁(yè)面。此時(shí),在 dist 文件夾中直接點(diǎn)擊 cart.html 文件,也可以順利訪問頁(yè)面內(nèi)容。
拆分公共資源
盡管目前可以編譯出兩個(gè) HTML 頁(yè)面的資源,但如果查看 dist 文件夾下的 index.js 或者 cart.js 文件,會(huì)發(fā)現(xiàn)里面仍然包含有 jQuery 的代碼。

為了優(yōu)化這種情況,我們希望將像 jQuery 這樣的重復(fù)資源作為公共模塊來引用,而不是讓它們?cè)诓煌?JS 文件中反復(fù)編譯。以下是一個(gè)詳細(xì)的 splitChunks 配置示例,它可以將 node_modules 中的資源進(jìn)行分類處理,將 jQuery 編譯成單獨(dú)的文件,并將其他第三方庫(kù)編譯為另一個(gè)文件。
module.exports = {
optimization: {
splitChunks: {
chunks: "all",
name: "common",
cacheGroups: {
jquery: {
// 測(cè)試模塊是否包含 'jquery' 字符串
test: /[\\/]node_modules[\\/]jquery[\\/]/,
// 設(shè)置文件名
name: "jquery",
// 文件名可以是函數(shù)形式,也可以直接指定字符串
filename: "jquery.js",
// 確保只包含異步加載的 chunk 中的 jQuery
priority: 10, // 可以設(shè)置優(yōu)先級(jí)來控制合并順序
enforce: true, // 強(qiáng)制創(chuàng)建這個(gè) chunk 即使其他規(guī)則可能忽略它
},
vendors: {
// 這個(gè) cache group 用來處理其他第三方庫(kù)
test: /[\\/]node_modules[\\/]/,
name: "vendors",
priority: -10,
filename: "vendors.js",
chunks: "all",
},
},
},
},
};
由于當(dāng)前項(xiàng)目中只用到了 jQuery 這一資源,因此只有 jQuery 被單獨(dú)打包。隨著項(xiàng)目發(fā)展和資源的增加,可以進(jìn)一步細(xì)化拆分規(guī)則。從結(jié)果可以看出,當(dāng) jQuery 被拆分出來后,index.js 和 cart.js 的文件體積都有了顯著的減少。

模板文件ejs
在不同的頁(yè)面中,頁(yè)面頂部的導(dǎo)航通常是固定且相同的。在當(dāng)前項(xiàng)目中,相同的 HTML 部分是通過復(fù)制來定義的。為了提高代碼的復(fù)用性和維護(hù)性,我們可以使用 EJS(Embedded JavaScript)來將這部分相同的邏輯抽離出來。

首先,在 src 文件夾下創(chuàng)建一個(gè) ejs 文件夾,并在其中創(chuàng)建一個(gè) header.ejs 文件。找到定義 header 的代碼,將其復(fù)制到 header.ejs 文件中,并將變化的內(nèi)容(如頁(yè)面標(biāo)題)通過 <%= title %> 的方式定義。
然后,在原來 HTML 頁(yè)面中定義 header 代碼的地方引入 header.ejs 文件,并傳入動(dòng)態(tài)變量:
<%=require('./ejs/header.ejs')({ title: '首頁(yè)'})%>
由于 Webpack 本身不具備處理 EJS 文件的能力,因此我們需要安裝 ejs-loader 并配置相應(yīng)的處理規(guī)則:
module.exports = {
module: {
rules: [
{ test: /\.ejs/, loader: "ejs-loader", options: { esModule: false } },
],
},
};
通過這樣的配置,我們就實(shí)現(xiàn)了公共代碼的復(fù)用。
以上步驟完成了從純 HTML/JS 項(xiàng)目遷移到使用 Webpack 進(jìn)行開發(fā)的全過程。通過使用 Webpack,我們實(shí)現(xiàn)了代碼分割、資源按需加載,并采用了模塊化開發(fā)。借助 html-webpack-plugin 和 clean-webpack-plugin 等插件,簡(jiǎn)化了構(gòu)建流程,確保每次構(gòu)建都能得到干凈且優(yōu)化的輸出文件。
通過 EJS 抽象公共頭部等重復(fù)代碼片段,減少了冗余,提高了代碼復(fù)用率,使代碼庫(kù)更簡(jiǎn)潔。
如果你對(duì)前端、JavaScript 和工程化感興趣,快來瞅瞅我的其他文章吧~我會(huì)不定期分享各種學(xué)習(xí)心得和使用技巧。戳我的頭像,一起探索更多好玩的內(nèi)容吧!