1、 技术选型

在AI快速发展的时代下,很多AI产品伴随而来,同时对后台系统+AI系统的需求也随之出现。
最近做的一个新的项目,是基于SmartAdmin(后台系统)+MateChat (一个前端智能化场景解决方案UI库)
经过与AI的探讨分析,决定采用微前端方式把这两个项目集成在一起
想要把两个不同项目集成在一起,最开始我有三种想法;

方案 复杂度 适用场景 优缺点
方案一:iframe ⭐ 最简单 快速集成,两个项目独立维护 ✅ 零耦合,互不干扰
❌ 跨域通信稍麻烦
方案二:源码合并 ⭐⭐⭐ 复杂 深度定制,需要修改 MateChat ✅ 完全融合,无跨域问题
❌ 维护成本高
方案三:微前端 ⭐⭐⭐⭐ 最复杂 大型项目,需要完全隔离 ✅ 技术先进,独立部署
❌ 配置复杂

经过上面三种方案的对比分析,采用微前端的方式作为技术选型;

  • 整体架构设计
    ┌─────────────────────────────────────────┐
    │ SmartAdmin(主应用) │
    │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
    │ │ 系统管理 │ │ 数据报表 │ │ AI聊天 │ │ ← 菜单导航统一
    │ └─────────┘ └─────────┘ └─────────┘ │
    │ │
    │ ┌─────────────────────────────────────┐│
    │ │ ││
    │ │ 【MateChat 子应用】 ││ ← 全屏区域
    │ │ 独立运行,独立技术栈 ││
    │ │ 左侧会话 + 右侧聊天 ││
    │ │ ││
    │ └─────────────────────────────────────┘│
    └─────────────────────────────────────────┘

  • 微前端的思想
    1、把多个独立的前端应用组合成一个完整的应用,每个子应用可以独立开发、独立部署、独立运行,但用户感知上是一个整体;
    2、可以把它看作是后端“微服务”思想在前端的延伸,将一个庞大复杂的“巨石”前端应用,拆分成多个独立、小巧的“微应用”。这些微应用可以由不同团队独立开发、独立部署,最后像搭积木一样组装成一个完整的产品;

  • 为什么要使用微前端
    1、技术栈解耦:不再受限于单一框架。老旧模块用 Vue2,新模块用 React 或 Vue3,两者可以完美共存;
    2、独立开发部署:各团队负责各自的模块,代码互不干扰。更新某个模块时只需单独部署,无需全量发布,极大降低上线风险;
    3、渐进式重构:面对庞大的老项目,不需要一次性重写。可以按业务模块,一点一点地用新技术进行替换和升级;
    4、提升协作效率:解决了多人共用一个代码仓库带来的频繁代码冲突和沟通成本;

  • 总结
    微前端的核心不在于某项具体技术,而是一种“高内聚,低耦合”的架构思想

2、微前端技术选型与对比

单页应用(SPA)与微前端的适用场景分析
主流微前端方案对比:Module Federation、Single-SPA、Qiankun
选择 Qiankun 作为集成方案的理由(沙箱隔离、通信机制等)

项目 技术栈 关键特征
SmartAdmin Vue3 + Vite5 + Pinia + Ant Design Vue 中后台管理系统,有完整的路由/权限体系
MateChat Vue3 + TypeScript + DevUI Design AI对话UI组件库,支持按需引入
微前端方案 Qiankun + vite-plugin-qiankun 业界主流微前端方案,支持Vite

基于 Single-SPA 封装,提供了开箱即用的 JS 沙箱和样式隔离,是目前国内最成熟的方案之一;

3、SmartAdmin 基础框架改造

1、主应用(SmartAdmin)的 Qiankun 初始化配置

  • 安装 qiankun
cd smartadmin-project
npm install qiankun
  • 创建微前端配置文件
// smartadmin-project/src/micro-apps.ts
import { registerMicroApps, start } from 'qiankun'

let started = false

