重構案例:將純HTML/JS專案遷移到Webpack

一颗冰淇淋發表於2024-10-20

我們已經瞭解了許多關於 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-loaderstyle-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-plugincss-minimizer-webpack-plugin 分別對 JavaScriptCSS 資源進行進一步的壓縮。

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 和工程化感興趣,快來瞅瞅我的其他文章吧~我會不定期分享各種學習心得和使用技巧。戳我的頭像,一起探索更多好玩的內容吧!

相關文章