前言
模块联邦(Module Federation)是一种前端技术,允许不同构建的应用之间共享模块。类似于微服务架构在前端的体现。通过模块联邦,可以实现跨应用的代码共享,这对于构建微前端架构非常有用。
- 远程应用(Remote Application):暴露模块的应用,其他应用可以引用这些模块。
- 宿主应用(Host Application):引用远程应用模块的应用。
背景
前不久,临时接到个需求,想要实现串联多个系统,能够实现自由切换又能分开单独运行,也要数据共享也要相互引用(实在是既要又要还要,十分符合当下产品的设计思维)。无奈谁还不是头牛马,只能绞尽脑汁造了。 好在平常的知识储备还是有的,monorepo、微前端、模块联邦顿时打入脑海,由于之前微前端和monorepo都搞过了,勉为其难的选择了模块联邦。于是开始着手打开各种搜索引擎各种Ai开始研究,没成想,越研究还觉得越对味哈哈哈哈!😀😀😀😀😀😀
一、架构设计

1、系统概念: 每个独立的前端项目都为一个独立系统;
2、系统互通: 能相互跳转(实现SSO单点登录),系统能访问其他系统的组件;
3、插件: 类似工具类,可提供各个系统/组件使用,可考虑独立库方式实现;
二、场景及目标
(一) 应用场景
系统太多且割裂,假设当前有多个系统主系统、系统2、...、系统N,且分别由多个不同的代码仓库管理
(二) 目标效果
- 多个系统既能集成运行又能单独运行;
- 数据共享和统一登录;
- 系统互认,各系统之间能够相互引用;
三、技术实现
(一) 技术选型
- 技术栈 vue3+pinia+vite
- ui框架 element-plus + Geeker Admin
- 核心插件 @originjs/vite-plugin-federation( 模块联邦 )
(二) 项目创建

首先以Geeker Admin 为脚手架模板(远程拉取),分别创建三个系统项目main-system、system2和system3,并分别安装依赖
shell
pnpm install
接着在每个项目中分别安装上模块联邦插件
注:建议安装1.3.6版本,最新版本有 样式丢失的问题
shell
pnpm install @originjs/vite-plugin-federation@1.3.6 -D
安装完事后,可以分别运行,看看效果
注:路由模式建议使用history模式, 刷新404问题可以通过nginx配置解决
shell
pnpm dev
- 主系统

- 系统2

- 系统3

(三) 具体实现

首先,先确定功能的host(应用者)跟remote(提供者)。
比如在主系统上开发设计一个系统菜单组件,供所有系统使用,那么在这个功能上主系统就是remote,其他系统都是host。
- 将系统菜单组件暴露出去:
ts
// main-system 中 vite.config.ts
import federation from "@originjs/vite-plugin-federation";
export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
...
return {
...
plugins:[
...,
federation({
name: "remote_main", // 远程模块
filename: "remoteEntry.js", // 远程资源路径
exposes: { // 暴露的组件
"./SysNavComp": "./src/remote/components/SysNav/index.vue"
},
shared: ["vue", "vue-router", "pinia"] // 共享的依赖
}),
]
}
- 其他系统需要跟这个资源包建立远程连接
js
// 系统2、3 的vite.config.ts
import federation from "@originjs/vite-plugin-federation";
export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
...
return {
...
plugins:[
...,
federation({
remotes: { // 远程模块跟地址
remote_main: "主系统服务地址/remoteEntry.js"
},
shared: ["vue", "vue-router", "pinia"] // 共享的依赖
}),
]
}
- 完事后就可以在页面中引入
js
<template>
<el-config-provider :locale="locale" :size="assemblySize" :button="buttonConfig">
<SysNavComp />
<router-view></router-view>
</el-config-provider>
</template>
<script setup lang="ts">
import { defineAsyncComponent } from "vue";
...
const SysNavComp = defineAsyncComponent(() => import("remote_main/SysNavComp"));
</script>
大致效果如下,实现各个系统既能独立运行又能自由切换:
(四) 功能拓展
- 主系统除了能将功能暴露出去供其他系统使用外,其他系统也能提供组件、页面或者方法供主系统使用。
js
// 子系统 vite.config,ts
import federation from "@originjs/vite-plugin-federation";
export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
...
return {
...
plugins:[
...,
federation({
remotes: { // 远程模块跟地址
remote_main: "主系统服务地址/assets/remoteEntry.js"
},
exposes: {
"./HomePage": "./src/views/home/index.vue",
"./UseProTablePage": "./src/views/proTable/useProTable/index.vue"
},
shared: ["vue", "vue-router", "pinia"] // 共享的依赖
}),
]
}
在主系统中应用
js
// 主系统 vite.config.ts
import federation from "@originjs/vite-plugin-federation";
export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
...
return {
...
plugins:[
...,
federation({
name: "remote_main", // 远程模块
filename: "remoteEntry.js", // 远程资源路径
exposes: { // 暴露的组件
"./SysNavComp": "./src/remote/components/SysNav/index.vue"
},
remotes: {
remote_main1: "系统2服务地址/remoteEntry.js",
},
shared: ["vue", "vue-router", "pinia"] // 共享的依赖
}),
]
}
js
<template>
<div class="home card">
<HomePageComp>
<p>hello world</p>
</HomePageComp>
<UseProTablePageComp />
</div>
</template>
<script setup lang="ts" name="home">
import { defineAsyncComponent } from "vue";
const HomePageComp = defineAsyncComponent(() => import("remote_main1/HomePage"));
const UseProTablePageComp = defineAsyncComponent(() => import("remote_main1/UseProTablePage"));
</script>
<style scoped lang="scss">
@import "./index";
</style>
大致效果

