基于模块联邦打通多系统的探索

前言

模块联邦(Module Federation)是一种前端技术,允许不同构建的应用之间共享模块。类似于微服务架构在前端的体现。通过模块联邦,可以实现跨应用的代码共享,这对于构建微前端架构非常有用。

  1. 远程应用(Remote Application):暴露模块的应用,其他应用可以引用这些模块。
  2. 宿主应用(Host Application):引用远程应用模块的应用。

背景

前不久,临时接到个需求,想要实现串联多个系统,能够实现自由切换又能分开单独运行,也要数据共享也要相互引用(实在是既要又要还要,十分符合当下产品的设计思维)。无奈谁还不是头牛马,只能绞尽脑汁造了。 好在平常的知识储备还是有的,monorepo、微前端、模块联邦顿时打入脑海,由于之前微前端和monorepo都搞过了,勉为其难的选择了模块联邦。于是开始着手打开各种搜索引擎各种Ai开始研究,没成想,越研究还觉得越对味哈哈哈哈!😀😀😀😀😀😀

一、架构设计

1、系统概念: 每个独立的前端项目都为一个独立系统;

2、系统互通: 能相互跳转(实现SSO单点登录),系统能访问其他系统的组件;

3、插件: 类似工具类,可提供各个系统/组件使用,可考虑独立库方式实现;

二、场景及目标

(一) 应用场景

系统太多且割裂,假设当前有多个系统主系统、系统2、...、系统N,且分别由多个不同的代码仓库管理

(二) 目标效果

  • 多个系统既能集成运行又能单独运行;
  • 数据共享和统一登录;
  • 系统互认,各系统之间能够相互引用;

三、技术实现

(一) 技术选型

  1. 技术栈 vue3+pinia+vite
  2. ui框架 element-plus + Geeker Admin
  3. 核心插件 @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;
        }
    }
}
相关推荐
季禮祥1 小时前
彻底弄懂KeepAlive
javascript·vue.js·面试
小胖霞1 小时前
彻底搞懂 JWT 登录认证与路由守卫(五)
前端·vue.js·node.js
用户93816912553601 小时前
VUE3项目--组件递归调用自身
前端
昔人'1 小时前
CSS content-visibility
前端·css
灵魂学者1 小时前
Vue3.x —— ref 的使用
前端·javascript·vue.js
梦6501 小时前
VUE树形菜单组件如何实现展开/收起、全选/取消功能
前端·javascript·vue.js
我命由我123451 小时前
微信小程序 - 避免在 data 初始化中引用全局变量
开发语言·前端·javascript·微信小程序·小程序·前端框架·js
可爱又迷人的反派角色“yang”2 小时前
Mysql数据库(二)
运维·服务器·前端·数据库·mysql·nginx·云计算