广告位 · 页头(后台-模块管理-新建模块后替换此区域)

Webpack 5 模块联邦如何构建可导航的微前端宇宙?

微前端架构中,多个团队独立开发、独立部署的子应用往往像一座座孤岛,用户在不同应用间切换时需要整页刷新,体验割裂。Webpack 5 引入的模块联邦特性提供了一种轻量级的运行时共享机制,让这些孤岛能够通过远程模块加载实现无缝导航,构建出一个可自由穿梭的前端宇宙。

Webpack 5 模块联邦如何构建可导航的微前端宇宙?

接下来将围绕模块联邦的配置、导航实现和性能优化三个层面展开,帮助你把这一特性应用到实际项目中。

模块联邦基础:远程模块的暴露与加载

模块联邦的核心思想是让一个应用在运行时动态加载另一个应用暴露出来的模块,就像加载本地模块一样自然。要实现这一点,需要借助 Webpack 5 内置的 ModuleFederationPlugin。该插件既可以把当前应用的某些模块暴露出去供其他应用消费,也可以声明当前应用需要远程加载哪些模块。与传统的 NPM 包发布不同,远程模块不需要提前安装到 node_modules,而是通过 URL 在运行时拉取。

在一个典型的微前端项目中,通常有一个宿主应用(host)负责整体布局和路由分发,若干远程应用(remote)提供具体的业务模块。远程应用需要在自己的 webpack 配置中声明暴露的模块,例如一个导航组件或者一个业务页面。下面这段配置展示了远程应用如何暴露一个名为 Navigation 的组件:

// remote 应用的 webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
  // 其他配置省略
  plugins: [
    new ModuleFederationPlugin({
      name: 'remoteApp',
      filename: 'remoteEntry.js',
      exposes: {
        './Navigation': './src/components/Navigation',
      },
      shared: {
        react: { singleton: true, eager: true },
        'react-dom': { singleton: true, eager: true },
      },
    }),
  ],
};

宿主应用则需要在配置中声明远程模块的来源和需要消费的模块名称。例如宿主应用需要在 remotes 字段中指定远程应用的地址,然后就可以像导入本地模块一样使用远程组件。这种加载是异步的,因此需要使用动态导入语法来包裹远程模块的引用。

// host 应用的 webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'hostApp',
      remotes: {
        remoteApp: 'remoteApp@http://localhost:3001/remoteEntry.js',
      },
      shared: {
        react: { singleton: true },
        'react-dom': { singleton: true },
      },
    }),
  ],
};

当宿主应用在代码中执行 import('remoteApp/Navigation') 时,Webpack 运行时会先加载远程入口文件,再获取对应的模块并执行。整个过程中不会重新打包远程模块,也不会污染全局命名空间,这种按需加载的方式为构建大型分布式前端系统提供了极大的灵活性。

导航宇宙的实现:统一路由与跨应用跳转

微前端导航的难点在于如何让不同子应用之间的路由跳转像单页应用一样平滑,同时保持各个子应用自身的路由独立性。模块联邦可以通过共享导航组件和统一路由表来化解这个矛盾。具体做法是:由一个远程应用暴露一个全局导航菜单组件,宿主应用在启动时加载该组件并挂载到自己的布局中;当用户点击导航项时,通过浏览器的 History API 或者全局事件总线来通知目标子应用进行路由切换。

这种方案下,每个子应用仍然维护自己的路由表,但跳转逻辑不再依赖整页刷新。例如远程应用暴露一个导航组件,该组件内部使用 history.pushState 修改地址栏路径,并派发自定义事件。宿主应用监听该事件,动态加载目标子应用的入口并渲染。下面这段代码展示了远程导航组件的简单实现:

// remote 应用暴露的 Navigation 组件
import React from 'react';
const Navigation = () => {
  const handleNavigate = (path) => {
    // 更新地址栏
    window.history.pushState({}, '', path);
    // 派发全局导航事件,宿主应用监听后加载对应子应用
    window.dispatchEvent(new CustomEvent('navigate', { detail: { path } }));
  };
  return (
    <nav>
      <button onClick={() => handleNavigate('/app1')}>应用一</button>
      <button onClick={() => handleNavigate('/app2')}>应用二</button>
    </nav>
  );
};
export default Navigation;

宿主应用收到导航事件后,根据路径决定加载哪个远程模块。为了简化示例,这里使用一个映射表将路径与远程模块关联起来,然后通过动态导入渲染对应组件。实际的微前端框架中还会处理子应用卸载、生命周期同步等细节,但核心机制是相同的。

// host 应用中的导航处理逻辑
import React, { useState, useEffect } from 'react';
const App = () => {
  const [activeComponent, setActiveComponent] = useState(null);
  useEffect(() => {
    const handleNavigation = async (event) => {
      const { path } = event.detail;
      let RemoteComponent = null;
      if (path === '/app1') {
        RemoteComponent = (await import('remoteApp/AppOne')).default;
      } else if (path === '/app2') {
        RemoteComponent = (await import('remoteApp/AppTwo')).default;
      }
      setActiveComponent(() => RemoteComponent);
    };
    window.addEventListener('navigate', handleNavigation);
    return () => window.removeEventListener('navigate', handleNavigation);
  }, []);
  return (
    <div>
      <Navigation />
      <main>
        {activeComponent ? React.createElement(activeComponent) : <p>请选择应用</p>}
      </main>
    </div>
  );
};

这种基于事件和动态导入的导航方式,让各个子应用可以独立部署、独立升级,同时宿主应用保持轻量。更重要的是,用户在不同子应用之间切换时无需刷新页面,地址栏会正确更新,浏览器前进后退也能正常工作,从而真正实现了微前端宇宙中的无缝导航体验。

依赖共享与性能优化:避免导航宇宙中的重复加载

在微前端架构中,如果每个子应用都打包了一份 React、Vue 或 Ant Design,那么用户切换多个子应用时会重复下载相同的库,导致首屏变慢、流量浪费。模块联邦通过 shared 配置解决了这个问题。当多个应用声明共享同一个依赖时,Webpack 运行时会优先复用已经加载的模块,而不是重复请求。

配置共享依赖时需要关注版本兼容性。例如三个子应用分别使用 React 16、17、18,如果强制使用 singleton 模式,则可能会导致低版本应用运行异常。通常的做法是:对于 React、Vue 这类核心库使用 singleton: true 以确保全局只有一个实例,同时设置 strictVersion 或者依赖 semver 范围来控制版本匹配。下面是一个更完善的 shared 配置示例:

shared: {
  react: {
    singleton: true,
    requiredVersion: '^18.2.0',
    eager: false,
  },
  'react-dom': {
    singleton: true,
    requiredVersion: '^18.2.0',
    eager: false,
  },
  'react-router-dom': {
    singleton: true,
    requiredVersion: '^6.14.0',
  },
},

与传统的 iframe 方案相比,模块联邦的优势在于共享运行时、无边框加载、通信成本低,同时避免了 iframe 带来的样式隔离、弹窗遮盖、历史记录混乱等问题。与 NPM 包方案相比,模块联邦的远程模块是运行时加载的,不需要每次升级都发布新包并让所有消费方重新构建,大大缩短了交付链路。当然,模块联邦也不是银弹,它在跨域、安全、版本治理等方面需要额外的基础设施支撑,但作为 Webpack 5 的原生能力,它已经足够支撑大多数中大型前端项目的微前端改造。

广告位 · 文章底部(后台-模块管理替换此区域)