- 后续可以将一些公共的js代码抽离出来做成插件包,供各个系统使用。
(五) 测试效果
本地体验地址: http://192.168.120.178:8882/home。
四、数据共享
- 本地缓存
- 跨窗口通讯
- ......
!!!前提条件得符合浏览器的同源协议。
五、打包部署
将各个系统分别进行打包
shell
pnpm build:pro
除了主项目,其他子项目打包后的bundle文件夹名可以约定以 'sys+数字' 的形式区分。
打包并整合完后的bundle结构,外层是主系统,sys2是系统2,sys3是系统3

这里用的nginx本地部署
nginx
#nginx.conf
#user nobody;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
# 日志格式配置
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
#access_log logs/access.log main;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
#gzip on;
gzip on;
gzip_comp_level 6;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
server {
#配置nginx启动的端口,服务器名字(本地localhost)
listen 8882;
server_name 192.168.120.178;
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Headers X-Requested-With;
add_header Access-Control-Allow-Methods GET,POST,OPTIONS;
root html;
index index.html index.htm;
#charset koi8-r;
# upstream backend_server {
# server mock.mengxuegu.com/mock;
# }
# access_log logs/host.access.log main;
location /api/ {
# rewrite ^/api/(.*) /v1/$1 break;
proxy_pass https://mock.mengxuegu.com/mock/629d727e6163854a32e8307e/;
proxy_set_header Host $http_host; #后台可以获取到完整的ip+端口号
proxy_set_header X-Real-IP $remote_addr; #后台可以获取到用户访问的真实ip地址
}
#配置启动nginx后打开的静态文件html页面
#静态文件一般是前端项目打包之后的dist文件(该文件下的html文件为启动页面)
location / {
# root html;
# index index.html index.htm;
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE';
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
add_header 'Access-Control-Allow-Credentials' 'true';
error_page 404 = @purge_cache;
try_files $uri $uri/ /index.html;
}
location /sys2/ {
root html;
index /sys2/index.html index.htm;
proxy_set_header Host $http_host; #后台可以获取到完整的ip+端口号
proxy_set_header X-Real-IP $remote_addr; #后台可以获取到用户访问的真实ip地址
error_page 404 = @purge_cache;
try_files $uri $uri/ /sys2/index.html;
}
location /sys3/ {
root html;
index /sys3/index.html index.htm;
proxy_set_header Host $http_host; #后台可以获取到完整的ip+端口号
proxy_set_header X-Real-IP $remote_addr; #后台可以获取到用户访问的真实ip地址
error_page 404 = @purge_cache;
try_files $uri $uri/ /sys3/index.html;
}
location = /favicon.ico {
log_not_found off;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}