🚀微前端与模块联邦的深度结合(基于vue+vite)

1️⃣ 项目背景与挑战

随着业务规模的扩大,传统的单体应用架构逐渐暴露出开发效率低、协作难、部署风险高等问题。为了解决这些痛点,我们团队决定引入微前端架构,并结合模块联邦技术,实现真正意义上的解耦与资源共享。

2️⃣ 技术选型

2.1 核心框架

  • Vue 3:主流的前端框架,支持 Composition API
  • Vite:极速开发与构建工具
  • @micro-zoe/micro-app:微前端框架,基于 Web Components
  • @originjs/vite-plugin-federation:Vite 生态下的模块联邦实现
  • Pinia:新一代状态管理
  • pnpm:高效包管理器

3️⃣ 架构设计

3.1 整体架构

js 复制代码
admin-micro-app/
├── apps/
│   └── main/           # 主应用
├── micro/
│   ├── modules/        # 联邦模块(共享库/组件)
│   ├── login/          # 登录子应用
│   └── child/          # 业务子应用
└── scripts/            # 自定义脚手架

3.2 模块联邦的实现

共享模块的配置

在micro/modules目录中,我们通过@originjs/vite-plugin-federation插件配置共享模块:

js 复制代码
// scripts/models/vite/plugins/provider.ts
export const ModuleFederationProviderPlugin = () => {
  const exposes = moduleFederationUtils.getExposes();
  return federation({
    name: 'modules',
    filename: 'remoteEntry.js',
    exposes
  });
};

共享模块的暴露配置通过moduleFederationUtils.getExposes()自动生成,它会扫描micro/modules/src目录下的所有文件,并生成相应的暴露配置:

js 复制代码
// scripts/utils/module-federation.ts
export const getExposes = () => { 
  const dir = pathUtils.getMicroModulesDir();
  let exposes = moduleFederationUtils.filePathsToExposes(
    moduleFederationUtils.resolveCodeFiles(pathUtils.getMicroModulesDir())
  );
  moduleFederationUtils.createAbsolutePath(exposes, dir);
  return exposes;
}

getExposes的返回值大概长这个样子

消费端的配置

主应用和子应用作为消费端,通过以下配置引用共享模块:

js 复制代码
// scripts/models/vite/plugins/consumer.ts
export const ModuleFederationConsumerPlugin = () => {
  const ports = fsx.readJSONSync(pathUtils.getPortsJsonPath());
  const external = process.env.NODE_ENV === 'dev' 
    ? `new Promise(resolve=>resolve('http://localhost:${ports['@micro/modules']}/assets/remoteEntry.js'))` 
    : `new Promise(resolve=>resolve('/v2/micro/modules/assets/remoteEntry.js'))`;
    
  return federation({
    name: 'layout',
    filename: 'remoteEntry.js',
    remotes: {
      modules: {
        external,
        externalType: "promise"
      }
    }
  });
};

消费端配置之后即可在主或子应用里面通过这种形式引入

js 复制代码
// 引用共享的Vue
import { createApp } from 'modules/vue';
// 引用共享的Vue Router
import { createRouter } from 'modules/vue-router';
// 引用共享的组件
import { SomeComponent } from 'modules/@shared/components';

同时为了提供TS提示的能力需要在当前应用的根目录新建一个tsconfig.json

js 复制代码
{
  "extends": "@shared/tsconfig",
  "compilerOptions": {
    "paths": {
      "modules/*": ["../../micro/modules/src/*"],
      "shared/*": ["../../shared/*"],
    }
  }
}

3.3 微前端的实现

主应用配置

js 复制代码
// apps/main/src/main.ts
microApp.start({
  'router-mode': 'native',
  prefetchDelay: 1000,
  preFetchApps: MicroUtils.getMicroPreFetch(),
  plugins: {}
});

子应用加载

js 复制代码
<!-- apps/main/src/components/MicroApp/index.vue -->
<template>
  <MicroAppDevMode 
    v-if="envUtils.isDev()" 
    :app-name="props.appName" 
    :init-data="props.initData" 
    @mounted="microAppMounted" 
  />
  <MicroAppProdMode 
    v-else 
    :app-name="props.appName" 
    :init-data="props.initData" 
  />
</template>

4️⃣ 开发流程

