实战项目:多页面应用(MPA)
多页面应用(MPA)需要为每个页面配置独立的入口和 HTML 文件。本章介绍如何使用 Webpack 搭建多页面应用。
📋 学习目标
- ✅ 理解多页面应用的结构
- ✅ 配置多入口
- ✅ 动态生成配置
- ✅ 实现代码分割策略
- ✅ 优化多页面应用构建
多页面应用 vs 单页面应用
单页面应用(SPA)
- 一个入口文件
- 一个 HTML 文件
- 路由切换页面
- 适合现代 Web 应用
多页面应用(MPA)
- 多个入口文件
- 多个 HTML 文件
- 页面跳转刷新
- 适合传统网站
项目结构
mpa-project/
├── src/
│ ├── pages/
│ │ ├── home/
│ │ │ ├── index.js
│ │ │ └── index.html
│ │ ├── about/
│ │ │ ├── index.js
│ │ │ └── index.html
│ │ └── contact/
│ │ ├── index.js
│ │ └── index.html
│ ├── shared/
│ │ ├── utils.js
│ │ └── styles.css
│ └── assets/
├── dist/
├── webpack.config.js
└── package.json
第一步:安装依赖
npm install --save-dev webpack webpack-cli webpack-dev-server
npm install --save-dev html-webpack-plugin
npm install --save-dev clean-webpack-plugin
npm install --save-dev glob第二步:创建页面文件
首页
src/pages/home/index.html:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>首页</title>
</head>
<body>
<div id="app">
<h1>首页</h1>
<p>这是首页内容</p>
</div>
</body>
</html>src/pages/home/index.js:
import '../../shared/styles.css'
console.log('首页加载完成')关于页
src/pages/about/index.html:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>关于我们</title>
</head>
<body>
<div id="app">
<h1>关于我们</h1>
<p>这是关于页内容</p>
</div>
</body>
</html>src/pages/about/index.js:
import '../../shared/styles.css'
console.log('关于页加载完成')第三步:配置 Webpack
动态生成入口和 HTML
webpack.config.js:
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const glob = require('glob')
// 动态获取所有页面入口
function getEntries() {
const entries = {}
const htmlPlugins = []
// 查找所有页面的 index.js
glob.sync('./src/pages/**/index.js').forEach(file => {
// 提取页面名称(如 home, about)
const match = file.match(/\/pages\/(.+)\/index\.js$/)
if (match) {
const name = match[1]
entries[name] = file
// 为每个页面创建 HtmlWebpackPlugin
htmlPlugins.push(
new HtmlWebpackPlugin({
template: file.replace('index.js', 'index.html'),
filename: `${name}.html`,
chunks: [name] // 只包含当前页面的 chunk
})
)
}
})
return { entries, htmlPlugins }
}
const { entries, htmlPlugins } = getEntries()
module.exports = {
entry: entries,
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'js/[name].[contenthash:8].js',
clean: true
},
mode: 'production',
plugins: [
new CleanWebpackPlugin(),
...htmlPlugins
],
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
// 公共代码
common: {
name: 'common',
minChunks: 2,
priority: 5,
reuseExistingChunk: true
},
// 第三方库
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10,
reuseExistingChunk: true
}
}
}
},
devServer: {
static: {
directory: path.join(__dirname, 'dist')
},
port: 3000,
open: true
}
}第四步:共享代码
共享样式
src/shared/styles.css:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
line-height: 1.6;
color: #333;
}
h1 {
color: #42b983;
margin-bottom: 20px;
}共享工具
src/shared/utils.js:
export function formatDate(date) {
return new Date(date).toLocaleDateString()
}
export function debounce(func, wait) {
let timeout
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout)
func(...args)
}
clearTimeout(timeout)
timeout = setTimeout(later, wait)
}
}第五步:优化配置
分离公共代码
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
// 公共样式
styles: {
test: /\.css$/,
name: 'styles',
chunks: 'all',
enforce: true
},
// 公共代码
common: {
name: 'common',
minChunks: 2,
priority: 5
},
// 第三方库
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10
}
}
}
}
}配置 HTML 插件
htmlPlugins.push(
new HtmlWebpackPlugin({
template: file.replace('index.js', 'index.html'),
filename: `${name}.html`,
chunks: [name, 'common', 'vendors'], // 包含公共代码
minify: {
removeComments: true,
collapseWhitespace: true
}
})
)完整配置示例
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const glob = require('glob')
function getEntries() {
const entries = {}
const htmlPlugins = []
glob.sync('./src/pages/**/index.js').forEach(file => {
const match = file.match(/\/pages\/(.+)\/index\.js$/)
if (match) {
const name = match[1]
entries[name] = file
htmlPlugins.push(
new HtmlWebpackPlugin({
template: file.replace('index.js', 'index.html'),
filename: `${name}.html`,
chunks: [name, 'common', 'vendors']
})
)
}
})
return { entries, htmlPlugins }
}
const { entries, htmlPlugins } = getEntries()
module.exports = {
entry: entries,
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'js/[name].[contenthash:8].js',
clean: true
},
mode: 'production',
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
},
plugins: [
new CleanWebpackPlugin(),
...htmlPlugins
],
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
common: {
name: 'common',
minChunks: 2,
priority: 5
},
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10
}
}
}
}
}最佳实践
1. 使用 glob 动态发现页面
const glob = require('glob')
const pages = glob.sync('./src/pages/**/index.js')2. 合理配置 chunks
chunks: [name, 'common', 'vendors']3. 分离公共代码
splitChunks: {
cacheGroups: {
common: {
minChunks: 2 // 被 2 个以上页面使用
}
}
}4. 使用模板引擎
可以使用 Handlebars、EJS 等模板引擎生成 HTML。
总结
- 多入口:为每个页面配置独立入口
- 动态生成:使用 glob 自动发现页面
- 代码分割:分离公共代码和第三方库
- HTML 生成:为每个页面生成独立的 HTML