export function initMicroApps() {
    // 防止重复初始化
    if (started) {
        console.log('微前端已初始化,跳过')
        return
    }

    registerMicroApps([
        {
            name: 'matechat-subapp',
            entry: '//localhost:7100',
            container: '#matechat-container',
           activeRule: (location) => {
                // hash 模式用 location.hash 判断
                const hash = location.hash.replace('#', '')
                console.log('当前hash:', hash)
                return hash === '/ai-chat'
            },
            props: {
                token: localStorage.getItem('token'),
                userInfo: JSON.parse(localStorage.getItem('userInfo') || '{}')
            }
        }
    ])

    start({
        sandbox: false
    })

    started = true
    console.log('微前端初始化完成')
}

这里一定要把start({ sandbox: false })设置为false,不然会报错在这里插入图片描述
产生这个错误的原因是

  • qiankun 为了隔离子应用,会重写(代理)浏览器原生 API,比如 window、document、addEventListener 等,它代理了 EventTarget.prototype.addEventListenerMateChat(或 DevUI 组件库)在代码中调用了 addEventListener,并传入了 { passive: true } 选项,qiankun 的代理代码试图修改这个 options 对象上的 passive 属性
  • 但现代浏览器中,passive 是只读属性(read-only getter),不能通过赋值修改于是报错:Cannot set property passive

2、路由配置以及AI智能助手页面

  • 路由配置
    需要在SmartAdmin后台管理系统的菜单配置中,配置好AI智能助手的菜单;
    在这里插入图片描述

  • AI智能助手页面
    1、我想要的是点击AI智能助手打开新的浏览器窗口,并且不受SmartAdmin菜单栏的限制,子应用单独占用一整个页面的布局,而不是内嵌在SmartAdmin的layout里面;
    所以在side-layout.vue文件中用v-if="route.path === ‘/ai-chat’"独立渲染布局

<template>
  <!-- 独立页面:直接渲染内容,没有任何布局 -->
  <template v-if="route.path === '/ai-chat'">
    <div class="standalone-page">
      <router-view />
    </div>
  </template>
  <a-layout v-else class="admin-layout" style="min-height: 100%">
    <!-- 侧边菜单 side-menu -->
    <a-layout-sider
      :id="LAYOUT_ELEMENT_IDS.menu"
      class="side-menu"
      :width="sideMenuWidth"
      v-model:collapsed="collapsed"
      :theme="theme"
      v-show="!fullScreenFlag"
    >
      <!-- 左侧菜单 -->
      <SideMenu :collapsed="collapsed" />
    </a-layout-sider>
    ..........
    
  </a-layout>
</template>


在recursion-menu.vue文件中的turnToPage方法中添加一下代码

// 页面跳转
function turnToPage(menu) {
  // useUserStore().deleteKeepAliveIncludes(menu.menuId.toString());
  // router.push({ path: menu.path });

  if (menu.path === '/ai-chat') {
    const origin = window.location.origin
    const isHash = window.location.hash || router.options.history.base
    const url = isHash ? `${origin}/#${menu.path}` : `${origin}${menu.path}`

    const width = screen.availWidth
    const height = screen.availHeight
    window.open(
      url,
      '_blank',
    )
    return
  }
  useUserStore().deleteKeepAliveIncludes(menu.menuId.toString())
  router.push({ path: menu.path })
}

通过这种方式点击AI智能助手菜单时,就可以在浏览器打开新的同源窗口,然后不受主应用layout布局的影响,全屏展示AI聊天页面。
3、智能助手页面,因为是打开新的浏览器窗口,所以 独立页面需要手动初始化微前端
initMicroApps()

<template>
  <div class="ai-chat-standalone">
    <div id="matechat-container"></div>
  </div>
</template>

<script setup lang="ts">
import { onMounted } from 'vue'
import { initMicroApps } from '../../micro-app'

onMounted(() => {
  // 独立页面需要手动初始化微前端
  initMicroApps()
})
</script>

<style scoped>
.ai-chat-standalone {
  width: 100vw;
  height: 100vh;
  overflow: hidden;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}

#matechat-container {
  width: 100%;
  height: 100%;
}
</style>

4、MateChat 子应用适配

1、安装必要依赖

cd matechat-subapp

# 1. 安装 MateChat 核心库
npm install @matechat/core @devui-design/icons vue-devui

