开篇吐槽

你是不是也遇到过这种情况:兴冲冲地想用 Vite 的模块联邦把项目拆分成微前端,结果 npm run dev 一跑,浏览器控制台一片红?remoteEntry.js 404?Top-level await is not availableFailed to resolve module specifier

别慌,这篇文章就是来救你的。我把踩过的坑都填平了,你只管抄作业。

第一个坑:remoteEntry.js 404,人都傻了

崩溃现场

我按照官方文档配置好了远程应用(表单设计器),信心满满地 npm run dev,然后在主应用里引入远程组件。结果浏览器控制台直接给我来了一个:

GET http://localhost:3001/mf/remoteEntry.js 404 (Not Found)

我一脸懵逼:配置明明写了 filename: 'mf/remoteEntry.js',为啥找不到?

闭坑方案

后来我才发现,@originjs/vite-plugin-federation 这个插件有个大坑

核心问题
  • 远程应用:开发模式(npm run dev不生成 remoteEntry.js
  • 主应用:开发模式(npm run dev)可以正常使用。

也就是说,远程应用必须用 构建模式 才能生成 remoteEntry.js。我改了 package.json

{
  "scripts": {
    "dev": "vite",
    "dev:mf": "vite build --watch",
    "dev:remote": "npm run dev:mf & npm run preview",
    "preview": "vite preview --port 3001"
  }
}

然后运行 npm run dev:remote,终于看到 build/assets/mf/remoteEntry.js 生成了!

关键点

注意路径是 /assets/mf/remoteEntry.js,不是 /mf/remoteEntry.js!Vite 构建时会自动把文件放到 assets 目录下。

第二个坑:Top-level await 报错,又懵了

崩溃现场

好不容易解决了 404,结果浏览器又给我来了一个:

Top-level await is not available in the configured target environment

我心想:啥玩意儿?我又没写 await,哪来的 top-level await?

闭坑方案

后来我翻了半天源码才发现,模块联邦生成的代码里用了 top-level await,而 Vite 默认的 targetes2020,不支持这个特性。

核心问题

必须把 target 改成 esnext,而且要在 三个地方 都改:

  • build.target
  • esbuild.target
  • optimizeDeps.esbuildOptions.target

我改了 vite.config.js

export default defineConfig({
  esbuild: {
    target: 'esnext',
  },
  optimizeDeps: {
    esbuildOptions: {
      target: 'esnext',
    },
  },
  build: {
    target: 'esnext',
  },
});

重新构建,终于不报错了!

第三个坑:Failed to resolve module specifier,崩溃三连

崩溃现场

前两个坑填完了,我以为终于可以跑起来了。结果浏览器又给我来了一个:

Failed to resolve module specifier "form_designer_remote"

我一看主应用的配置:

remotes: {
  formDesigner: 'form_designer_remote@http://localhost:3001/assets/mf/remoteEntry.js'
}

这不是官方文档的写法吗?为啥不行?

闭坑方案

后来我发现,@originjs/vite-plugin-federation 的配置格式和 Webpack 的不一样

核心问题

Vite 的模块联邦配置必须用 对象格式,不能带 form_designer_remote@ 前缀:

remotes: {
  formDesigner: {
    external: 'http://localhost:3001/assets/mf/remoteEntry.js'
  }
}

改完之后,终于跑起来了!泪目!

闭坑方案:完整可用的配置

远程应用配置(表单设计器)

// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import federation from '@originjs/vite-plugin-federation';

export default defineConfig({
  plugins: [
    react(),
    federation({
      name: 'form_designer_remote',
      filename: 'mf/remoteEntry.js',
      exposes: {
        './FormDesigner': './src/views/FormDesigner/FormDesigner.jsx',
      },
      shared: {
        react: { singleton: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
      },
    }),
  ],
  esbuild: {
    target: 'esnext',
  },
  optimizeDeps: {
    esbuildOptions: {
      target: 'esnext',
    },
  },
  build: {
    target: 'esnext',
    minify: false,
    cssCodeSplit: false,
  },
});

主应用配置(流程引擎)

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import federation from '@originjs/vite-plugin-federation';

export default defineConfig({
  plugins: [
    react(),
    federation({
      name: 'process_engine_host',
      remotes: {
        formDesigner: {
          external: 'http://localhost:3001/assets/mf/remoteEntry.js'
        }
      },
      shared: {
        react: { singleton: true, requiredVersion: '^18.2.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.2.0' },
      },
    }),
  ],
  server: {
    port: 3900,
    cors: true,
  },
});

使用远程组件

// RemoteFormDesigner.tsx
import { lazy, Suspense } from 'react';
import { Spin } from 'antd';

const FormDesigner = lazy(() => import('formDesigner/FormDesigner'));

function RemoteFormDesigner(props) {
  return (
    <Suspense fallback={<Spin size="large" tip="加载表单设计器..." />}>
      <FormDesigner {...props} />
    </Suspense>
  );
}

export default RemoteFormDesigner;

开发流程:一键启动

# 1. 远程应用:一键启动构建+预览
cd lowcodeform-frontend
npm run dev:remote

# 2. 主应用:启动开发服务器
cd ProcessEngineServer/client
npm run dev

访问主应用:http://localhost:3900,终于看到远程组件加载成功了!

开发联调架构流程

graph TB subgraph Remote["远程应用 (表单设计器 - 端口 3001)"] A1[npm run dev:mf] --> A2[vite build --watch] A2 --> A3[生成 build/assets/mf/remoteEntry.js] A3 --> A4[监听文件变化
自动重新构建] B1[npm run preview] --> B2[启动静态服务器] B2 --> B3["http://localhost:3001/assets/mf/remoteEntry.js ✓"] end subgraph Host["主应用 (流程引擎 - 端口 3900)"] C1[npm run dev] --> C2[Vite 开发服务器] C2 --> C3["配置 remotes:
formDesigner: { external: 'http://localhost:3001/...' }"] C3 --> C4["懒加载组件:
const FormDesigner = lazy(() => import('formDesigner/FormDesigner'))"] C4 --> C5[运行时动态加载远程模块 ✓] end B3 -->|CORS 跨域访问| C3 style Remote fill:#e1f5ff style Host fill:#fff4e1 style B3 fill:#90EE90 style C5 fill:#90EE90

问题排查流程:遇到问题怎么办?

如果你也遇到了模块联邦的问题,可以按照这个流程图快速定位:

flowchart TD Start([遇到模块联邦问题]) --> Q1{远程应用
是否运行?} Q1 -->|否| A1[启动远程应用
npm run dev:remote] Q1 -->|是| Q2{访问 remoteEntry.js
是否返回 404?} A1 --> End([问题解决 ✓]) Q2 -->|是| A2[检查路径
应该是 /assets/mf/] Q2 -->|否| Q3{报错: Failed to
resolve module
specifier?} A2 --> End Q3 -->|是| A3[检查主应用配置
使用对象格式
不带前缀] Q3 -->|否| Q4{报错: Top-level
await not
available?} A3 --> End Q4 -->|是| A4[检查远程应用
三个 target 都是 esnext] Q4 -->|否| A5[其他问题
查看文档] A4 --> End A5 --> End style Start fill:#ffcccc style End fill:#90EE90 style A1 fill:#fff4e1 style A2 fill:#fff4e1 style A3 fill:#fff4e1 style A4 fill:#fff4e1

总结:三个关键点

闭坑秘籍
  1. 远程应用必须用构建模式npm run dev:mf + npm run preview
  2. 三个地方都配置 target: 'esnext'build.targetesbuild.targetoptimizeDeps.esbuildOptions.target
  3. 主应用用对象格式配置 remotes:不带前缀,路径是 /assets/mf/remoteEntry.js

希望这篇文章能帮你少踩点坑。如果你还遇到其他问题,欢迎加作者交流!

终极方案:一劳永逸解决所有问题

说实话,上面这些坑踩下来,我已经对 @originjs/vite-plugin-federation 失去信任了。它的问题不是一个两个,而是设计层面的缺陷

  • 开发模式不能用,必须 build --watch,体验割裂
  • target 要配三遍,封装不到位
  • 配置格式和 Webpack 不兼容,迁移成本高
  • 热更新不生效,改代码要手动刷新
  • 共享依赖版本冲突时报错信息不友好
  • 微前端场景下 CSS 样式冲突,主应用和子应用样式互相污染

如果你也受够了这些问题,可以试试我封装的插件组合(持续更新):

推荐方案

@jiayouzuo/vite-module-federation-core - 模块联邦核心插件

这个插件针对上述痛点做了完整优化:

  • 开发模式直接可用:不用 build --watch,真正的 HMR 体验
  • 自动处理 target:内部统一配置,不用写三遍
  • 兼容 Webpack 配置习惯:迁移零成本
  • 热更新正常工作:改代码自动刷新
  • 智能共享依赖管理:自动处理版本冲突
  • 暴露 exposes 配置:方便其他插件(如 CSS 作用域插件)读取
样式隔离方案

@jiayouzuo/vite-plugin-css-scope - CSS 作用域隔离插件

解决微前端场景下的样式冲突问题:

  • ✅ 自动给 CSS 选择器添加作用域前缀
  • ✅ 自动识别模块联邦暴露的组件并注入作用域
  • ✅ 支持多种模块联邦插件(包括 vite-module-federation-core)

安装使用:

# 安装模块联邦核心插件
npm install @jiayouzuo/vite-module-federation-core

# 如果需要样式隔离,额外安装
npm install @jiayouzuo/vite-plugin-css-scope

配置示例(远程应用):

// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import federation from '@jiayouzuo/vite-module-federation-core';
import cssScope from '@jiayouzuo/vite-plugin-css-scope';

export default defineConfig({
  plugins: [
    // 可选:CSS 作用域隔离,需要再react之前进行
    cssScope({
      scope: 'remote-app',
      include: ['src/'],
    }),
    react(),
    federation({
      name: 'remote_app',
      filename: 'remoteEntry.js',
      exposes: {
        './FormDesigner': './src/views/FormDesigner/FormDesigner.jsx',
      },
      shared: {
        react: { singleton: true },
        'react-dom': { singleton: true },
      },
    }),
  ],
});

配置示例(主应用):

// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import federation from '@jiayouzuo/vite-module-federation-core';

export default defineConfig({
  plugins: [
    react(),
    federation({
      name: 'host_app',
      remotes: {
        remoteApp: {
          external: 'http://localhost:3001/assets/remoteEntry.js',
          from: 'vite',
          format: 'esm',
        },
      },
      shared: {
        react: { singleton: true },
        'react-dom': { singleton: true },
      },
    }),
  ],
});

就这么简单,不用配 target,不用 build --watch,直接 npm run dev 就能用。样式冲突?加上 css-scope 插件一键解决。

相关技术文献资源

如果你想深入了解 Vite 模块联邦的更多细节,可以参考以下资源: