ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

JeecgBoot接入Qiankun微前端:子应用改造实操指南

JeecgBoot接入Qiankun微前端:子应用改造实操指南 在做企业级前端架构的时候我接手过不少“把某个老系统塞进微前端壳子”的活。大多数情况下塞进去的不是一个页面而是一个完整的业务应用。JeecgBoot 作为开源的 Java 低代码平台本身自带了完整的用户体系、菜单权限、表单设计器和代码生成器用拖拉拽的方式就能快速搭出一套中后台管理系统。但这套东西真正要作为子应用接入 Qiankun 微前端架构时事情就不像“把入口改成挂载函数”那么简单了。JeecgBoot 内部有动态路由、登录拦截、Axios 封装、公共前缀代理、低代码表单动态渲染这些组件在独立跑的时候一切正常一旦被主应用接管任何一环没有处理好都会变成白屏、路由错乱、接口 401、样式被污染。这篇文章就从实操角度完整讲清楚 JeecgBoot 低代码平台作为 Qiankun 子应用的接入过程。包括 Vue2 webpack 和 Vue3 Vite 两种项目形态的改造方式主应用侧的注册配置登录态联动、公共依赖与静态资源的处理最后附上我在实际项目里遇到过的典型问题和排查思路。如果你正准备把低代码平台接进现有的微前端体系这篇文章应该能帮你省掉几次通宵排查。1. 拆清楚 JeecgBoot 和 Qiankun 的适配关系1.1 微前端 低代码到底解决了什么问题微前端解决的是“多个团队、多个技术栈、多个独立部署的应用怎么共用一个门户”的问题。Qiankun 是目前使用最广的方案它在 single-spa 基础上封装了样式隔离、JS 沙箱、预加载等能力对老项目的接入相对友好。你可以把一个单体前端拆成多个独立开发、独立部署的应用由主应用作为壳统一调度子应用按路由加载。低代码平台解决的是“大量重复 CRUD 页面怎么写更快”的问题。JeecgBoot 提供了在线表单设计器、在线报表、代码生成器、工作流等功能后台管理系统里常见的增删改查页面通过拖拽配置就能生成效率比手写提高非常多。把两者结合起来其实是很多中大型企业正在做的事主应用承载统一的工作台入口、门户框架和全局登录态低代码平台作为子应用专门承担快速交付的业务模块。开发人员在 JeecgBoot 里拖出页面打包成子应用接入主应用菜单用户就能通过门户统一入口访问新功能不再需要在不同的域名和系统之间来回切换。对技术团队来说这个组合最直接的收益是主应用不用跟着低代码平台的版本迭代走低代码平台也不受主应用技术栈的约束两边各自发版互不阻塞。1.2 JeecgBoot 前端项目的两种形态接入 Qiankun 之前先要弄清楚你手里的是哪个 JeecgBoot 前端版本。因为这两个版本对应的改造方式完全不同走错了路真的会白干几天。JeecgBoot 前端现在常见两条线。一条是 jeecgboot-vue2基于 Vue 2.6 Vue CLI/webpack Ant Design Vue 1.x Vuex这套老版本在存量项目里用得不少。另一条是官方后来推出的 jeecgboot-vue3基于 Vue 3 Vite Ant Design Vue 3.x TypeScript Pinia是现在新项目的主要方向。Qiankun 对 webpack 项目的支持非常成熟只需要改几个配置就能接入。但 Vite 项目因为是原生 ESM 的构建机制Qiankun 官方原生不支持需要借助 vite-plugin-qiankun 这种插件来桥接。所以“你的 JeecgBoot 是 Vue2 还是 Vue3”这个问题的答案直接决定了后续的整改路径。我这次实际项目里对接的是一个老版本 jeecgboot-vue2主应用是 Vue3 Vite。两套技术栈混在一起踩了不少版本兼容的坑。这篇文章会把两种形态的改造都讲一遍方便你对号入座。1.3 理解 JeecgBoot 的启动链路拿到 JeecgBoot 前端工程后不要急着改动先花半天把 main.js 逐行走一遍。这个工程的启动链路比普通 Vue 项目复杂得多关键步骤包括创建 Vue 实例、注册全局组件和插件、初始化 Vuex/Pinia 状态、创建 Vue Router 实例、请求后端接口获取用户信息、拉取菜单权限列表、动态注册路由、最后挂载根组件。Qiankun 接入的关键在于第 4 步和第 7 步。Qiankun 要求子应用必须导出 bootstrap、mount、unmount 三个生命周期钩子并且把“创建应用实例”这个动作放到 mount 里执行而不是在入口文件里直接自动挂载。也就是说原来“启动即挂载”的逻辑要改造成“被 Qinkun 调用时才挂载”的模式。JeecgBoot 的动态路由也是一个大坑。它的菜单不是前端写死的而是后端根据用户权限返回的权限列表前端再通过 addRoute 动态注册。这导致的结果是子应用被嵌入主应用后如果初始化顺序没处理好菜单可能拉不到路由也可能注册失败页面一片空白。2. 接入前先做好的工程准备2.1 版本选型和环境清单接入前先把自己这边的环境梳理清楚。这里有一份我当时实际使用的环境清单可以参考一下项目版本/说明主应用框架Vue 3 Vite 4主应用微前端方案Qiankun 2.x子应用JeecgBootjeecgboot-vue2 3.xVue 2.6 Vue CLINode 环境16.xVite 4 要求 16.20 以上后端服务JeecgBoot Spring Boot 3.x独立部署接入前先把两个项目都独立跑通这是最基础的前提。很多同学一上来就急着改 Qinkun 配置结果子应用本身都无法正常启动后面排查起来特别混乱。我在做的时候先是把 JeecgBoot 前端单独启动确认后端接口能通、登录页能进、页面能正常渲染然后再开始动主应用那侧。2.2 主应用侧需要提前准备什么主应用这边Qiankun 的注册逻辑一般会集中在一个微前端配置模块里。先把最基础的子应用注册搭起来再考虑后面的样式隔离、预加载等增强配置。import { registerMicroApps, start } from qiankun; registerMicroApps([ { name: jeecgboot, entry: //localhost:3001, container: #subapp-viewport, activeRule: /jeecg, props: { token: getToken(), userInfo: getUserInfo(), }, } ]); start({ prefetch: true, sandbox: { experimentalStyleIsolation: true, }, });这里我提前做了三件事一是规划好子应用的 activeRule我用的是 /jeecg这样主应用路由里 /jeecg 开头的地址都会落到子应用容器二是准备了一个承载子应用的容器组件里面就是一个带 id 的 div三是通过 props 把主应用的 token 和用户信息传给了子应用为后续登录态打通做好准备。承载子应用的容器组件很简单比如 SubAppContainer.vuetemplate div idsubapp-viewport / /template主应用路由里对应配置{ path: /jeecg/*, name: JeecgBootSubApp, component: () import(/views/SubAppContainer.vue), }到这一步主应用侧能做的准备工作基本就绪。3. 子应用改造JeecgBoot 接入的核心实操3.1 基于 Vue 2 webpack 的经典改造方式如果你是老版的 jeecgboot-vue2改造步骤可以概括为配置打包格式 暴露生命周期。具体来说分三步。第一步修改 vue.config.js让子应用能被正确加载为 UMD 库并设置一个明确的全局变量名const packageName require(./package.json).name; module.exports { publicPath: /jeecg/, devServer: { port: 3001, headers: { Access-Control-Allow-Origin: *, }, historyApiFallback: true, }, configureWebpack: { output: { library: ${packageName}-[name], libraryTarget: umd, jsonpFunction: webpackJsonp_${packageName}, }, }, };这里有两个容易被忽略的配置。publicPath 设成 /jeecg/是为了让子应用打包出来的 JS、CSS、图片资源都从 /jeecg/js/xxx.js 这种路径去加载避免部署到主应用域名下以后资源 404。headers 里的 Access-Control-Allow-Origin: *是让主应用以 fetch 方式拉取子应用 HTML 和 JS 的时候不被浏览器跨域策略拦截。jsonpFunction 也是 Qinkun 文档里重点提到的如果主应用和子应用的 webpack jsonpFunction 重名资源加载会互相干扰。第二步修改 main.js把自动启动改造成生命周期接管模式。核心代码是这样import Vue from vue; import App from ./App.vue; import router from ./router; import store from ./store; let instance null; function render(props {}) { const { container } props; instance new Vue({ router, store, render: h h(App), }).$mount(container ? container.querySelector(#app) : #app); } if (!window.__POWERED_BY_QIANKUN__) { render(); } export async function bootstrap() { console.log(jeecgboot bootstrap); } export async function mount(props) { render(props); } export async function unmount() { instance.$destroy(); instance.$el.innerHTML ; instance null; }window.POWERED_BY_QIANKUN是 Qiankun 在加载子应用时注入的全局标记。用这个变量区分“独立运行”和“被 Qiankun 加载”两种模式独立运行时就正常自动挂载被加载时就把控制权交给 mount 生命周期。第三步处理路由实例。JeecgBoot 默认可能是 history 模式但在 Qiankun 里被挂载后路由要跟着主应用的路径走。我建议直接把子应用的路由改成 hash 模式省心省力const router new VueRouter({ mode: hash, base: window.__POWERED_BY_QIANKUN__ ? /jeecg/ : /, routes, });这样访问主应用 /jeecg/xxx 地址时子应用内部实际路径是 /jeecg/#/xxx路由的命名空间完全隔离开不容易和主应用冲突。3.2 基于 Vue 3 Vite 的改造方式再讲讲新版 jeecgboot-vue3 的接入。Vite 构建产物是原生 ESM 模块Qiankun 基于 single-spa 拉取 HTML 后对 ESM 的处理没有 webpack 那么直接所以官方推荐使用 vite-plugin-qiankun 这个插件。先安装依赖npm install vite-plugin-qiankun -D然后在 vite.config.ts 里引入配置import qiankun from vite-plugin-qiankun; export default defineConfig({ plugins: [ vue(), qiankun(jeecgboot-vue3, { useDevMode: true, }), ], server: { port: 3001, cors: true, origin: http://localhost:3001, }, });插件参数里的第一个字符串是子应用名称这个名称必须和主应用 registerMicroApps 里的 name 完全一致否则生命周期识别不了。main.ts 的改造思路和 Vue2 版本类似只是 Vue 的 API 变了。用 renderWithQiankun 包裹生命周期挂载方式从 new Vue 改成 createAppimport { createApp } from vue; import { renderWithQiankun, qiankunWindow } from vite-plugin-qiankun/dist/helper; import App from ./App.vue; import router from ./router; import pinia from ./store; let app: any null; function render(props: any {}) { const { container } props; app createApp(App); app.use(router); app.use(pinia); app.mount(container ? container.querySelector(#app) : #app); } renderWithQiankun({ bootstrap() {}, mount(props) { render(props); }, unmount() { app.unmount(); app null; }, }); if (!qiankunWindow.__POWERED_BY_QIANKUN__) { render({}); }用 qiankunWindow 而不是 window是 vite-plugin-qiankun 提供的兼容封装。Vite 在 dev 模式下直接操作 window 可能拿不到正确的沙箱上下文用这个封装会更可靠。3.3 生命周期改造中容易踩的细节坑生命周期改造看起来简单实际有几个细节不处理好就会出问题。第一个是 axios 请求的 baseURL。JeecgBoot 的 axios 封装一般带了一个公共前缀比如 /jeecg-boot/。这个前缀在独立运行时通过 devServer 的 proxy 代理到后端服务。一旦接入 Qiankun子应用请求的实际域名变成了主应用域名所以主应用也要把 /jeecg-boot 这个前缀代理到 JeecgBoot 后端否则登录、菜单、权限接口全部 404。第二个是 render 函数里的 container。用 container.querySelector(#app) 挂载子应用时要确保子应用 index.html 里真的有一个 id 为 app 的根节点。Qiankun 会把子应用的 HTML 整个拉回来放到容器里如果根节点 id 对不上挂载必然白屏。第三个是全局事件和定时器清理。JeecgBoot 里可能注册了窗口监听事件、轮询定时器如果 unmount 时不清理子应用切换走以后事件依然存在可能影响主应用甚至造成内存泄漏。我习惯在 unmount 里把 instance 置空的同时把全局监听也一并移除。3.4 适配低代码生成页面的运行时问题JeecgBoot 最有价值的部分就是通过拖拉拽方式创建表单和页面。在独立运行模式下低代码生成的页面是按需动态渲染的靠的是后台返回的 schema 配置。但接入 Qiankun 后我遇到一个比较特殊的情况部分低代码页面依赖的组件在子应用沙箱环境里没有被全局注册运行时直接报“component is not registered”。解决方式是在 main.js 里把低代码页面可能用到的组件统一全局注册或者统一 import 一遍import OnlineForm from /components/OnlineForm; Vue.component(OnlineForm, OnlineForm);另外低代码平台在运行时可能会动态向内联 style 标签。在沙箱开启后这类动态插入的样式不一定能生效会造成页面错乱。建议在主应用 start 时开启 experimentalStyleIsolation并配合子应用端对基础组件样式做兼容处理。4. 主应用注册与整体联调4.1 子应用注册配置与路由映射主应用把 JeecgBoot 注册成子应用后有几个关键配置项需要和子应用严格对应我列个表方便对照检查配置项主应用值子应用要求namejeecgboot与 package.json name 或插件中名称保持一致entryhttp://localhost:3001子应用开发服务器地址container#subapp-viewport主应用内容器 div 的 idactiveRule/jeecg子应用应命中的主应用路由前缀activeRule 建议设置成带前缀的路径最好不要用 “/” 这种全匹配。否则用户访问主应用任何路径都可能把子应用触发加载一遍资源开销大还容易出冲突。如果你希望子应用只在特定菜单下被激活可以用函数方式精确控制activeRule: (location) { return location.pathname.startsWith(/jeecg); }4.2 登录态与权限的打通这一节是很多接入方案里最头疼的部分。JeecgBoot 自带登录页和完整的用户权限体系但在微前端架构下如果用户已经在主应用登录过了进入 JeecgBoot 子应用时还要再登录一次体验非常割裂。我建议的做法是主应用通过 props 把 token 和用户信息传给子应用子应用在 mount 时写入自己的存储并初始化用户信息和菜单权限。主应用注册子应用时传参registerMicroApps([ { name: jeecgboot, entry: //localhost:3001, container: #subapp-viewport, activeRule: /jeecg, props: { token: getToken(), userInfo: getUserInfo(), }, } ]);子应用 mount 时接收并使用export async function mount(props) { if (props props.token) { localStorage.setItem(jeecg-token, props.token); store.commit(SET_TOKEN, props.token); } render(props); }这里有一个实操细节值得注意JeecgBoot 的菜单权限是需要向后端重新请求的不是光有一个 token 就万事大吉。所以 mount 之后最好主动调用一次获取用户信息和菜单权限的接口让子应用的动态路由和菜单在每次进入时都刷新一遍。export async function mount(props) { render(props); await store.dispatch(GetUserInfo); await store.dispatch(UpdateMenu); }4.3 公共依赖与样式隔离处理JeecgBoot 依赖 Vue、Vue Router、Ant Design Vue、Axios。理论上可以通过 externals 机制把公共依赖抽到主应用统一加载减少子应用体积。但我实际测试下来并不建议这样做。因为 JeecgBoot 的依赖版本可能和主应用不一致强行外部化很容易导致组件 API 不兼容低代码平台内部依赖关系复杂外部化以后排查问题难度会明显增加。我建议保持子应用独立打包让 Qiankun 的沙箱机制来处理重复依赖。这样更省事稳定性也更高。样式隔离方面建议开启 Qiankun 的 experimentalStyleIsolation它会为子应用样式加作用域前缀极大减少子应用样式污染主应用的概率。start({ sandbox: { experimentalStyleIsolation: true, }, });不过开启样式隔离后也要注意副作用。子应用里一些依赖 body 或全局选择器的弹层、提示组件比如 Ant Design Vue 的 Modal、message、notification可能会因为样式被隔离而显示异常。遇到这种情况一般需要手动把这些弹层挂载到子应用容器下或者单独为不受隔离的样式做处理。4.4 静态资源路径处理JeecgBoot 低代码平台里有很多上传的图片、文件、头像之类的资源。接入 Qiankun 后这些资源的访问路径也需要处理。我的做法是资源地址统一用后端返回的绝对路径不要用相对路径。如果必须用相对路径就要保证子应用的 publicPath 与主应用路由前缀一致。之前遇到过一个典型问题子应用入口 HTML 能加载但引用的 JS、CSS 全部 404最后发现就是 publicPath 写成了 /资源跑到主应用域名根路径下去找了。把 publicPath 改成 /jeecg/ 后问题立刻消失。5. 接入过程中的典型问题与排查思路5.1 子应用白屏先分清是加载还是渲染问题白屏是 Qinkun 接入里出现频率最高的问题排查思路要有顺序不然就是乱找。第一步打开浏览器 Network 面板确认子应用 HTML 是否被成功加载。如果 HTML 都 404检查主应用路由匹配和 entry 地址。第二步看子应用 JS 是否加载成功。如果 JS 404大概率是 publicPath 配置错误或 jsonpFunction 冲突。第三步看控制台报错。如果报错是 “Cannot read properties of undefined”多半是入口没有正确导出生命周期或者沙箱环境里访问 window 出了问题。第四步切到 Elements 面板看 #subapp-viewport 里有没有内容。如果容器是空的检查 render 函数里 container.querySelector(#app) 是否找得到节点。我遇到过一种很隐蔽的情况控制台一个错误都没有但容器是空的最后发现是子应用内部路由守卫被触发跳到登录页了而登录页因为资源路径问题又没渲染出来看起来就是一片白。5.2 路由跳转后子应用不渲染另一个常见现象是浏览器地址已经变成 /jeecg/xxx但子应用始终没有加载出来或者一直在转圈。这种情况通常和 activeRule 匹配无关而是子应用内部路由没匹配上。一个常见原因是主应用和子应用都用了 history 路由。主应用地址切到 /jeecg/xxx 后子应用内部路由的 base 没有同步导致子应用认为当前路径不在它自己的路由表里渲染了一个空页面。解决办法就是在子应用里改 hash 模式或者给 history 模式传入和 activeRule 一致的 base。还有一类情况是子应用路由守卫里做了登录校验token 没拿到就强制跳登录页导致展示空白。这时候需要把守卫里的 token 强校验逻辑做条件判断或者确保 props 传 token 先于路由初始化完成。5.3 API 请求跨域与代理问题JeecgBoot 独立开发时通过 devServer proxy 把 /jeecg-boot 代理到后端。接入 Qiankun 后子应用资源加载和 API 请求都发生在主应用环境下跨域问题就从子应用 devServer 转移到了主应用 devServer。我本地联调时在主应用 devServer 里额外加了一段代理proxy: { /jeecg-boot: { target: http://localhost:8080, changeOrigin: true, }, }子应用自己的 devServer 保留原有代理。这样独立调试和微前端联调都能正常工作。如果部署到测试环境主应用一般由 Nginx 承载需要同时代理 /jeecg-boot 和 /jeecg 两个路径location /jeecg-boot/ { proxy_pass http://jeecg-backend:8080/; } location /jeecg/ { proxy_pass http://jeecg-frontend:3001/; }这里的转发规则不是唯一的要根据你自己的部署架构调整但原则是后端接口和子应用静态资源都要经过主应用域名能访问到。5.4 登录失效和权限不同步JeecgBoot 的 axios 响应拦截器里通常有一条逻辑后端返回 401 就跳转到登录页。这个逻辑在微前端环境里非常容易引发问题。用户在子应用里停留时间长了token 过期后端返回 401子应用马上跳自己的登录页把用户从主应用框架里拽出去体验很差。我的处理方案是在子应用的 401 处理里加一个判断如果是被 Qinkun 加载的模式就通过自定义事件通知主应用去处理登录失效只有独立运行时才跳子应用自己的登录页。if (error.response.status 401) { if (window.__POWERED_BY_QIANKUN__) { window.dispatchEvent(new CustomEvent(jeecg-unauthorized)); } else { router.push(/user/login); } }主应用监听这个事件统一跳转主登录页。这样既保持了单点登录体验又不会让子应用的登录页把用户带走。5.5 一个容易被忽略的沙箱常见问题Qiankun 的沙箱机制会拦截子应用里 window.addEventListener 的注册为的是在应用卸载时自动清理。但 JeecgBoot 有些功能比如 WebSocket 连接、全局键盘事件、消息推送正是通过 window.addEventListener 实现的。在沙箱开启的情况下这些全局事件可能被代理到虚拟 window 上导致主应用拿不到或者切换路由后事件被清除。遇到这种情况建议把关键事件绑定改到具体的 DOM 节点上或者由主应用统一管理这类全局消息子应用通过 props 接收通知。这样虽然改起来多花点时间但从架构上看更干净。6. 一些实操后的个人建议JeecgBoot 接入 Qiankun技术上并不复杂难的是细节多路由模式、公共路径、代理转发、登录态联动、低代码组件注册、样式隔离每一个环节都可能出问题。我做完整个项目之后的体会是先让子应用独立跑得明明白白再动微前端改造是最稳的路径。另外想说一句微前端不是银弹。样式隔离、沙箱隔离、路由隔离都有成本和边界。比如我开启 experimentalStyleIsolation 之后JeecgBoot 的部分弹层样式确实出了问题最后只能用“把影响范围限制在子应用页面内部 手动修复关键样式”的方式解决。这个过程里没有能抄一次的作业必须有耐心一个个踩、一个个修。如果你也正在走这条路不妨从一个最小的可运行 demo 开始先把空页面子应用跑通再逐步把 JeecgBoot 的功能引进来。增量式接入会比一把梭省心很多。
返回列表