4.1 开发环境启动

开发环境启动时,只会启动共享模块服务和主应用:

js 复制代码
/ scripts/models/commands/dev.ts
async devRoot() {
  const modules = ProjectFactory.create(
    EProjectType.Provider, 
    pathUtils.getAppProjectDir('@micro/modules')
  );
  await modules.microModulePreview();
  const mainApp = ProjectFactory.create(
    EProjectType.Consumer, 
    pathUtils.getAppProjectDir('@apps/main')
  );
  mainApp.serve();
}

在开发环境下,子应用不会在启动时全部加载,而是采用按需加载的方式

子应用启动流程:

js 复制代码
// apps/main/src/components/MicroApp/MicroAppDevMode.vue
onMounted(() => {
  if (!props.appName) return null;
  // 检查子应用是否已经启动
  if (getAllApps().includes(props.appName)) {
    loading.value = false;
    return null;
  }
  // 发送启动子应用的请求
  import.meta.hot.send({ 
    type: EAppDevSocketType.StartApp, 
    data: props.appName 
  });
  // 监听子应用启动完成事件
  import.meta.hot.on(EAppDevSocketType.AppStarted, startAppChange);
});

vite写一个服务器插件去监听客户端启动子应用的请求

子应用启动流程总结

  • 主应用启动时,只启动共享模块服务
  • 当用户访问子应用路由时,触发子应用加载
  • 主应用通过 WebSocket 发送启动子应用的请求
  • 子应用开发服务器启动,并通知主应用
  • 主应用收到通知后,加载子应用组件
  • 子应用组件通过 micro-app 加载子应用内容

构建流程

共享模块的构建
js 复制代码
// scripts/models/vite/configuration/provider-build.ts
export const ProviderBuildConfiguration = () => {
  return defineConfig({
    plugins: ViteBase.plugins?.concat([
      ModuleFederationProviderPlugin(),
      topLevelAwait({
        promiseExportName: "__tla",
        promiseImportName: i => `__tla_${i}`
      })
    ]),
    build: {
      minify: 'terser',
      terserOptions: {
        mangle: true,
        compress: {
          drop_console: true
        }
      }
    }
  });
};
主子应用的构建
js 复制代码
export const ConsumerBuildConfiguration = () => {
  return defineConfig({
    plugins: ViteBase.plugins?.concat([
      RedirectVueImports(),
      ModuleFederationConsumerPlugin()
    ]),
    optimizeDeps: {
      exclude: ['vue']
    },
    build: ViteBase.build,
    mode: 'production',
    css: ViteBase.css,
    resolve: ViteBase.resolve
  });
};

5️⃣ 实践效果

5.1 开发效率提升

  • 开发环境启动时间缩短 70%
  • 构建效率提升 50%
  • 代码复用率提升
  • 团队协作更顺畅

5.2 资源利用优化

  • 按需加载减少不必要的资源消耗
  • 共享模块减少重复代码
  • 构建产物优化,减少包体积

结语

如果想尝试微前端又想把公用资源进行抽离可以尝试一下这套解决方案,具体代码参考github.com/chaorenluo/...

相关推荐
烛阴7 分钟前
Puppeteer入门指南:掌控浏览器,开启自动化新时代
前端·javascript
全宝1 小时前
🖲️一行代码实现鼠标换肤
前端·css·html
小小小小宇1 小时前
前端模拟一个setTimeout
前端
萌萌哒草头将军1 小时前
🚀🚀🚀 不要只知道 Vite 了,可以看看 Farm ,Rust 编写的快速且一致的打包工具
前端·vue.js·react.js
芝士加2 小时前
Playwright vs MidScene:自动化工具“双雄”谁更适合你?
前端·javascript
Carlos_sam3 小时前
OpenLayers:封装一个自定义罗盘控件
前端·javascript
前端南玖3 小时前
深入Vue3响应式:手写实现reactive与ref
前端·javascript·vue.js
wordbaby4 小时前
React Router 双重加载器机制:服务端 loader 与客户端 clientLoader 完整解析
前端·react.js
itslife4 小时前
Fiber 架构
前端·react.js
3Katrina4 小时前
妈妈再也不用担心我的课设了---Vibe Coding帮你实现期末课设!
前端·后端·设计