跳到主要内容

Webpack MultiPage

2024年11月21日
柏拉文
越努力,越幸运

一、认识


我们的目标是实现 多页面配置代码体积优化构建速度优化,并解决 重复打包 的问题

二、准备


2.1 初始化

mkdir webpack-multiPage
cd webpack-multiPage

2.2 安装依赖

1. 安装 Webpack 及必要依赖

pnpm add webpack webpack-cli webpack-merge webpack-dev-server html-webpack-plugin -D

2. 安装 SWCTypeScript: 安装 SWC 加载器和核心依赖, 安装 TypeScript 及类型支持

pnpm add @swc/cli @swc/core swc-loader typescript -D

2.3 项目结构

├── build/
│ ├── webpack.common.js
│ ├── webpack.dev.js
│ ├── webpack.prod.js
├── common/
│ ├── components/
│ │ ├── Header.js
│ │ ├── Footer.js
│ │ └── Sidebar.js
│ └── utils.js
├── page/
│ ├── APage/
│ │ └── index.js
│ ├── BPage/
│ │ └── index.js
│ └── CPage/
│ └── index.js
├── public/
│ └── index.html

三、配置


3.1 webpack.common.js

const Path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");

const entryMap = {
pageA: Path.resolve(process.cwd(), "./src", "pageA/index.ts"),
pageB: Path.resolve(process.cwd(), "./src", "pageB/index.ts"),
pageC: Path.resolve(process.cwd(), "./src", "pageC/index.ts"),
};

const getEntry = () => {
const entry = {};
for (const key in entryMap) {
entry[key] = {
import: entryMap[key],
};
}
return entry;
};

const getEntryHtmlWebpackPlugin = () => {
const plugins = [];
for (const entry in entryMap) {
plugins.push(
new HtmlWebpackPlugin({
filename: `${entry}/index.html`,
chunks: [entry, "common", "runtime"],
template: Path.resolve(process.cwd(), "public", `index.html`),
})
);
}
return plugins;
};

module.exports = {
entry: getEntry(),
output: {
clean: true,
path: Path.resolve(process.cwd(), "dist"),
filename: "[name]/[name].[contenthash:8].js",
chunkFilename: "[name]/[id].[contenthash:8].js",
},
module: {
rules: [
{
test: /\.ts$/,
use: ["swc-loader"],
exclude: /node_modules/,
include: Path.resolve(process.cwd(), "src"),
},
],
},
resolve: {
extensions: [".ts", ".js"],
},
plugins: [...getEntryHtmlWebpackPlugin()],
optimization: {
minimize: true,
runtimeChunk: "single",
splitChunks: {
chunks: "all",
cacheGroups: {
lodash: {
priority: 10,
chunks: "all",
name: "lodash",
test: /[\\/]node_modules[\\/]lodash[\\/]/,

},
vendors: {
priority: 9,
chunks: "all",
name: "vendors",
test: /[\\/]node_modules[\\/]/,
},
common: {
minSize: 0,
priority: 8,
chunks: "all",
name: "common",
reuseExistingChunk: true,
test: /[\\/]src[\\/]common[\\/]/,

},
},
},
},
};

3.2 webpack.dev.js

const Path = require("path");
const { merge } = require("webpack-merge");
const common = require("./webpack.config.common");

module.exports = merge(common, {
mode: "development",
devtool: "source-map",
devServer: {
port: 9000,
compress: true,
client: {
progress: true,
},
static: {
directory: Path.resolve(process.cwd(), "public"),
},
},
});

3.3 webpack.prod.js

const { merge } = require("webpack-merge");
const common = require("./webpack.config.common");

module.exports = merge(common, {
mode: "production",
devtool: "source-map",
});

3.4 pageA/index.ts

import { test } from "../common/e";
import { multiply, deepClone } from "../common/a";

const a = {
a: 1,
b: 2,
};

const b = deepClone(a);
console.log(b);

const result = multiply(2, 3);
console.log(result);

console.log(test());

import(/* webpackChunkName: "PageA/hello" */ "./hello").then(
({ printHello }) => {
printHello();
}
);

