我們已經瞭解了許多關於 Webpack 的知識,但要完全熟練掌握它並非易事。一個很好的學習方法是透過實際專案練習。當我們對 Webpack 的配置有了足夠的理解後,就可以嘗試重構一些專案。本次我選擇了一個純HTML/JS的PC專案進行重構,專案位於 GitHub 上,非常感謝該專案的貢獻者。
重構案例選擇了兩個頁面:首頁 index.html 和購物車頁面 cart.html。
專案目錄結構清晰,根目錄包含了各個 HTML 頁面,同一層級下還有 CSS、JS 和 IMG 資料夾。每個 HTML 頁面對應各自的 CSS 和業務 JS 檔案。
初始化 npm 專案
首先,建立一個新的空資料夾,並在其中執行 npm init -y
命令來初始化專案。接著,在專案根目錄下建立 src 資料夾,將 index.html
檔案複製到 src 目錄下,以此為基礎進行重構。開啟 index.html
檔案,可以看到頁面中引入了 CSS、圖片和 JS 資源。然後將 CSS、IMG 和 JS 資料夾也移至 src 目錄下。
隨後,我們觀察 index.html
檔案中的 <link>
和 <script>
標籤,它們分別用於載入外部的 CSS 檔案和 JavaScript 檔案。為了使專案更好地適應 Webpack 的模組化打包機制,在 index.html
同一目錄級別的位置建立一個新的 index.js
檔案。在這個新的 index.js
檔案中,我們將使用模組化的方式匯入原本透過 <link>
標籤引入的 CSS 檔案以及透過 <script>
標籤載入的 JavaScript 檔案。
對於那些直接嵌入在 <script>
標籤內的指令碼程式碼,例如圖中提到的 flexslider
函式,我們暫且保持其原樣,不做變動。
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
,並建立 webpack.config.js
檔案來定義基本的配置。由於原專案包含多個 HTML 頁面,因此這是一個多入口專案。
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
預設不支援處理 CSS 和圖片資源。要處理 CSS 資源,可以透過 css-loader
和 style-loader
;而圖片資源則可以透過 Webpack 5 的內建功能——asset module
來處理。
首先,安裝處理 CSS 所需的依賴項:
npm install css-loader style-loader --save
這裡我們使用 css-loader
來解析 CSS 檔案,並透過 style-loader
將其作為內聯樣式插入到 DOM 中。初期階段,我們可以先這樣建立內聯樣式,之後再考慮將 CSS 資源進一步抽離最佳化。
module.exports = {
module: {
rules: [
{ test: /\.css$/, use: ["style-loader", "css-loader"] },
{ test: /\.(jpg|jpeg|png|gif|svg)/i, type: "asset" },
],
},
}
處理 html
使用 html-webpack-plugin
外掛根據 index.html
建立壓縮後的 HTML 檔案,並將編譯後的 JS 檔案引入。
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
plugins: [
new HtmlWebpackPlugin({
template: "./src/index.html",
filename: "index.html",
}),
],
}
圖片資源
asset module
主要用於處理在 CSS 檔案中透過背景影像或其他方式引入的圖片資源。然而,對於 HTML 頁面中直接透過標籤引入的資源,它則無能為力。
如圖所示,這些圖片的路徑都是 img/xxx.png。由於編譯後的檔案位於 dist 資料夾下,而此時 dist 資料夾下沒有 img 目錄。因此,我們可以透過 copy-webpack-plugin
將 src 目錄下的 img 資料夾複製到 dist 目錄下。
const CopyPlugin = require("copy-webpack-plugin");
module.exports = {
plugins: [
new CopyPlugin({
patterns: [
{
from: "./src/img",
to: "./img",
},
],
}),
],
};
這樣一來,當我們執行 npm run build
時,dist 資料夾中已經生成了 index.html 及其對應的 CSS、JS 和圖片等資源。然而,當我們嘗試從 index.html
開啟頁面時,卻發現頁面報錯提示 $
未定義,並且頁面底部定義的 flexslider
方法並未生效。
ProvidePlugin $ 符號
我們知道 $
符號實際上是 jQuery
提供的一個全域性變數。提示找不到 $
符,意味著 jQuery
的全域性變數尚未正確暴露。為了解決這個問題,我們可以按照以下步驟操作:
首先,透過安裝 jQuery
:
npm install jquery --save
接著,調整 index.js 檔案中的引入方式:
// 修改前
import './js/jquery-1.12.4.min.js'
// 修改後
import 'jquery';
然後,使用 ProvidePlugin
來定義 $ 的對映關係:
const webpack = require("webpack");
module.exports = {
plugins: [
new webpack.ProvidePlugin({
$: "jquery",
jQuery: "jquery",
}),
],
};
最後,將 index.html
檔案底部透過 <script>
標籤呼叫的 flexslider
函式程式碼移動到需要引入的業務 JS 檔案中。
完成上述步驟後,再次執行 npm run build
,原有的 index.html
功能就能實現基本的重構,接下來就可以進行更多的最佳化工作了。
自動清空編譯後資料夾
在執行 npm run build
時,Webpack
會根據 webpack.config.js
中的規則,在 dist
目錄下生成編譯後的檔案。為了避免 dist
資料夾中生成的檔案混雜在一起,通常我們需要在每次編譯前手動清理該目錄。
為了省去這一手動操作的麻煩,我們可以使用 clean-webpack-plugin
來自動清空 dist
資料夾。這樣可以確保每次構建時,dist
目錄都是乾淨的,從而避免舊檔案的干擾。
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
module.exports = {
plugins: [
new CleanWebpackPlugin()
],
};
抽離css檔案
這樣會導致 JS 檔案體積過大,並且 JS 和 CSS 程式碼混合在一起,不夠清晰。在開發環境中,這種方式是可行的,因為編譯速度快,但在生產環境中,我們需要將 CSS 資源抽離成單獨的檔案。
為此,我們可以使用 mini-css-extract-plugin
替換掉 style-loader
,以實現 CSS 資源的獨立打包。
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 資源會被單獨打包成一個檔案,從而使得最終的輸出更加規範和高效。如圖所示,CSS 檔案已經被獨立出來。
js和css壓縮
在前面的配置中,mode
被設定為 development
,這在開發模式下便於除錯。然而,在程式碼釋出時,我們需要切換到 production
模式。在這種模式下,Webpack
會自動對資原始檔進行壓縮,以減小檔案大小。
除了更改 mode 設定之外,我們還可以利用 terser-webpack-plugin
和 css-minimizer-webpack-plugin
分別對 JavaScript
和 CSS
資源進行進一步的壓縮。
const TerserPlugin = require("terser-webpack-plugin");
const CssMinimizerWebpackPlugin = require("css-minimizer-webpack-plugin");
module.exports = {
mode: "production",
optimization: {
minimizer: [new TerserPlugin({}), new CssMinimizerWebpackPlugin()],
},
};
如圖所示,我們可以看到不同 mode 設定下,以及使用外掛對程式碼資源進行壓縮後的檔案體積變化。儘管當前專案只有一個頁面,包含少量的 HTML、CSS 和 JS 檔案,因此程式碼壓縮的效果可能不是特別顯著,但隨著專案規模的擴大,這種壓縮策略的效果將更加明顯。
增加開發模式
以上程式碼的修改,我們都透過執行 npm run build
來觀察編譯後的產物。然而,當需要遷移多個檔案時,使用開發模式會更便於實時檢視所有業務場景的使用情況。
為了實現這一點,我們可以使用 webpack-dev-server
來啟動一個開發伺服器。安裝完成後,在 webpack.config.js
檔案中增加 devServer
的配置:
module.exports = {
devServer: {
open: true,
compress: true,
port: 8000,
},
};
接著,在 package.json
檔案中配置一個用於啟動開發伺服器的指令碼指令:
"scripts": {
"dev": "webpack serve",
},
這樣一來,透過執行 npm run dev
即可啟動開發伺服器,並自動開啟瀏覽器檢視 index.html
頁面的內容。這樣不僅方便除錯,還能實時預覽程式碼改動的效果。
多入口
到目前為止,我們僅遷移了首頁的資源。現在我們將繼續遷移購物車頁面。與首頁的遷移類似,首先將 cart.html 檔案複製到 src 目錄下,並查詢其中引入的 CSS 和 JS 資源。
接著,建立一個 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';
接下來的配置非常關鍵。我們需要在 webpack.config.js
中定義多入口,併為每個頁面生成相應的模板 HTML 檔案。這裡需要注意的是,一定要定義 chunks
屬性,否則生成的 HTML 頁面會錯誤地引入所有 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"],
}),
],
};
完成上述配置後,再次執行 npm run build
,即可編譯出兩個頁面。此時,在 dist 資料夾中直接點選 cart.html
檔案,也可以順利訪問頁面內容。
拆分公共資源
儘管目前可以編譯出兩個 HTML
頁面的資源,但如果檢視 dist
資料夾下的 index.js
或者 cart.js
檔案,會發現裡面仍然包含有 jQuery
的程式碼。
為了最佳化這種情況,我們希望將像 jQuery
這樣的重複資源作為公共模組來引用,而不是讓它們在不同的 JS 檔案中反覆編譯。以下是一個詳細的 splitChunks
配置示例,它可以將 node_modules
中的資源進行分類處理,將 jQuery
編譯成單獨的檔案,並將其他第三方庫編譯為另一個檔案。
module.exports = {
optimization: {
splitChunks: {
chunks: "all",
name: "common",
cacheGroups: {
jquery: {
// 測試模組是否包含 'jquery' 字串
test: /[\\/]node_modules[\\/]jquery[\\/]/,
// 設定檔名
name: "jquery",
// 檔名可以是函式形式,也可以直接指定字串
filename: "jquery.js",
// 確保只包含非同步載入的 chunk 中的 jQuery
priority: 10, // 可以設定優先順序來控制合併順序
enforce: true, // 強制建立這個 chunk 即使其他規則可能忽略它
},
vendors: {
// 這個 cache group 用來處理其他第三方庫
test: /[\\/]node_modules[\\/]/,
name: "vendors",
priority: -10,
filename: "vendors.js",
chunks: "all",
},
},
},
},
};
由於當前專案中只用到了 jQuery 這一資源,因此只有 jQuery
被單獨打包。隨著專案發展和資源的增加,可以進一步細化拆分規則。從結果可以看出,當 jQuery
被拆分出來後,index.js 和 cart.js 的檔案體積都有了顯著的減少。
模板檔案ejs
在不同的頁面中,頁面頂部的導航通常是固定且相同的。在當前專案中,相同的 HTML 部分是透過複製來定義的。為了提高程式碼的複用性和維護性,我們可以使用 EJS(Embedded JavaScript)
來將這部分相同的邏輯抽離出來。
首先,在 src
資料夾下建立一個 ejs
資料夾,並在其中建立一個 header.ejs
檔案。找到定義 header
的程式碼,將其複製到 header.ejs
檔案中,並將變化的內容(如頁面標題)透過 <%= title %>
的方式定義。
然後,在原來 HTML 頁面中定義 header
程式碼的地方引入 header.ejs
檔案,並傳入動態變數:
<%=require('./ejs/header.ejs')({ title: '首頁'})%>
由於 Webpack
本身不具備處理 EJS
檔案的能力,因此我們需要安裝 ejs-loader
並配置相應的處理規則:
module.exports = {
module: {
rules: [
{ test: /\.ejs/, loader: "ejs-loader", options: { esModule: false } },
],
},
};
透過這樣的配置,我們就實現了公共程式碼的複用。
以上步驟完成了從純 HTML/JS 專案遷移到使用 Webpack 進行開發的全過程。透過使用 Webpack,我們實現了程式碼分割、資源按需載入,並採用了模組化開發。藉助 html-webpack-plugin 和 clean-webpack-plugin 等外掛,簡化了構建流程,確保每次構建都能得到乾淨且最佳化的輸出檔案。
透過 EJS 抽象公共頭部等重複程式碼片段,減少了冗餘,提高了程式碼複用率,使程式碼庫更簡潔。
如果你對前端、JavaScript 和工程化感興趣,快來瞅瞅我的其他文章吧~我會不定期分享各種學習心得和使用技巧。戳我的頭像,一起探索更多好玩的內容吧!