实战项目:多页面应用(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

下一步


Webpack 多页面应用 MPA 代码分割