前言:什么是模块联邦?
模块联邦(Module Federation)使多个独立构建的应用或组件能够共享同一个代码库。这种策略彻底革新了传统的前端开发模式,提供了更高的灵活性和可扩展性。
在微前端架构中,尤其显著,因为它允许各个前端应用独立运行,各自服务于不同的业务需求。通过模块联邦,这些应用可以在运行时动态共享组件和数据,极大地增强了模块间的互操作性。
基础知识
- 消费应用:在模块联邦中扮演宿主的角色,主要负责加载和调用远程应用的模块。
- 远程应用: 表现为独立的微应用,它不仅可以暴露自己的模块供其他消费应用使用,还能加载和利用来自其他应用的模块。
需求背景
在现今的开发中,模块联邦主要被用来复用组件库,这让技术团队能够更高效地利用现有资源。市面上不乏有教程去教学在同一技术栈内复用组件。但跨技术栈的组件复用?那就不那么常见了。
想象一下,如果你的公司已经有了一个基于 React 的特定组件库,Vue 项目能不能也来分一杯羹,直接利用这些组件,省去重新开发的麻烦呢?
实际上,这种思路其实并不新鲜,解决方案也有很多。比如,有些工具可以帮你把 React 组件转换成 Vue 项目能识别的格式。但这往往意味着要对代码进行些许修改。这时候,模块联邦就能大显身手,它可以让你在不侵入原有代码的情况下,轻松实现这一需求。
技术实现
目录结构
对于 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>
注意事项
这个错误是因为我们在 vite 的配置中没有配置支持顶级 await 语法的目标环境。顶级 await 是从 ES2022 开始支持的,但我们当前的配置目标环境不支持这个特性。
我们需要更新 vite.config.js 文件来设置更高版本的目标环境,例如 es2022 或 esnext。
esbuild: {
target: "esnext";
}
这个错误是因为我们没有在消费应用和远程应用中配置共享依赖
shared: {
react: {},
"react-dom": {},
},
预览
实现原理
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…