# 2. 安装微前端依赖
npm install vite-plugin-qiankun --save-dev

# 3. 安装路由(如果需要)
npm install vue-router@4

2、修改 vite.config.ts

import path from "node:path";
import vue from "@vitejs/plugin-vue";
import vueJsx from "@vitejs/plugin-vue-jsx";
import AutoImport from "unplugin-auto-import/vite";
import { defineConfig } from "vite";
import qiankun from 'vite-plugin-qiankun'

// https://vite.dev/config/
export default defineConfig({
  base: "/",
  plugins: [
    vue(),
    vueJsx(),
    AutoImport({
      include: [/\.[tj]sx?$/, /\.vue$/, /\.vue\?vue/],
      imports: ["vue"],
      dirs: ["./src"],
    }),
    qiankun('matechat-subapp', { useDevMode: true })
  ],
  server: {
    host: true,
    open: true,
    port: 7100, //固定端口
    cors: true,
    headers: {
      'Access-Control-Allow-Origin': '*'
    }
  },
  resolve: {
    alias: {
      "@view": path.resolve(__dirname, "./src/view"),
      "@": path.resolve(__dirname, "./src"),
    },
  },
  build: {
    target: 'esnext',
    rollupOptions: {
      output: {
        format: 'umd',    // qiankun 需要 UMD 格式
        name: 'matechat-subapp',
        entryFileNames: 'static/js/[name].js',
        assetFileNames: (assetInfo) => {
          const info = assetInfo.name.split('.')
          const ext = info[info.length - 1]
          return `static/[ext]/[name][extname]`
        },
      },
    },
  },
  /* 关键:静默 Sass @import 警告 */
  css: {
    preprocessorOptions: {
      scss: {
        quietDeps: true, // 不打印依赖里的警告
        silenceDeprecations: ["import"], // 彻底关闭 import 弃用警告
      },
    },
  },
});

3、修改 main.ts 为微前端入口,所有 import 之前执行,必须在子应用上打补丁 ,关闭沙箱注入

// ================== 必须在所有 import 之前执行 ==================
// 子应用补丁 关闭沙箱注入
// 保存原始方法
const originalAddEventListener = EventTarget.prototype.addEventListener

// 重写 addEventListener,拦截对 passive 的修改
EventTarget.prototype.addEventListener = function(
  type: string,
  listener: EventListenerOrEventListenerObject | null,
  options?: boolean | AddEventListenerOptions
) {
  // 如果 options 是对象,创建一个新的,避免修改原始对象
  if (options && typeof options === 'object') {
    // 用 Object.defineProperty 创建新对象,确保 writable
    const newOptions: any = {}
    
    // 复制所有属性
    Object.keys(options).forEach(key => {
      const descriptor = Object.getOwnPropertyDescriptor(options, key)
      if (descriptor) {
        Object.defineProperty(newOptions, key, {
          value: descriptor.value,
          writable: true,
          enumerable: true,
          configurable: true
        })
      }
    })
    
    return originalAddEventListener.call(this, type, listener, newOptions)
  }
  
  return originalAddEventListener.call(this, type, listener, options)
}

import { createPinia } from 'pinia';
import { createApp } from 'vue';
import './style.scss';
import MateChat from '@matechat/core';
import VueDevui from 'vue-devui';
import App from './App.vue';
import i18n from './i18n';
// 微前端辅助函数
import { renderWithQiankun, qiankunWindow } from 'vite-plugin-qiankun/dist/helper'

const pinia = createPinia();
let instance: any = null

function render(props: any = {}) {
    const { container } = props
 // 直接找 #app,不管在不在 container 里
    const mountNode = container
        ? container.querySelector('#app')
        : document.getElementById('app')
  
  if (!mountNode) {
    console.error('找不到 #app 节点')
    return
  }
    instance = createApp(App)
    instance.use(MateChat)
    instance.use(pinia);
    instance.use(VueDevui);
    instance.use(i18n);
    // 挂载到微前端容器或独立运行

    instance.mount(mountNode || '#app')
}

