ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Vue2老项目接入qiankun微前端:完整指南与避坑经验

Vue2老项目接入qiankun微前端:完整指南与避坑经验 上个月我们刚把公司里一个跑了六年的 Vue2 老项目接进了 qiankun 微前端前后折腾了大半个月。这个项目是当初用 Vue2 Vue CLI 3 起的业务模块越堆越多光路由文件就有三千多行每次发版都牵一发动全身。正好新团队那边要用 Vue3 Vite 做一套全新的业务系统两边又需要共用主框架导航、外层鉴权和统一的登录态微前端这件事终于被提上了日程。方案选型阶段我没怎么纠结直接定了 qiankun。单从 Vue2 老项目接入的舒适度来看qiankun 的几个设计确实是对存量项目最友好的不需要推倒重来打包层面的改造量很小HTML Entry 这套玩法连不怎么懂构建的同事都能理解。这篇文章把整个改造过程里我认为最有价值的思路、核心机制原理、实操步骤和踩过的坑都串一遍给打算在 Vue2 项目里搞微前端的朋友做个参考。1. 为什么要把好好的 Vue2 老项目接进 qiankun1.1 单体应用被撑爆的信号先确认“要不要拆”在动手之前先得确认自己的项目到底是不是真的需要微前端。很多团队看到“微前端”三个字就觉得高级想跟风搞一套结果拆完比不拆还痛苦。我的判断标准很简单当你发现下面这几种情况出现了两个以上再考虑拆分。第一你们有两条或两条以上业务线各自独立迭代、独立发版却挤在同一个仓库和同一套部署流程里。比如我们这个项目里内部管理端和 To C 门户就是两套完全不同的业务但是共用一套发布管道内部业务改一行代码也要走整个项目的回归测试。第二技术栈升级被卡死。老项目是 Vue2团队里新人已经写了很久 Vue3但核心模块不敢动一升级就是全局联动。我们当时也评估过直接把 Vue2 升到 Vue3结果发现组件库、第三方插件、内部封装的公共模块全部要跟着动工期排到明年。第三某个模块的访问量或者内存占用已经影响整体稳定性。我们有个在线文档预览模块基于 vue-office 那套方案做的渲染起来非常吃内存经常把整个后台首页的响应速度拖下来。如果上面这些情况你一条都不占那微前端对你就是负担。微前端本质上解决的是“一个团队维护多个技术栈、多条业务线、多套部署流程”的组织效率问题而不是技术炫技。1.2 微前端方案横向对比iframe、single-spa、Module Federation 与 qiankun聊 qiankun 之前得把它放进微前端方案的坐标系里看看否则你很难理解为什么选它而不是别的。iframe 是最容易被拉出来对比的方案。实现成本确实低隔离性也最好子应用内部随便折腾都影响不到主应用。但它在真实业务里有两个致命问题一是刷新、前进后退、弹窗层级和登录态同步这些交互体验会被割裂二是每个 iframe 都是一套独立的浏览器上下文内存和连接数开销翻倍。我们最早内部做 POC 的时候拿 iframe 试过一个模块结果那个页面打开超过半小时整个后台的响应就开始变卡。问题还不出在业务代码而是 iframe 本身就是个性能黑洞。single-spa 是祖师爷级的方案qiankun 的核心调度能力也是构建在它之上的。但 single-spa 只负责加载和生命周期管理JS 沙箱、样式隔离这些全都要自己额外实现。换句话说用 single-spa 你得先成为一名微前端框架开发者这在项目交付场景里是不可接受的。webpack Module FederationMF又是另一个维度的方案它解决的是“模块共享”问题可以做到构建时或运行时共享依赖。但 MF 更适合技术栈统一、构建链路可控的团队用在多技术栈并存的老项目改造上对各个子应用的构建工具版本要求很苛刻Vue CLI 3 这种老构建链路接起来会很痛苦。qiankun 强在它是站在 single-spa 肩膀上做的开箱即用封装把沙箱、样式隔离、通信都内置了而且对子应用的构建要求极低。它允许你继续用 Vue CLI、继续保留原有的 index.html甚至子应用不需要知道自己是微前端的一部分就能跑。这一点对存量老的 Vue2 项目简直太关键了。下面是当时我做技术选型时列的简表方案隔离能力接入改造成本对历史构建链路的兼容性适合场景iframe强极低无要求完全隔离的独立页面不追求交互统一single-spa无内置高需要改造成 JS Entry有精力自行封装团队Module Federation模块级中高要求 webpack5 统一构建技术栈统一的新体系qiankun内置 JS 沙箱 样式隔离低几乎任意HTML Entry 直读存量老项目多技术栈并存1.3 qiankun 的三板斧HTML Entry、JS 沙箱与样式隔离的原理qiankun 和早期微前端方案最大的区别是它默认用 HTML Entry 而不是 JS Entry。所谓 HTML Entry是说你在主应用里注册子应用时entry 字段直接填一个 URL比如http://localhost:7101。qiankun 会在运行时用 fetch 把这个地址对应的 HTML 拉下来解析出里面的script和link标签然后动态加载并执行这些脚本。老项目不需要为了让别人接入而特意改造成“导出 JS 文件”的形式入口是什么样微前端框架就直接按什么样去加载。这个设计让 Vue2 老项目的接入成本低了一大截你甚至都不用改子应用的 index.html 里引的那些全局脚本。JS 沙箱是 qiankun 做的第二件事。多个子应用挂在同一个页面下如果你不隔离 windowA 应用给 window 上挂了一个window.__fooB 应用再挂一个window.__foo两个应用就直接打架了。qiankun 在单实例模式下默认使用 Proxy 沙箱它给子应用构造了一个虚拟的 window 对象子应用对 window 的读写操作都会先经过这个 Proxy。写操作记录在沙箱内部应用卸载时再把变更过的属性复原。这样能保证子应用之间的全局变量互不干扰这也是我对 qiankun 最有信心的部分。不过在沙箱这件事上有一点要提前有心理准备Proxy 沙箱对通过Object.defineProperty修改全局对象、或者某些第三方 SDK 直接往 document 上塞东西的行为确实存在漏网之鱼。这点后面在坑的部分会细说。样式隔离是第三板斧。qiankun 提供了两种模式一个是严格模式的 Shadow DOM 隔离这个隔离效果最强但会让弹窗、下拉这些渲染到 body 上的组件样式失效另一个是实验性样式隔离它会在运行时把子应用的所有样式选择器加上div[data-qiankun应用名]这样的前缀。实际业务里我们用的就是实验性隔离配合团队内部的样式命名规范基本够用。2. 主应用改造实战一个 Vue2 壳工程的四个关键步骤2.1 安装 qiankun 并注册第一个子应用主应用这里其实就是把原来的 Vue2 项目当成一个“壳”只负责用户登录、框架布局、菜单权限和子应用调度。安装依赖很简单一条命令搞定npm i qiankun -S接下来在主应用的入口文件里注册子应用。我的建议是单独建一个micro-app.js来管理注册逻辑不要在main.js里堆一大堆注册代码后面加子应用的时候会非常乱。// src/micro-app.js import { registerMicroApps, start, initGlobalState } from qiankun; registerMicroApps([ { name: old-vue2-doc, // 子应用唯一标识不能重复 entry: //localhost:7101, // 开发环境指向子应用 devServer container: #micro-container, // 子应用挂载的 DOM 节点 activeRule: /doc-preview, // 当 URL 匹配到该路径时激活 props: { // 通过 props 下发静态配置 mainBase: /, appName: 文档预览模块 } } ]); start();这里有几个字段值得啰嗦一下。name不只是标识它还参与了样式隔离前缀的生成比如>activeRule: (location) location.pathname.startsWith(/doc-preview) location.query.from workbench很多教程到这里就完了但实际开发中主应用还需要处理一件事给子应用预留路由兜底。因为主应用的路由表里通常没有/doc-preview相关的路由直接访问这个地址时 Vue Router 会匹配不到组件页面就白屏。我当时的处理是在主应用路由的 catch-all 规则里把这类路径转发到布局组件让布局框架正常渲染然后由 qiankun 把子应用内容填充到容器里。2.2 start 参数怎么调沙箱、预加载与单例模式start()虽然可以直接调用但生产环境我建议把所有可选参数显式配置出来避免不同版本的默认行为差异踩坑start({ prefetch: all, // 预加载所有子应用也可设为 [old-vue2-doc] sandbox: { experimentalStyleIsolation: true, // 开启实验性样式隔离 proxy: true // 开启 Proxy 沙箱 }, singular: true, // 单实例模式同一时间只渲染一个子应用 fetch: window.fetch.bind(window) });singular这个参数在 Vue2 老项目场景下建议保持默认的 true。因为老项目里很多全局连接、全局定时器、全局监听都是按“单实例”假设写的如果开了多实例同一个子应用被同时挂载多次这些全局逻辑会直接互相踩踏。网上很多讨论说多实例是 qiankun 的卖点但那是针对新项目从一开始就按多实例规范来写的场景不建议在老项目改造时逞强。prefetch默认是 all意思是首屏加载完就去后台拉取所有子应用的静态资源。如果你的子应用数量多、首屏网络环境又差这里可以改成手动指定或者干脆关掉。我实际测试下来预加载对用户体验的提升主要体现在子应用首屏速度上少了 200 到 500 毫秒的加载等待但代价是主应用首屏会多传输几 MB 的静态资源。怎么取舍看你们的网络状况。2.3 主子应用通信全局状态、props 与兜底方案微前端架构里最容易被低估的就是通信设计。很多人一开始图省事直接用window上挂变量结果子应用一多根本分不清数据是谁写的。qiankun 官方提供了一套全局状态机制// 主应用初始化全局状态 const actions initGlobalState({ token: localStorage.getItem(token) || , userInfo: null }); // 监听状态变化 actions.onGlobalStateChange((state, prev) { console.log(子应用修改了全局状态, prev, state); }); // 主动更新状态 actions.setGlobalState({ token: new-token });子应用侧可以通过 mount 阶段的 props 拿到这套通信能力export async function mount(props) { props.onGlobalStateChange((state, prev) { // 监听到主应用状态变化 }); props.setGlobalState({ moduleState: ready }); }这套机制解决“主应用下发登录态、子应用上报状态”是够用的。但我在实际改造中发现一个场景它处理得不好两个子应用之间要直接做业务数据联动。这时候硬用全局状态会非常别扭因为状态变更需要先上报到主应用主应用再广播给其他子应用链路长且难排查。我的经验是登录态、权限、用户信息这类基础数据走initGlobalState子应用之间的业务数据走各自的业务接口让后端去做数据中轉前端不要试图通过微前端框架建立一个跨应用状态总线。说白了微前端是页面组织方案不是前端状态管理方案硬把状态耦合揉进去只会把系统搞得更僵。如果遇到确实需要解耦的场景我还会用window.dispatchEvent(new CustomEvent(xxx))这类浏览器原生事件做兜底。虽然听起来很原始但它在多框架共存的场景下意外地可靠每个子应用只需要知道自己关心哪个事件名就行。2.4 路由联动与加载失败的兜底处理主应用菜单点击跳转子应用这个链路其实由 activeRule 自动完成了用户访问/doc-preview/xxx时 qiankun 会自动加载对应子应用。但有一个体验细节容易忽略子应用内部的二级路由切换时主应用侧边栏的菜单高亮不会自动跟随。这个一般通过主应用监听路由变化、再根据路径匹配菜单项来解决代码不复杂但必须在规划阶段就写好否则后面加子应用就会出现菜单高亮和内容对不上的体验问题。qiankun 启动后容器区域在子应用加载过程中是空白的。我建议在主应用布局的容器节点附近放一个 Loading 组件然后通过start({ loader })或者手动监听子应用加载中状态来控制它的显隐。这里我用了最简单的方式在container的 DOM 节点内部预先放一行“模块加载中”的提示等子应用 mount 完成qiankun 会自动往容器里填内容加载提示自然被顶掉。还有一个必须处理的问题是子应用加载失败。比如子应用服务挂了、静态资源 404qiankun 只会往控制台抛异常页面容器会一直空白。我们的做法是在主应用的registerMicroApps生命周期钩子里监听load失败事件然后渲染一个统一的错误组件registerMicroApps(apps, { beforeLoad: () {}, beforeMount: () {}, afterUnmount: () {}, // 加载失败时的处理目前建议用 try/catch 包住 start或者监听子应用 entry fetch 的异常 });qiankun 的start()返回的是一个 Promise有异常会冒泡出来。把start()包一层 try/catch失败时在主应用里跳转到错误页或提示刷新效果比干等白屏强得多。3. Vue2 子应用接入老项目改造最容易翻车的三个地方3.1 生命周期导出与独立运行判断子应用侧的第一步是把入口文件从“直接创建 Vue 实例”改成“导出生命周期函数”。qiankun 要求子应用必须导出的三个生命周期是bootstrap、mount、unmount。这里有个容易搞混的点qiankun 要求的是“导出”而不是“挂在 window 上”。Vue CLI 默认打包出来的库是 UMD 格式会把这个导出挂到window上qiankun 的 HTML Entry 机制是能拿到 window 上的生命周期函数的。但要写成下面的标准结构兼容性最稳。// src/main.js import Vue from vue; import App from ./App.vue; import router from ./router; import store from ./store; let instance null; function render() { instance new Vue({ router, store, render: h h(App) }).$mount(#app); } // 独立运行直接渲染 if (!window.__POWERED_BY_QIANKUN__) { render(); } // 子应用生命周期 export async function bootstrap() { console.log(vue2 子应用 bootstrap); } export async function mount(props) { render(); } export async function unmount() { instance.$destroy(); instance null; // 清干净容器里的 DOM避免二次挂载时内容重复 const container document.querySelector(#app); container container.innerHTML ; }window.__POWERED_BY_QIANKUN__是 qiankun 在沙箱环境注入给子应用的标记用来区分当前是独立运行还是被微前端加载。这个判断必须放在入口最早的位置因为有些全局初始化逻辑只在独立运行时要执行比如开发环境的 mock 数据、独立的登录跳转等。子应用被反复挂载和卸载时最容易出问题的是没有执行反初始化。比如unmount阶段只把instance置空了但全局的事件监听、定时器、第三方实例都还残留着。这里我的经验是子应用里所有“启动”逻辑都包到mount里所有“清理”逻辑都包到unmount里不要在模块顶层做任何会被重复执行的初始化。3.2 publicPath 与分包输出静态资源 404 的根源这一步是 Vue2 旧项目接 qiankun 时翻车率最高的环节没有之一。默认情况下Vue CLI 构建出来的资源路径是绝对路径/js/app.js。子应用被嵌进主应用后如果子应用部署在子路径比如/doc-preview/下直接访问/js/app.js会打到主应用的域名根目录去必然 404。解决办法是让子应用的 webpack publicPath 变成动态的。官方推荐的方式其一是在入口文件里引入qiankun提供的 setPublicPathimport { setPublicPath } from qiankun; setPublicPath(/doc-preview/);这种方式会把运行时 publicPath 设置成部署后的子路径前缀。但我自己项目里更推荐直接用 webpack 的动态 publicPath 方案因为它不依赖 qiankun 导出的方法独立运行时不也不会被误伤// 入口文件最顶部 if (window.__POWERED_BY_QIANKUN__) { __webpack_public_path__ window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; }__INJECTED_PUBLIC_PATH_BY_QIANKUN__是 qiankun 在加载子应用时注入好的运行时 publicPath把它赋给__webpack_public_path__会让所有懒加载 chunk、图片引用的相对基础路径都变成子应用自己的部署路径。这个变量在子应用入口里要尽早赋值必须在任何 import 其他模块的语句之前执行webpack 的公共路径计算发生在模块加载最开始。除了 publicPath还有一个很隐蔽的分包问题。两个 Vue 子应用如果都是 Vue CLI 默认配置打包分包的 chunk 命名规则都是chunk-xxxx主应用同时预加载两个子应用时可能出现 webpack runtime 里的 chunk 命名冲突表现在浏览器里是加载了 A 应用的 chunk却被 B 应用拿来执行。我们的应对方案是在子应用的 vue.config.js 里给 chunk 命名加上应用前缀module.exports { productionSourceMap: false, configureWebpack: { output: { chunkFilename: old-doc/[name].[contenthash:8].js } } };这个改动成本极低但能让多个子应用的产物在浏览器 cache 里井水不犯河水。另外一个容易被忽略的点是 CSS 里的字体文件、背景图片它们使用的相对路径同样基于 publicPath 解析。我们项目里有一个 iconfont 图标库字体文件路径在样式隔离开启后一直报 404排查到最后就是 publicPath 没覆盖到字体文件。处理方案一个是把字体文件改成绝对 URL 引用另一个是确保 media 目录一起被打包到子应用部署目录下。3.3 路由 base、挂载容器与开发环境联调子应用的 Vue Router 在主应用环境下运行路由实例的基础路径必须和 activeRule 匹配。比如主应用在/doc-preview激活这个子应用那子应用的路由 base 也要设置成/doc-previewconst router new VueRouter({ mode: history, base: window.__POWERED_BY_QIANKUN__ ? /doc-preview : /, routes });如果这个 base 不设置子应用内部跳转时 URL 就会变成主应用根路径 子应用内部路径activeRule 失配子应用会被直接卸载掉表现为“进去页面一闪就回到主应用首页了”。挂载容器#app在上面的代码里直接用了页面上的节点但实际运行在微前端环境时这个节点存在于主应用的 DOM 里。要确保子应用脚本执行时这个节点一定存在否则$mount(#app)会报找不到目标容器。我们的做法是在主应用布局里把容器节点写死在模板上不要用 v-if 包裹否则 qiankun 激活容器时节点还没创建出来子应用 mount 就会失败。开发环境联调是另一个高频坑。子应用npm run dev监听localhost:7101主应用注册时的 entry 也填了这个地址但你需要给子应用的 devServer 加上 CORS 响应头否则主应用 fetch 子应用的 HTML 时会因为跨域被浏览器拦截// vue.config.js module.exports { devServer: { port: 7101, headers: { Access-Control-Allow-Origin: * } } };这里我额外提醒一句开发环境让子应用单独跑在 7101主应用跑在另一个端口两个应用在微前端环境下共享了 localStorage但主应用和子应用如果你配置了不同的publicPath和 URL 前缀它们内部存储的键容易互相覆盖。建议从一开始就把存储键规划好命名前缀比如$old-doc-token、$main-token避免调试时数据串了怀疑人生。4. 跑起来之后躲不开的坑样式、全局变量与部署4.1 样式隔离的边界弹窗、字体图标和 body 级 DOMqiankun 的实验性样式隔离原理是动态地把子应用里的样式选择器套上一层div[data-qiankun应用名]前缀。这意味着只有挂在子应用容器内部的 DOM才会被这层前缀命中。但 Element UI 这类组件库的 MessageBox、Select 下拉、Tooltip 是直接往 body 上挂节点的这些节点不在容器内样式隔离对它们完全不生效表现就是弹窗样式错乱或者按钮间距失控。这个问题有三个层次的处理方法。假如你能接受少量样式互相渗透最简单的方案是关掉experimentalStyleIsolation改为让主应用和子应用都用同一套 element-ui 样式版本。我们项目里主应用和子应用之前就有版本差异合到一起后样式乱成一团最后统一把两边的 element-ui 都升级到同一版本弹窗样式问题少了一大半。如果必须要隔离那就要给“弹窗层”单独做一份全局样式。Element UI 组件会渲染到 body 下如果你给它加一个特殊的应用命名空间比如通过appendTo属性把弹窗挂载到容器内那样式隔离前缀就能命中。某些组件不支持 appendTo 的话就只能手动修改弹窗 DOM 节点等 render 完后给它加上>location /doc-preview { try_files $uri $uri/ /doc-preview/index.html; } location /doc-preview/index.html { add_header Cache-Control no-cache, no-store; }子应用里面的 JS、CSS、图片等带 hash 的资源文件则要开长效缓存这样既保证及时更新又能享受缓存加速。还有一个刷新 404 的问题。单页应用如果直接刷新一个子路由比如访问/doc-preview/detail/123Nginx 找不到这个物理路径会返回 404。上面try_files $uri $uri/ /doc-preview/index.html;的配置会把请求都 fallback 到子应用的 index.html再由子应用路由接管渲染这样刷新就不会白屏。主应用也要注意如果主应用不是部署在根路径比如部署在/portal/下那么主应用路由的 base 以及 qiankun 注册子应用时的 activeRule 都要按这个前缀走这个容易漏。5. 常见问题速查表与个人实操体会5.1 按报错现象快速定位把这次改造里遇到的高频问题和对应的排查思路整理成了下面这张表遇到类似情况可以直接照着对。现象可能原因处理建议子应用一直加载中容器空白entry 域名跨域、子应用 devServer 没开 CORS检查 Network 里 entry HTML 请求是否 CORS 报错给子应用加响应头子应用 JS 加载后提示生命周期导出失败入口文件没有正确导出 bootstrap/mount/unmount检查入口文件导出确认使用 webpack UMD 格式子应用内部图片、字体 404publicPath 未动态化或路径前缀不对在入口最顶部设置__webpack_public_path__或setPublicPath进入子应用后闪退回主应用首页子应用路由 base 与 activeRule 不匹配确认子应用 base 等于主应用激活路径前缀子应用弹窗样式错乱弹窗挂在 body 下样式隔离前缀未覆盖统一 UI 组件版本或通过 appendTo 让弹窗挂载到容器内二次进入子应用报错或页面重复渲染unmount 未销毁 Vue 实例和全局监听检查 unmount 中是否执行$destroy()和事件清理线上更新半天不生效子应用 index.html 被缓存配置 index.html 不缓存hash 资源长效缓存子应用 A 挂载后主应用埋点失效子应用内脚本覆盖了 window 上的同名全局变量收敛子应用全局变量公共 SDK 统一由主应用初始化两个子应用 chunk 命名冲突webpack 分包 chunk 命名未加前缀给分包配置独立chunkFilename命名空间5.2 几句掏心窝的话这次改造下来我最大的感受是微前端接入的技术难度其实不高真正的复杂度在于周围那些“非技术”的约束。比如你得说服老团队接受一个新的发布流程得跟每个业务模块的负责人确认拆分的边界还得处理老代码里各种不规范的全局写法。如果你正准备在自己的 Vue2 项目里引入 qiankun我建议按这个顺序推进先挑一个边界清晰、独立迭代频率高的模块试水我们当时就选的文档预览模块验证从主应用注册、子应用改造、部署到联调的完整链路跑顺之后再逐步把其它模块迁移进来。不要一上来就规划一个宏大无比的全量拆分盘子那不是微前端改造那是项目重构风险完全不是一个量级。开发规范和回归测试清单要在真正联调之前就定好尤其是生命周期里所有销毁动作。我在联调阶段有一半的时间都花在“切到另一个子应用再切回来发现页面崩了”的问题上后来总结的经验就一句话每一个在 mount 里创建的东西都要在 unmount 里找到对应的销毁代码。最后再分享一个小技巧把子应用统一设计成“裸跑”模式也就是不依赖主应用环境也能独立运行。这样每个子应用都能单独开 devServer 开发能单独部署、单独验证排查问题时也不用总开着主应用效率会高很多。我们后续接入 Vue3 Vite 新技术栈的步子能走得稳靠的就是这个先定好的规则。
返回列表