vite联邦实现微前端(vite-plugin-federation)

使用vite-plugin-federation实现微前端的搭建开发

使用背景

老板说了,项目比较大,不好维护要拆分成多个子项目,这样每次维护发包时候及时报错了也不会影响其他的项目(我现在用的是microapp,因为之前他们用的iframe拆分了,只有弹框问题不好解决,用microapp改造下是最快的,插件那块microapp嵌套报错用了联邦实现了)。

注意:联邦搭建微前端适合新项目,如果已经有用iframe拆分实现微前端过的老项目还是用microapp比较好,啥都不用改直接引入就能实现微前端。

  • 官方描述的是去中心画,我还是按照老板现在想法实现1+n+n,
  1. 1个框架(登录,接口封装,layout布局等)只要搭建好基本上不会在改的(新开项目这个也直接能拿去用)
  2. n个应用(大屏,多个后台管理模块)就是改动相对较少的
  3. n个插件(要给不同客户部署,他们都会提自己需求,例如大屏的某块展示,客户不提我们就展示默认的,客户提了就渲染他们个性化的内容)

使用方法

  1. 首先创建两个项目:base,运营管理 全是vite+vue3 pnpm create vue
  2. 安装联邦pnpm add @originjs/vite-plugin-federation --save-dev
  3. 官方使用方法
  4. base项目可以dev启动,运营管理必须build之后用preview预览
js 复制代码
// vite.config.js 运营管理
import federation from "@originjs/vite-plugin-federation";
export default {
    plugins: [
        federation({
            name: 'remote-app',
            filename: 'remoteEntry.js',
            // Modules to expose
            exposes: {
                './Button': './src/Button.vue',
            },
            shared: ['vue']
        })
    ]
}
js 复制代码
// vite.config.js base项目
import federation from "@originjs/vite-plugin-federation";
export default {
    plugins: [
        federation({
            name: 'host-app',
            remotes: {
                remote_app: "http://localhost:5001/assets/remoteEntry.js",
            },
            shared: ['vue']
        })
    ]
}
vue 复制代码
// 页面使用
const RemoteButton = defineAsyncComponent(() => import("remote_app/Button"));
<template>
    <div>
        <RemoteButton />
    </div>
</template>

推荐使用方法(超级坑:他们中文文档没有介绍,找了好久发现写在了英文文档的最下面)

用上面这个方法,在做定制化时候,不好用,例如后台配置个性化接口时候,前端自动通过接口展示对应的个性化。所以需要有个能够动态加载组件方法。

运营管理那块导出不用修改,base的导入方式需要修改下

js 复制代码
// vite.config.js base项目
federation({
  remotes:{
    "None": "" //这个不加就报错,他们issues里找到的别人的解决办法
  },
  shared: ['vue', 'pinia', 'vue-router'],
}),

先定一个公共的util方法获取动态组件和方法

js 复制代码
//util.ts
import {
  __federation_method_getRemote as getRemote,
  __federation_method_setRemote as setRemote,
  __federation_method_unwrapDefault as unwrap,
} from 'virtual:__federation__'

interface RemoteOptions {
  url: string
  moduleName: string,
  type?: 'ts' | 'component'
}

export const getRemoteComponent = async (options: RemoteOptions): Promise<any> => {
  try {
      const { url, moduleName, type = 'component' } = options
      const remoteName = `remote_${Math.random().toString(36).slice(2)}`
      // 1. 注册 remote 信息
      setRemote(remoteName, {
        url: () => Promise.resolve(url),
        format: 'esm',
        from: 'vite',
      })

      // 2. 加载模块
      const mod = await getRemote(remoteName, `./${moduleName}`)
      console.log('======', type)
      if(type === 'ts') return mod
      // 3. 解包模块
      const Comp = await unwrap(mod)

      return Comp
  } catch (error) {

  }
}
js 复制代码
//使用导出的方法
const util = await getRemoteComponent({
    url: 'http://localhost:20001/assets/remoteEntry.js', 
    moduleName: 'Util',
    type: 'ts'
})
console.log('util', util?.add(1, 2))
//引入组件
const remoteButton = getRemoteComponent({
  url: 'http://localhost:5001/assets/remoteEntry.js',
  moduleName: "Button"
})
<template>
<Suspense>
  <!-- 具有深层异步依赖的组件 -->
  <remoteButton />

  <!-- 在 #fallback 插槽中显示 "正在加载中" -->
  <template #fallback>
    Loading...
  </template>
</Suspense>
</template>

这样就可以通过base里面设置动态路由来加载不同子项目的组件了

我的示例

base项目内容

  1. 登录页面
  2. layout布局
  3. 获取用户菜单动态加载菜单路由
  4. 一些他自己的页面,用户管理,角色管理等
