FORMA

进阶配置

Webpack 进阶场景:多页面(MPA)、devServer、环境变量注入、Source Map、Module Federation 等。基础见 工作原理

一、多页面应用配置

多页面应用(MPA)需要生成多个 HTML 文件,每个文件对应不同的入口。

1. 动态生成多个 entryHtmlWebpackPlugin 实例

js
// webpack.config.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const glob = require("glob");

// 约定页面目录:src/pages/下每个子文件夹代表一个页面
const pages = glob.sync("./src/pages/*/index.js");
const entry = {};
const htmlPlugins = [];

pages.forEach((pagePath) => {
  const pageName = path.basename(path.dirname(pagePath));
  entry[pageName] = pagePath;
  htmlPlugins.push(
    new HtmlWebpackPlugin({
      template: `./src/pages/${pageName}/index.html`,
      filename: `${pageName}.html`,
      chunks: [pageName, "vendor"], // 指定引入的chunk
    }),
  );
});

module.exports = {
  entry,
  plugins: [...htmlPlugins],
};

2. 多页面共享 vendor chunk

为了避免每个页面重复打包公共库,使用 splitChunks 提取 node_modules 中的依赖:

js
optimization: {
  splitChunks: {
    cacheGroups: {
      vendor: {
        test: /[\\/]node_modules[\\/]/,
        name: 'vendor',
        chunks: 'all',        // 对所有入口生效
        priority: 10,
      },
      common: {
        minChunks: 2,          // 至少被2个入口引用
        name: 'common',
        chunks: 'all',
        priority: 5,
        reuseExistingChunk: true,
      },
    },
  },
},

然后在 HtmlWebpackPluginchunks 数组中包含 vendorcommon,确保生成的 HTML 会引用这些共享 chunk。

二、开发服务器(webpack-dev-server)进阶

1. 代理配置(proxy)解决跨域、路径重写

开发环境下,前端请求后端 API 时若遇到跨域问题,可以通过 devServer 代理将请求转发到真实服务器。

js
devServer: {
  proxy: {
    '/api': {
      target: 'http://localhost:3000',
      pathRewrite: { '^/api': '' },   // 将 /api/users 重写为 /users
      changeOrigin: true,              // 修改请求头中的 origin
      secure: false,                   // 接受 HTTPS 自签名证书
    },
    '/socket': {
      target: 'ws://localhost:8080',
      ws: true,                        // 支持 WebSocket 代理
    },
  },
},

2. 热更新(HMR)原理

工作原理

  • 构建时,HotModuleReplacementPlugin 给模块添加 HMR runtime 代码。
  • 运行时,通过 WebSocket 与 devServer 通信,接收模块更新通知。
  • 当某个模块更新时,客户端只请求变更的 chunk,并执行模块的热替换逻辑(需要模块自身支持,如 Vue、React 的 HMR API)。

启用 HMR

js
devServer: {
  hot: true,   // 启用 HMR,自动注入 HotModuleReplacementPlugin
},

自定义模块热接受:

js
if (module.hot) {
  module.hot.accept("./module.js", () => {
    console.log("module 更新了");
    // 重新执行副作用
  });
}

3. 自定义 dev-server 中间件

onBeforeSetupMiddleware / onAfterSetupMiddleware(v3 之前为 before / after)已在 webpack-dev-server v5 中移除,统一改用 setupMiddlewares:用 middlewares.unshift(...) 在内部中间件之前注入(对应旧的 onBeforeSetupMiddleware),用 middlewares.push(...) 在之后注入(对应旧的 onAfterSetupMiddleware),并需要 return middlewares

js
devServer: {
  setupMiddlewares: (middlewares, devServer) => {
    if (!devServer) {
      throw new Error("webpack-dev-server is not defined");
    }
    // 对应旧的 onBeforeSetupMiddleware:在内部中间件之前执行
    middlewares.unshift((req, res, next) => {
      if (req.path === "/custom") {
        return res.json({ custom: "response" });
      }
      next();
    });
    // 对应旧的 onAfterSetupMiddleware:在内部中间件之后执行
    middlewares.push((req, res, next) => {
      console.log(`[${new Date()}] ${req.method} ${req.url}`);
      next();
    });
    return middlewares;
  },
},

三、环境变量注入

1. DefinePlugin vs EnvironmentPlugin

插件作用用法
DefinePlugin定义全局常量,可用于任意字符串替换new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify('production') })
EnvironmentPlugin读取系统环境变量并注入到代码中,底层仍使用 DefinePluginnew webpack.EnvironmentPlugin(['NODE_ENV', 'API_BASE'])

注意:传入的值会被 JavaScript 表达式求值,因此必须使用 JSON.stringify 包裹字符串,否则会被当成变量名。

js
new webpack.DefinePlugin({
  __VERSION__: JSON.stringify("1.0.0"),
  __API_BASE__: process.env.API_BASE ? JSON.stringify(process.env.API_BASE) : '"/api"',
});

2. 类型安全的全局变量注入(配合 TypeScript)

如果在代码中使用了全局变量(如 __API_BASE__),TypeScript 会报错,需要声明其类型。

