Webpack MultiPage
一、认识
我们的目标是实现 多页面配置、代码体积优化、构建速度优化,并解决 重复打包 的问题
二、准备
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. 安装 SWC
和 TypeScript
: 安装 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 是如何设计构建的?
小型项目或明确公共依赖时: 如果是小型项目或者项目公共依赖非常清晰(如所有页面都依赖 React
、lodash
、ahooks
),可以显式声明 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.common
是 Webpack
的模块分割功能,通过自动分析和提取代码中的共享部分来优化打包结果。 它会自动根据配置,分析哪些模块是多入口或懒加载模块共享的,并提取到公共 chunk
。 splitChunks
能够分割的不仅是 entry
声明的依赖,还包括动态导入(import()
)或异步加载的模块。通过 test
、priority
和其他规则,可以对不同范围的模块分割细化控制。 如果模块分割策略设置得当,可以移除 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.common
和 splitChunks.cacheGroups.common
在功能上可能会存在重叠,具体情况取决于项目结构:
重叠场景: 如果 entry.common
和 splitChunks.cacheGroups.common
的内容一致(如都包含 lodash
),那么 splitChunks
的提取逻辑会将它们合并到一个 chunk
中,避免重复代码。这种情况下,entry.common
只是起到显式声明依赖关系的作用,而实际提取仍由 splitChunks
处理。
非重叠场景: 如果 entry.common
中的内容和 splitChunks.cacheGroups.common
提取的内容不同(如 entry.common
包含 lodash
,而 splitChunks
提取的是共享组件),两者将生成各自的 chunk
,不会相互影响。