js 复制代码
//模拟的用户菜单
export const getMenuList = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve([
        {
          id: 1,
          title:'运营管理',
          children:[
            {
              id: 11,
              title:'车辆管理',
              name: 'CarManage',
              path: '/business/CarManage',
              component: 'CarManage',
              source: 'http://localhost:20001/assets/remoteEntry.js'
            },
            {
              id: 12,
              title:'车辆详情',
              name: 'CarDetail',
              path: '/business/CarDetail',
              component: 'CarDetail',
              source: 'http://localhost:20001/assets/remoteEntry.js'
            }
          ]
        },
        {
          id: 2,
          title:'用户管理',
          children:[
            {
              id: 21,
              title:'用户列表',
              name: 'UserList',
              path: '/user/UserList',
              component: '/user/UserListView',
            },
            {
              id: 22,
              title:'用户角色',
              name: 'UserRole',
              path: '/user/UserRole',
              component: '/user/UserRoleView',
            }
          ]
        }
      ])
    }, 1000)
  })
}
js 复制代码
/**
 * 动态加载组件(支持本地和远程组件)
 */
const modules = import.meta.glob("../views/**/*.vue")
export function loadDynamicComponent(menu: MenuType) {
  if (menu.source && menu.component) {
    // 远程组件加载
    return () => getRemoteComponent({
      url: menu.source as string,
      moduleName: menu.component
    })
  } else if (menu.component) {
    // 本地组件加载 - 使用 import.meta.glob
    let componentPath: string

    if (menu.component.startsWith('/')) {
      // 如果是路径格式 (如 /user/UserList),转换为相对路径
      componentPath = `../views${menu.component}.vue`
    } else {
      // 如果是组件名格式 (如 UserList),添加路径前缀
      componentPath = `../views/${menu.component}.vue`
    }

    const moduleLoader = modules[componentPath]

    if (moduleLoader) {
      return moduleLoader
    } else {
      console.warn(`组件未找到: ${componentPath},可用组件:`, Object.keys(modules))
      return () => Promise.resolve({
        template: '<div>组件未找到</div>'
      })
    }
  }
  return undefined
}

运营管理内容

只需要把要导出的东西都导出就行

javascript 复制代码
export const remoteExport = {
  CarManage: 'src/views/CarManage/CarManage.vue',
  CarDetail: 'src/views/CarManage/CarDetail.vue',
  Util: 'src/utils/index.ts'
}
// vite.config.ts
federation({
  name: 'remote-business',
  filename: 'remoteEntry.js',
  exposes: Object.fromEntries(
    Object.entries(remoteExport).map(([key, value]) => [`./${key}`, value])
  ),
  shared: ['vue', 'pinia', 'vue-router']
}),

到现在已经实现了微前端了

开发时候的优化

现在子项目必须build之后才能生成remoteEntry,也就没办法热更新。

可以使用通知,base项目全局刷新,实现伪热更新效果

参考下下面的插件

ts 复制代码
interface Options {
  role: 'remote' | 'host';
  host?: string;
}
export default function syncReloadPlugin(options: Options) {
  const role = options.role;
  const hostUrl = options.host;

  return {
    name: 'vite-plugin-sync-reload',

    apply(config: any, { command }: any) {
      if (role !== 'remote') return 'dev';
      return Boolean(command === 'build' && config.build?.watch);
    },

    async buildEnd(error: any) {
      if (role !== 'remote') return;
      if (error) return;

      try {
        await fetch(`${hostUrl}/__fullReload`);
        console.log(`[remote] 已通知 host 刷新`);
      } catch (e) {
        console.log(`[remote] 通知 host 失败(可能 host 未启动)`);
      }
    },

    configureServer(server: any) {

      if (role !== 'host') return;

      server.middlewares.use((req: any, res: any, next: any) => {

          // remote build 后会访问这里
          if (req.url === '/__fullReload') {

            console.log('[host] 收到 remote 通知,即将刷新页面');

            // 触发浏览器刷新
            setTimeout(() =>{
              server.hot.send({
                type: 'full-reload'
              });
            },100)

            res.end('Full reload triggered');
          } else {
            next(); // 继续下一个中间件
          }
        });
    }
  };
}

syncReloadPlugin({ role: 'host' }),
syncReloadPlugin({
  role: 'remote',
  host: 'http://localhost:20000'
})

我的示例代码

一个联邦实现微前端的示例代码 : github.com/5563/federa...

相关推荐
丸子哥哥38 分钟前
同一个域名,如何添加多个网站?
服务器·前端·nginx·微服务
伍亿伍千万40 分钟前
Uptime Kuma修改作为内嵌页面的自适应
前端
Heo42 分钟前
原来Webpack在大厂中这样进行性能优化!
前端·javascript·vue.js
涔溪42 分钟前
Vue2 项目中通过封装 axios 来同时连接两个不同的后端服务器
前端·vue.js·axios
Codebee1 小时前
SOLO+OODER全栈框架:图生代码与组件化重构实战指南
前端·人工智能
颜酱1 小时前
CLI 工具开发的常用包对比和介绍
前端·javascript·node.js
Chen不旧1 小时前
关于用户权限的设计,前端和后端都需要考虑
前端·权限
DsirNg1 小时前
前端和运维的哪些爱
前端
7***31881 小时前
Go-Gin Web 框架完整教程
前端·golang·gin