在 Vite 中实现模块联邦:Vue 项目与 React 组件的无缝通信

3,469 阅读6分钟

前言:什么是模块联邦?

模块联邦(Module Federation)使多个独立构建的应用或组件能够共享同一个代码库。这种策略彻底革新了传统的前端开发模式,提供了更高的灵活性和可扩展性。

在微前端架构中,尤其显著,因为它允许各个前端应用独立运行,各自服务于不同的业务需求。通过模块联邦,这些应用可以在运行时动态共享组件和数据,极大地增强了模块间的互操作性。

基础知识

  • 消费应用:在模块联邦中扮演宿主的角色,主要负责加载和调用远程应用的模块。
  • 远程应用: 表现为独立的微应用,它不仅可以暴露自己的模块供其他消费应用使用,还能加载和利用来自其他应用的模块。

需求背景

在现今的开发中,模块联邦主要被用来复用组件库,这让技术团队能够更高效地利用现有资源。市面上不乏有教程去教学在同一技术栈内复用组件。但跨技术栈的组件复用?那就不那么常见了。

想象一下,如果你的公司已经有了一个基于 React 的特定组件库,Vue 项目能不能也来分一杯羹,直接利用这些组件,省去重新开发的麻烦呢?

实际上,这种思路其实并不新鲜,解决方案也有很多。比如,有些工具可以帮你把 React 组件转换成 Vue 项目能识别的格式。但这往往意味着要对代码进行些许修改。这时候,模块联邦就能大显身手,它可以让你在不侵入原有代码的情况下,轻松实现这一需求。

技术实现

目录结构

image.png

对于 Remote-react模块作为远程应用,需要先进行构建然后部署到线上服务器,以便为其他所有应用提供服务。

Host-vue 模块则扮演消费应用的角色,能够引入本地开发的组件或者是线上已部署的远程应用组件。这种架构允许 Host-vue 模块灵活地整合多个来源的组件,从而增强应用的功能性和响应速度。

Remote 应用

首先,我们通过 Vite 创建一个 React 项目,然后安装 @originjs/vite-plugin-federation 插件。

安装完成后,我们可以开始编写组件。使得组件能够被远程访问和共享。

import React from "react";

interface ButtonProps {
  handleClick: (count: number) => void;
  title: string;
  count: number;
}

const Component: React.FC<ButtonProps> = ({
  handleClick,
  title,
  count: pureCount,
}) => {
  const [count, setCount] = React.useState(pureCount);

  function handleRemoteClick() {
    console.log("remote click in React");
    setCount(count + 1);
    handleClick(count);
  }

  return (
    <>
      <div>
        <h3>common element</h3>
        <button onClick={handleRemoteClick}>
          {title} - {count}
        </button>
      </div>
    </>
  );
};

export default Component;

Remote 配置

然后在 vite.config.ts 中进行配置

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

// https://vitejs.dev/config/
export default defineConfig({
  // 打包配置
  build: {
    target: "esnext",
    minify: false,
    cssCodeSplit: false,
  },
  plugins: [
    react(),
    reactRefresh(),
    //模块联邦配置
    federation({
      //定义模块服务名称
      name: "remoteReactComponents",
      //build后的入口文件
      filename: "remoteEntry.js",
      //需要暴露的组件
      exposes: {
        "./Button": "./src/components/Button.tsx",
      },
      //声明共享的依赖库
      shared: ["react", "react-dom"],
    }),
  ],
});

Host 应用

当远程应用编译完成后,它会生成一个 JavaScript 文件,供消费应用如 Host-vue 使用。

为了在 Vue 项目中集成 React 组件,首先安装@originjs/vite-plugin-dederation插件。

安装完成后,我们将利用 React 的渲染函数把组件渲染成 HTML。下面的示例展示了如何在 Vue 中加载和显示 React 组件:

<script setup lang="ts">
import { DefineComponent, onMounted, onUnmounted, ref, useAttrs } from "vue";
import React from "react";
import { createRoot } from "react-dom/client";
import SuspenseWithError from "./suspenseWithError.vue";

const attrs = useAttrs(); // 获取传递给组件的属性

const reactRef = ref<any>(); // 用于存储 React 根实例
const reactComponent = ref<DefineComponent<{}, {}, any>>(); // 用于存储加载的 React 组件

onMounted(() => {
  renderReactComponents(); // 在组件挂载时渲染 React 组件
});

onUnmounted(() => {
  if (reactRef.value) {
    reactRef.value.unmount(); // 在组件卸载时卸载 React 组件
  }
});

// 异步加载 React 组件
async function loadReactComponent() {
  try {
    // 使用 Module Federation 加载 React 组件
    const Component = (await import("remoteReactComponents/Button")).default;
    return Component;
  } catch (error) {
    console.error("Failed to load React component:", error);
    throw error;
  }
}

