重構(gòu)案例:將純HTML/JS項(xiàng)目遷移到Webpack

我們已經(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。

1_原生PC頁(yè)面.png

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

2_原生PC目錄結(jié)構(gòu).png

初始化 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 目錄下。

3_index.html引入資源.png

隨后,我們觀察 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-loaderstyle-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)簽引入的資源,它則無能為力。

4_處理html中圖片.png

如圖所示,這些圖片的路徑都是 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 方法并未生效。

5_$未定義.png

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ú)的文件。

6_style-loader處理的文件.png

為此,我們可以使用 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ú)立出來。

7_mini-css-extract-plugin.png

js和css壓縮

在前面的配置中,mode 被設(shè)置為 development,這在開發(fā)模式下便于調(diào)試。然而,在代碼發(fā)布時(shí),我們需要切換到 production 模式。在這種模式下,Webpack 會(huì)自動(dòng)對(duì)資源文件進(jìn)行壓縮,以減小文件大小。

除了更改 mode 設(shè)置之外,我們還可以利用 terser-webpack-plugincss-minimizer-webpack-plugin 分別對(duì) JavaScriptCSS 資源進(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ò)大,這種壓縮策略的效果將更加明顯。

8_mode區(qū)別.png

增加開發(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 資源。

9_carthtml.png

接著,創(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 的代碼。

10_jquery未拆分.png

為了優(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 的文件體積都有了顯著的減少。

11_拆分公共資源.png

模板文件ejs

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

固定頭部.png

首先,在 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)容吧!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容