import(/* webpackChunkName: "PageA/word" */ "./word").then(({ printWord }) => {
printWord("word");
});

3.5 package.json

{
"name": "dll",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "webpack serve --config ./build/webpack.config.dev.js",
"build": "webpack --config ./build/webpack.config.prod.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@swc/cli": "^0.5.1",
"@swc/core": "^1.9.2",
"@types/lodash": "^4.17.13",
"html-webpack-plugin": "^5.6.3",
"swc-loader": "^0.2.6",
"typescript": "^5.6.3",
"webpack": "^5.96.1",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.1.0",
"webpack-merge": "^6.0.1"
},
"dependencies": {
"lodash": "^4.17.21"
}
}

3.6 tsconfig.json

{
"compilerOptions": {
"target": "es2020",
"module": "esnext",
"moduleResolution": "node",
"strict": true,
"skipLibCheck": true,
"esModuleInterop": true
},
"exclude": ["node_modules", "dist"]
}

四、问题


4.1 对于公共模块 common 是如何设计构建的?

小型项目或明确公共依赖时: 如果是小型项目或者项目公共依赖非常清晰(如所有页面都依赖 Reactlodashahooks),可以显式声明 common 入口。entry.common, 用于显式地声明哪些模块是所有页面共享的公共依赖, 通过 dependOn 配置,其他入口可以依赖 common,明确它与其他入口的依赖关系,从而避免重复打包。Webpack 在构建时会优先处理 common,减少每个入口的重复处理开销。entry.common 能够明确指定哪些文件属于公共模块,具有更高的控制力, 明确公共库的范围并提升构建速度。在多页面应用中,能避免遗漏重要的共享代码。但是,如果显式声明的 common 模块体积较小,可能会导致额外的网络请求,而合并到页面入口文件中反而更高效。而且每次新增或移除公共模块,都需要手动调整 entry.common。如果模块的共享逻辑复杂,可能导致配置难以维护。如下所示:

entry: {
common: ["react", "react-dom", "lodash", "ahooks"],
pageA: { import: "./src/pageA/index.js", dependOn: "common" },
pageB: { import: "./src/pageB/index.js", dependOn: "common" },
}

大型项目或动态依赖时: 如果是大型项目或者项目公共依赖复杂多样,我们无需显示声明 common, 我们可以直接使用 SplitChunks 提取所有 common 公共模块。splitChunks.cacheGroups.commonWebpack 的模块分割功能,通过自动分析和提取代码中的共享部分来优化打包结果。 它会自动根据配置,分析哪些模块是多入口或懒加载模块共享的,并提取到公共 chunksplitChunks 能够分割的不仅是 entry 声明的依赖,还包括动态导入(import())或异步加载的模块。通过 testpriority 和其他规则,可以对不同范围的模块分割细化控制。 如果模块分割策略设置得当,可以移除 common,完全依赖 SplitChunks, 并避免重复配置的潜在问题。如下所示:

optimization: {
splitChunks: {
chunks: "all",
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
name: "vendors",
chunks: "all",
priority: 10,
},
common: {
test: /[\\/]src[\\/]common[\\/]/,
name: "common",
chunks: "all",
priority: 8,
reuseExistingChunk: true,
},
},
},
}

4.2 如果 entry 显示声明了 common 模块,而且在 SplitChunk 规则中也有,且有重叠,会发生什么呢?

entry.commonsplitChunks.cacheGroups.common 在功能上可能会存在重叠,具体情况取决于项目结构:

重叠场景: 如果 entry.commonsplitChunks.cacheGroups.common 的内容一致(如都包含 lodash),那么 splitChunks 的提取逻辑会将它们合并到一个 chunk 中,避免重复代码。这种情况下,entry.common 只是起到显式声明依赖关系的作用,而实际提取仍由 splitChunks 处理。

非重叠场景: 如果 entry.common 中的内容和 splitChunks.cacheGroups.common 提取的内容不同(如 entry.common 包含 lodash,而 splitChunks 提取的是共享组件),两者将生成各自的 chunk,不会相互影响。