微前端的“联邦”起义:Vite版之“散装”革命

463 阅读5分钟

听说你要把项目拆得七零八落?
别慌!这不是代码界的“离婚现场”,而是微前端的“联邦制”——
Vite 举着“模块联邦”的大旗高喊:
“拆,随便拆!拆完我还能让它们‘藕断丝连’!”

微前端的分类

  • 基于 NPM 包的微前端:将微应用打包成独立的 NPM 包,然后在主应用中安装和使用;
  • 基于代码分割的微前端:在主应用中使用懒加载技术,在运行时动态加载不同的微应用;
  • Web Components 的微前端:将微应用封装成自定义组件,在主应用中注册使用;
  • 模块联邦(Module Federation) 的微前端:借助 Webpack 5 的 Module Federation 实现微前端;
  • 动态 Script 的微前端:在主应用中动态切换微应用的 Script 脚本来实现微前端;
  • iframe 的微前端:在主应用中使用 iframe 标签来加载不同的微应用;
  • 基于框架(JavaScript SDK)的微前端:使用 single-spa、qiankun、wujie 等通用框架

什么是联邦模块

联邦模块最开始是由webpack提出来的Module Federation | webpack,初衷是下面所示

综上,联邦模块就是分为本地模块和远程模块,远程模块将其导出,本地模块将其导入,直接使用远程模块,远程模块可以是单体应用的一个组件甚至还可以是本身的当前单体应用,当远程模块导出的就是单体应用的时候就是传统意义上的微前端 [多个单独的构建应用(不限技术栈)组成一个应用程序]。

首先初始化项目pnpm init,配置所有项目的依赖安装等统一配置,然后通过 pnpm create vite创建React版本的Host应用以及两个React和Vue版本的Remote应用。

配置Remote应用

remote-react
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import federation from "@originjs/vite-plugin-federation";

// https://vite.dev/config/
export default defineConfig({
  plugins: [
    react(),
    federation({
      name: "remote_react",
      filename: "remoteEntry.js",
      exposes: {
        "./ReactApp": "./src/App.tsx",
        "./HelloWorld": "./src/components/HelloWorld.tsx",
      },
      shared: ["react", "react-dom"],
    }),
  ],
  build: {
    target: 'esnext',
  },
});
remote-vue
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import federation from "@originjs/vite-plugin-federation";

// https://vite.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    federation({
      name: "remote_vue",
      filename: "remoteEntry.js",
      exposes: {
        "./VueApp": "./src/App.vue",
      },
      shared: ["vue"],
    }),
  ],
  build: {
    target: "esnext",
  },
});

联邦模块配置(remote)

  • name: 远程模块名称
  • filename: 远程模块入口文件名,默认为remoteEntry.js
  • exposs: 设置对外暴露的模块名称
  • shared: 本地模块和远程模块共享的依赖

配置Host应用

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
import federation from "@originjs/vite-plugin-federation";

// https://vite.dev/config/
export default defineConfig({
  plugins: [
    react(),
    federation({
      name: "host-app",
      remotes: {
        remote_vue: "http://localhost:3001/dist/assets/remoteEntry.js",
        remote_react: "http://localhost:3002/dist/assets/remoteEntry.js",
      },
      shared: ["react", "react-dom", "vue"],
    }),
  ],
  build: {
    target: 'esnext',
  },
});

联邦模块配置(host)

  • remotes: 在本地模块中引入远程模块,

实践

以上就已经配置好了一个React版本的Host应用,以及React和Vue版本的Remote应用。

接下来就使用Host应用去引用远程模块。

引用React远程模块

由于Host应用本身就是一个React应用,本身没有难度,可以直接引用。

const ReactApp = lazy(() => import('remote_react/ReactApp'));

 <Suspense fallback={<div>Loading...</div>}>
    <ReactApp />
</Suspense>
引用Vue远程模块

也是由于Host应用是React应用的缘故,在React里面使用Vue会出现问题,需要将Vue进行挂载到React应用上。

import { useEffect, useRef } from 'react';
import { createApp, defineAsyncComponent, h } from 'vue';

// 创建一个React组件,用于加载Vue组件
const VueComponent: React.FC = () => {
  const containerRef = useRef<HTMLDivElement>(null);
  const vueAppRef = useRef<ReturnType<typeof createApp>>(null);

  useEffect(() => {
    // 确保只在DOM元素存在时创建Vue应用
    if (!containerRef.current) return;

    // 清理之前的Vue实例
    if (vueAppRef.current) {
      vueAppRef.current.unmount();
    }

    const loadVueComponent = async () => {
      try {
        // 异步加载远程Vue组件
        const VueApp = defineAsyncComponent(() => 
          import('remote_vue/VueApp')
        );

        // 创建新的Vue应用实例
        const app = createApp({
          render() {
            // 使用Vue的h函数创建虚拟DOM,而不是JSX
            return h(VueApp);
          }
        });

        // 挂载Vue应用 - 确保containerRef.current不为null
        if (containerRef.current) {          
          app.mount(containerRef.current);
          vueAppRef.current = app;
        }
      } catch (error) {
        console.error('Failed to load Vue component:', error);
      }
    };

    loadVueComponent();

    // 清理函数
    return () => {
      if (vueAppRef.current) {
        vueAppRef.current.unmount();
      }
    };
  }, []);

  return <div ref={containerRef} className="vue-component-container"></div>;
};

export default VueComponent;
思考

假如需要本地模块和远程模块进行通信怎么办?

  • 本地模块与远程模块通信 (相当于父组件与子组件进行通信),可以用props进行通信
  • 远程模块与本地模块通信 (相当于子组件与父组件进行通信),可以用方法回调进行通信
  • 远程模块与远程模块通信 (相当于子组件与子组件进行通信),可以通过浏览器存储localStorage、sesionStorage、Session通信,或者先将数据传入本地模块,然后由本地模块传入远程模块(感觉稍微麻烦)

预览

注意

Host应用里面的样式可能会将Remote应用里面的样子覆盖,造成样式污染的问题

  • css模块化
  • css原子化
  • css in js

本地模块调用远程模块,触发事件会导致本地模块重新渲染,需要将远程模块使用ref.current去挂载,控制只存在一个实例。

总结

一个微前端的搭建主要考虑的就是css, js的隔离,将子应用融合在主应用中,并考虑其中如何实现数据之间的通信,相对来说,使用联邦模块搭建的微前端更加容易实现,而且相对于传统一样上的微前端,联邦模块的去中心化思想更加深得吾心。

Vite模块联邦通过动态模块加载和依赖共享实现微前端架构,还可以组件直接进行复用,可以进行性能优化,并且淡化了微前端的主子应用的区分,实现了去中心化,任何应用都可以承载主应用的职责。

完整代码: github.com/krismile-su…

技术参考:

子弈大佬的《深入浅出微前端》小册

Webpack Module Federation

vite-plugin-federation