// 独立运行时直接渲染
if (!qiankunWindow.__POWERED_BY_QIANKUN__) {
    render()
} else {
    // 微前端环境下暴露生命周期
    renderWithQiankun({
        mount(props) {
            console.log('MateChat 挂载', props)
            render(props)
        },
        bootstrap() {
            console.log('MateChat 启动')
        },
        update(props) {
            console.log('MateChat 更新', props)
        },
        unmount() {
            console.log('MateChat 卸载')
            instance?.unmount()
            instance = null
        }
    })
}

5、部署

  1. 服务器目录结构
    ├── /var/www/
    │ ├── smartadmin/ # 主应用(SmartAdmin)
    │ │ ├── index.html
    │ │ └── assets/
    │ │
    │ └── matechat/ # 子应用(MateChat)
    │ ├── index.html
    │ └── assets/
  2. Nginx 配置示例
server {
    listen 80;
    server_name your-domain.com;

    # 主应用 - SmartAdmin
    location / {
        root /var/www/smartadmin;
        try_files $uri $uri/ /index.html;
    }

    # 子应用 - MateChat(独立访问,也供 qiankun 加载)
    location /matechat/ {
        root /var/www;
        try_files $uri $uri/ /matechat/index.html;
        
        # 必须加 CORS,否则 qiankun 跨域加载失败
        add_header Access-Control-Allow-Origin *;
        add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
        add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
    }
}
  1. 访问方式
    主应用:http://your-domain.com/
    子应用:http://your-domain.com/matechat/

  2. 总结流程图
    ┌─────────────────┐ ┌─────────────────┐
    │ 开发阶段 │ │ 部署阶段 │
    ├─────────────────┤ ├─────────────────┤
    │ 1. 改 MateChat │ │ 1. npm run build │
    │ vite.config │ │ (两个项目) │
    │ 2. 改 main.ts │────▶│ 2. 上传 dist/ │
    │ 暴露生命周期 │ │ 到服务器 │
    │ 3. 改 SmartAdmin│ │ 3. Nginx 配置 │
    │ 注册子应用 │ │ 4. 配置 CORS │
    │ 4. 创建全屏页面 │ │ 5. 访问测试 │
    └─────────────────┘ └─────────────────┘

6、项目集成中可能出现的前端问题

1、子应用的图标独立打开时能展示,但是集成到主应用通过微前端方式访问时,图标展示异常;

  • 这是微前端集成中非常经典的字体文件加载问题
  • MateChat 子应用独立运行时,字体文件(.woff / .ttf)的加载路径是相对于 localhost:7100 的,比如:
http://localhost:7100/static/fonts/devui-icon.woff2

但通过 qiankun 嵌入后,浏览器认为页面在 SmartAdmin 的域名下,字体文件的相对路径解析错误,或者因为跨域/路径解析问题导致加载失败。

情况 说明
独立运行正常 font-family: 'devui-icon' 生效,.icon-add:before { content: '\e900' } 正常
微前端下失效 qiankun 重写 CSS 选择器,如 [data-qiankun-matechat-subapp] .icon-add,但字体文件路径或 font-family 定义可能没被正确隔离
  • 解决办法

在 SmartAdmin 安装图标包

cd /path/to/smartadmin
npm install @devui-design/icons

在 SmartAdmin 的 main.ts 中加一行:

import '@devui-design/icons/icomoon/devui-icon.css'

2、子应用中的一些资源文件,比如图片,无法正常加载;
在这里插入图片描述

  • 这是因为微前端环境下,子应用的 public 目录资源路径解析方式变了。
场景 路径解析
子应用独立运行 http://localhost:7100/logo2x.svg
微前端嵌入 浏览器认为页面在 SmartAdmin 域名下,../../../public/logo2x.svg 解析到 SmartAdmin 的根目录,找不到文件

解决方法:直接把所有子应用相关的资源复制到主应用

优点 缺点
✅ 简单直接,不用改 Vite 配置 ❌ 资源需要手动同步,维护麻烦
✅ 没有跨域和路径问题 ❌ 子应用独立运行时可能找不到资源
✅ 生产环境部署简单 ❌ 资源重复,增加主应用体积
✅ 字体文件也能统一处理 ❌ 更新子应用时需要同步更新主应用资源
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