// 渲染 React 组件
async function renderReactComponents() {
  // 使用 createRoot API  创建 React 根实例
  reactRef.value = createRoot(reactComponent.value as any);
  // 加载 React 组件
  reactComponent.value = await loadReactComponent();
  // 渲染 React 组件到页面中并传递属性
  reactRef.value.render(
    React.createElement(reactComponent.value, mappedProps(attrs))
  );
}

// 映射属性函数
const mappedProps = <T extends Record<string, any>>(props: T): T => {
  return Object.keys(props).reduce((acc: T, key: string) => {
    return { ...acc, [key]: props[key] };
  }, {} as T);
};
</script>

<template>
  <div>
    <SuspenseWithError>
      <!-- 错误状态插槽 -->
      <template #error="props">
        <h1>{{ props.error }}</h1>
      </template>
      <!-- 默认插槽 -->
      <template #default>
        <div ref="reactComponent" />
      </template>
      <!-- 加载状态插槽 -->
      <template #fallback>
        <h1>Loading please wait...</h1>
      </template>
    </SuspenseWithError>
  </div>
</template>

<style scoped></style>

SuspenseWithError 是一个用于处理远程组件加载时的延迟和错误的二次封装组件。

因为请求远程组件通常需要一定的等待时间,使用 Suspense 可以优雅地管理这种异步加载过程,并提供一个加载状态的界面。同时,SuspenseWithError 还增加了错误处理功能,以确保在加载过程中出现问题时,我们能够获得恰当的反馈。这种封装方法提高了用户体验和应用的健壮性。

SuspenseWithError

<script setup lang="ts">
import { onErrorCaptured, ref } from "vue";

const error = ref<boolean | void>();

onErrorCaptured((err: boolean | void) => (error.value = err));
</script>

<template>
  <div>
    <slot name="error" :error="error" v-if="error" />
    <Suspense v-else>
      <template #default>
        <slot name="default" />
      </template>
      <template #fallback>
        <slot name="fallback" />
      </template>
    </Suspense>
  </div>
</template>

<style scoped></style>

Host 配置

然后在 vite.config.ts 中进行配置

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

// https://vitejs.dev/config/
export default defineConfig({
  build: {
    minify: false,
    target: ["chrome89", "edge89", "firefox89", "safari15"],
  },
  plugins: [
    vue(),
    federation({
      //定义模块服务名称
      name: "host",
      //build后的入口文件
      filename: "remoteEntry.js",
      //远程服务地址
      remotes: {
        //vue 组件的远程模块
        remoteVueComponents: "http://localhost:3002/assets/remoteEntry.js",
        // react组件的远程模块
        remoteReactComponents: "http://localhost:3001/assets/remoteEntry.js",
      },
      //共享依赖声明
      shared: {
        vue: {},
      },
    }),
  ],
});

组件使用

最终,我们可以跟操作本地组件一样,通过Props向这些远程React组件传递数据,实现跨框架的无缝通信

<script setup lang="ts">
import remoteReactBtn from "./components/reactComponents.vue";

function handleHostReactClick(value:number) {
  console.log("value",value);
}
</script>

<template>
    <h1>Host Application</h1>
    <h3>Remote Components in React</h3>
    <remote-react-btn title="host-in-react" :count="10" :handleClick="handleHostReactClick"/>
</template>

<style scoped></style>

注意事项

image-1.png

这个错误是因为我们在 vite 的配置中没有配置支持顶级 await 语法的目标环境。顶级 await 是从 ES2022 开始支持的,但我们当前的配置目标环境不支持这个特性。

我们需要更新 vite.config.js 文件来设置更高版本的目标环境,例如 es2022 或 esnext。

esbuild: {
  target: "esnext";
}

image.png

这个错误是因为我们没有在消费应用和远程应用中配置共享依赖

      shared: {
        react: {},
        "react-dom": {},
      },

预览

mnggiflab-video-to-gif.gif

实现原理

graph TD;
    A[启动 Vue 应用] 
    A --> C[在 Vue 应用中定义远程模块的访问点]
    C --> D[构建 React 应用]
    D --> E[部署 React 应用到服务器]
    E --> F[定义远程模块的来源]
    F --> G[加载 React 组件]

  • Vue 应用中定义远程模块的访问点: 这一步是在消费应用(Host)进行。主要目的是为了配置如何连接和加载远程应用中暴露的模块。这包括需要配置的应用的url地址,以及模块信息等。示例配置(在Host应用中vite.config.ts中)
  • 定义远程模块来源: 这一步是在远程应用(Remote)中进行,其目的是为了配置原因应用,使其可以被其他应用通过网络访问,这包括设置暴露的模块、入口文件、共享依赖等。示例配置(在Remote应用中vite.config.ts中)

后记

模块联邦不仅限于原生组件的共享,还能继承例Ant Design 等UI组件库,以及全局数据管理工具,这种技术为跨框架应用提供了卓越的灵活性和扩展性。

仓库地址 :github.com/dadaguai-gi…