创建 global.d.ts

ts
declare const __API_BASE__: string;
declare const __VERSION__: string;

然后在任意 .ts 文件中便可直接使用,类型安全。

四、source map 策略

devtool 选项决定了 source map 的生成方式,需要在构建速度、重建速度、调试体验之间权衡。

devtool 类型构建速度重建速度生产环境调试体验说明
eval最快最快一般每个模块用 eval 执行,报错只显示行号,无源码映射
eval-cheap-source-map较好每行映射,不包含列信息,丢失原始源文件(loader 转换后)
eval-cheap-module-source-map中等中等包含列信息,保留 loader 处理后的源码映射,开发推荐
source-map最佳生成完整独立 .map 文件,减速构建,生产可用但不推荐(暴露源码)
hidden-source-map不直接生成 source map 但不关联,用于错误上报平台
nosources-source-map有限只显示堆栈位置,不包含源代码内容,适合生产环境
cheap-module-source-map较慢较慢较好不包含列,但保留原始源码,适合生产调试

推荐组合

  • 开发环境eval-cheap-module-source-map(快速重建,调试友好)
  • 生产环境hidden-source-mapnosources-source-map(不暴露源码,但可提供错误追踪)
js
module.exports = {
  devtool:
    process.env.NODE_ENV === "production" ? "hidden-source-map" : "eval-cheap-module-source-map",
};

五、模块联邦(Module Federation)

模块联邦是 Webpack 5 的核心特性,用于实现微前端架构,允许多个独立构建的应用在运行时共享模块。

1. exposesremotes 配置

  • exposes:当前应用暴露给其他应用的模块。
  • remotes:当前应用引用的远程应用入口。

host 应用(shell)

js
new ModuleFederationPlugin({
  name: "shell", // 应用名称,必须唯一
  remotes: {
    app1: "app1@http://localhost:3001/remoteEntry.js",
    app2: "app2@http://localhost:3002/remoteEntry.js",
  },
  shared: ["react", "react-dom"],
});

remote 应用

js
new ModuleFederationPlugin({
  name: "app1",
  filename: "remoteEntry.js", // 远程入口文件
  exposes: {
    "./Button": "./src/Button",
    "./utils": "./src/utils",
  },
  shared: ["react", "react-dom"],
});

2. 共享依赖(shared)策略

shared 配置用于避免重复加载公共库,同时解决版本冲突。

js
shared: {
  react: {
    singleton: true,      // 全局唯一实例
    eager: false,         // 默认 false,不提前加载(懒加载)
    requiredVersion: '^18.0.0',
    version: '18.2.0',
  },
  'react-dom': {
    singleton: true,
    eager: false,
  },
  lodash: {
    singleton: false,     // 允许多版本共存
  },
}
  • singleton:是否只使用一个共享实例。如果多个应用版本不兼容,Webpack 会在控制台发出警告,并选择最高兼容版本。
  • eager:是否在初始 chunk 中直接加载该共享模块,而不是异步加载。如果设置为 true,可能会增加首屏体积,但避免异步加载的额外请求。
  • version / requiredVersion:用于版本冲突时的匹配。

3. 构建时与运行时的模块加载机制

构建时

  • 每个联邦构建会生成一个 remoteEntry.js 文件(或者其他名称),该文件包含了暴露模块的映射以及加载逻辑。
  • 构建时并不解析远程模块的具体代码,只记录其暴露路径和共享依赖关系。

运行时

  • 当 host 应用执行 import('app1/Button') 时,Webpack 运行时会根据 remotes 配置动态加载 app1remoteEntry.js
  • remoteEntry 中定义了一个 __webpack_require__.l 动态加载方法,可以按需获取暴露模块的 chunk。
  • 共享依赖的检测:加载远程模块前,运行时会检查本地是否已有匹配的共享模块版本,如果有则直接提供,避免重复加载。

图示流程

  1. Host 引用 app1/Button → 调用 __webpack_require__.f.remotes
  2. 检查缓存中是否已有该模块,没有则动态加载 app1/remoteEntry.js
  3. 执行 remoteEntry 代码,注册 app1 的模块工厂
  4. 从远程模块工厂中获取 Button 模块,同时处理共享依赖(优先使用 host 提供的版本)

总结

配置主题核心要点
MPA动态生成 entry + HtmlWebpackPlugin,splitChunks 提取共享 vendor
devServerproxy 代理跨域,HMR 原理(插件+WebSocket),自定义中间件扩展
环境变量DefinePlugin vs EnvironmentPlugin;配合 TypeScript declare 保证类型安全
source map开发用 eval-cheap-module-source-map,生产用 hidden-source-mapnosources-source-map,权衡构建速度和调试信息
模块联邦exposes/remotes 暴露/引用模块,shared 控制依赖共享策略(singleton、eager),实现微前端运行时模块共享

掌握这些高级配置能让 Webpack 适应更复杂的业务场景,并显著提升开发体验和生产性能。

参考文献

以下链接在编写时均可正常访问:

资料说明
Module Federation官方
DevServer配置
Source Mapdevtool

Series

webpack

1 / 6

产物优化