ARTICLE DETAIL

资讯详情

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

微前端架构实战:qiankun核心原理与生产级应用指南

微前端架构实战:qiankun核心原理与生产级应用指南 1. 为什么我们需要微前端以及为什么是qiankun如果你正在维护一个大型的、历史悠久的单体前端应用或者你的团队正在尝试将多个独立开发、技术栈各异的应用整合到一个统一的用户界面中那么“微前端”这个概念对你来说应该不陌生。简单来说微前端就是将后端微服务的架构思想延伸到了前端领域。它允许你将一个庞大的前端应用拆分成多个可以独立开发、独立部署、独立运行的小型应用最后再组合成一个完整的应用。听起来很美好但现实往往很骨感。我见过不少团队在尝试微前端时要么被复杂的通信机制搞得焦头烂额要么在样式隔离、JS沙箱这些基础问题上反复踩坑最终要么放弃要么搞出一个维护成本极高的“缝合怪”。这正是为什么我们需要一个成熟的框架来提供一套“最佳实践”和“基础设施”而qiankun就是目前社区里最受认可的选择之一。qiankun并不是凭空出现的它基于一个更底层的框架single-spa。你可以把single-spa看作是微前端的“路由器”和“生命周期管理器”它定义了应用如何注册、如何挂载和卸载。但single-spa只解决了“路由”和“生命周期”的问题对于实际生产环境它还缺少很多关键能力比如JS沙箱、样式隔离、资源预加载、应用间通信等。qiankun正是在single-spa的基础上补齐了这些生产级能力并提供了更友好的API让开发者能更专注于业务开发而不是重复造轮子去解决那些底层的基础设施问题。所以当你决定采用微前端架构时直接使用qiankun意味着你站在了一个相对成熟的起点上可以避免很多前人已经踩过的坑。接下来我们就深入它的内部看看它是如何工作的以及如何把它用起来。2. qiankun的核心原理不只是“加载iframe”那么简单很多人初次接触微前端会直觉地想到用iframe。iframe确实天然具备沙箱隔离独立的浏览器上下文和样式隔离的优势但它的问题也同样突出路由状态不同步、全局上下文完全隔离导致通信困难、性能开销大、SEO不友好等。qiankun走的是另一条路基于JavaScript的运行时集成。它通过动态加载子应用的资源HTML、JS、CSS并在主应用的页面上下文中执行它们从而实现“一个页面多个应用”的效果。这听起来有点“黑魔法”其核心原理可以拆解为几个关键部分。2.1 应用加载与资源导入如何“凭空”运行一个应用qiankun加载一个子应用并不是简单地在页面里插入一个script标签。它遵循一个标准的流程获取应用入口首先qiankun会根据你配置的entry入口地址通常是一个HTML文件的URL去请求这个HTML文件。解析HTML提取资源拿到HTML后qiankun会解析其内容从中提取出所有的script和link标签。这一步是关键它需要区分哪些是子应用运行时必需的资源如主JS文件、CSS文件哪些是公共资源或不需要加载的资源。动态创建并执行脚本/样式对于提取出的JS和CSS资源链接qiankun会动态创建script或link标签并将其插入到主应用的DOM中。对于JS脚本它需要确保在正确的时机应用挂载前执行。这里有一个非常重要的细节子应用的资源地址URL可能是相对的。比如子应用的主JS文件路径是/static/js/main.js。当这个HTML在主应用的域名下被解析时浏览器会基于主应用的当前路径去请求这个资源这大概率会导致404。qiankun的解决方案是资源地址补全。它在解析HTML时会记录子应用的entry地址如https://child-app.com然后将所有提取到的相对路径资源都补全为基于这个entry的绝对路径如https://child-app.com/static/js/main.js从而确保资源能够被正确加载。2.2 JS沙箱让多个应用“和平共处”的关键这是qiankun最核心、也最复杂的部分之一。想象一下两个子应用都修改了window.Promise或者都监听了window.onerror事件如果没有隔离它们会互相覆盖导致不可预知的行为。qiankun通过创建JS沙箱来为每个子应用提供一个独立的全局环境。qiankun主要实现了两种沙箱模式SnapshotSandbox快照沙箱原理在子应用挂载前遍历当前window对象的所有属性拍一张“快照”存起来。然后子应用可以任意修改window。当子应用卸载时qiankun会对比当前window和之前存的快照将window恢复成快照时的状态当子应用再次挂载时再应用上一次修改后的状态。特点兼容性极好因为它本质上只是属性的备份与恢复。但缺点是不支持多实例即同一时间只能运行一个子应用且性能相对较差需要遍历大量属性。ProxySandbox代理沙箱原理利用ES6的Proxy特性为每个子应用创建一个假的window对象我们称之为proxyWindow。当子应用代码访问或设置全局变量时实际上操作的是这个proxyWindow。proxyWindow内部维护着自己的属性池与真实的window隔离。同时对于一些不可配置的全局属性如window.location,window.top代理会将其指向真实的window。特点性能好支持多实例同时运行是qiankun默认且推荐的沙箱模式。但它的前提是浏览器需要支持Proxy。在实际使用中qiankun会根据浏览器支持情况自动选择沙箱。对于不支持Proxy的旧浏览器如IE它会降级到SnapshotSandbox。2.3 样式隔离避免CSS的“世界大战”JS有沙箱CSS同样需要隔离。否则子应用A的一个.btn { color: red; }可能会把主应用或其他子应用的按钮全部染红。qiankun提供了两种样式隔离方案Scoped CSS实验性qiankun会为子应用动态添加一个属性选择器前缀例如将.btn转换为[data-qiankun”appName”] .btn并将这个属性添加到子应用容器元素上。这样子应用的样式就只会作用于其容器内部。这种方式比较轻量但并非绝对安全某些动态添加的样式可能无法被捕获。Shadow DOM这是浏览器原生的隔离方案。qiankun可以将子应用的容器元素变成一个ShadowRoot。Shadow DOM内部的样式和DOM与外部完全隔离就像是一个“黑盒”。这是最彻底的隔离方式。但是Shadow DOM的兼容性需要考虑而且它会让一些全局性的操作如document.body.appendChild在子应用内失效也可能导致某些UI库特别是那些依赖全局样式或直接操作document的库出现样式问题。因此qiankun默认没有开启Shadow DOM需要显式配置。在大部分场景下使用默认的样式处理配合良好的子应用CSS编写规范加上Scoped CSS基本够用。对于样式冲突极其严重的遗留系统可以考虑开启Shadow DOM但要做好充分的测试。2.4 应用间通信松耦合的交互之道微前端应用之间需要通信但又不能紧密耦合。qiankun提供了一套基于发布订阅Pub/Sub模式的通信机制。initGlobalState(state)在主应用中调用初始化一个全局状态并返回通信实例。onGlobalStateChange(callback)主应用或子应用都可以订阅状态变化。setGlobalState(state)修改全局状态所有订阅者都会收到通知。它的本质是主应用维护一个中心化的状态对象并通过qiankun提供的API暴露给子应用。子应用可以监听和修改这个状态从而实现数据共享。这是一种松耦合的通信方式子应用不需要知道状态来自哪个具体的应用只需要关心状态本身。注意虽然方便但要谨慎使用全局状态。避免将大量频繁变化或私有的数据放在全局状态中否则很容易导致状态管理混乱和性能问题。通常它更适合用于传递一些共享的、不频繁变化的配置信息或用户身份信息。3. 从零开始一个完整的qiankun实战指南理解了原理我们动手搭建一个最简单的qiankun demo。这个demo将包含一个主应用基座和一个子应用。我们假设主应用使用Vue 3子应用使用React 18这是微前端中非常典型的异构技术栈集成场景。3.1 主应用基座配置首先创建一个Vue 3项目作为主应用。npm create vuelatest main-app cd main-app npm install安装qiankunnpm install qiankun -S接下来修改主应用入口文件通常是main.js或app.js注册并启动子应用。// main.js import { createApp } from vue import App from ./App.vue import { registerMicroApps, start } from qiankun; const app createApp(App); // 注册子应用 registerMicroApps([ { name: react-app, // 子应用名称必须唯一 entry: //localhost:3001, // 子应用的入口地址开发环境是本地服务 container: #subapp-container, // 子应用挂载的容器ID activeRule: /react, // 激活规则当URL以/react开头时加载该子应用 }, ]); // 启动 qiankun start(); app.mount(#app);然后在主应用的页面模板中如App.vue放置子应用的挂载容器并设置导航。!-- App.vue -- template div idapp h1主应用 (Vue 3)/h1 nav router-link to/首页/router-link | router-link to/reactReact子应用/router-link /nav !-- 主应用自己的路由视图 -- router-view v-if!isMicroApp / !-- 子应用挂载容器 -- div idsubapp-container v-else/div /div /template script setup import { ref, watch } from vue; import { useRoute } from vue-router; const route useRoute(); const isMicroApp ref(false); watch(() route.path, (newPath) { // 判断当前路径是否匹配子应用的激活规则 isMicroApp.value newPath.startsWith(/react); }); /script3.2 子应用React改造子应用需要“暴露”其生命周期钩子函数以便qiankun在适当的时机调用。这通常被称为“导出生命周期”。创建一个React子应用npx create-react-app react-child cd react-child安装craco/craco或react-app-rewired来覆盖Create React App的webpack配置因为我们需要修改打包输出格式。npm install craco/craco -D在项目根目录创建craco.config.js// craco.config.js module.exports { webpack: { configure: (webpackConfig, { env, paths }) { // 1. 修改输出为UMD库格式并指定库名 webpackConfig.output.library react-app; webpackConfig.output.libraryTarget umd; // 2. 将webpack的publicPath改为动态的这样资源才能被正确加载 // 注意这里和下面的public path配置相关 webpackConfig.output.publicPath auto; // 或者根据环境动态设置 // 3. 允许跨域因为主应用和子应用域名/端口可能不同 webpackConfig.headers { Access-Control-Allow-Origin: *, }; return webpackConfig; }, }, devServer: (devServerConfig) { // 开发服务器允许跨域 devServerConfig.headers { Access-Control-Allow-Origin: *, }; return devServerConfig; }, };修改package.json中的scripts使用craco启动scripts: { start: craco start, build: craco build, test: craco test, eject: react-scripts eject }最关键的一步在子应用的入口文件src/index.js中导出qiankun要求的生命周期函数。// src/index.js import React from react; import ReactDOM from react-dom/client; import ./index.css; import App from ./App; // 独立运行时直接渲染 if (!window.__POWERED_BY_QIANKUN__) { const root ReactDOM.createRoot(document.getElementById(root)); root.render(App /); } // 定义子应用的生命周期 export async function bootstrap() { console.log([react-app] bootstraped); } export async function mount(props) { console.log([react-app] mount, props); // props 中包含从主应用传递过来的信息如容器等 const { container } props; const root ReactDOM.createRoot(container ? container.querySelector(#root) : document.getElementById(root)); root.render(App /); } export async function unmount(props) { console.log([react-app] unmount); const { container } props; const root ReactDOM.createRoot(container ? container.querySelector(#root) : document.getElementById(root)); root.unmount(); }注意我们在mount函数中使用了container.querySelector(#root)。这是因为qiankun会将子应用挂载到主应用指定的容器#subapp-container内我们需要在这个容器内部找到一个节点来渲染React应用。通常我们会在子应用的public/index.html中保留一个div idroot/div。3.3 运行与联调启动React子应用npm start它默认运行在http://localhost:3001。启动Vue主应用npm run dev假设运行在http://localhost:3000。打开浏览器访问http://localhost:3000。点击“React子应用”链接URL变为http://localhost:3000/react。此时qiankun会动态加载localhost:3001的资源并将其渲染到#subapp-container中。你应该能看到React应用的内容出现在Vue主应用的页面里。至此一个最基本的qiankun微前端应用就跑通了。你可以看到React应用完全运行在Vue应用的页面上下文中路由由主应用控制实现了无缝集成。4. 生产环境部署与高级配置实战开发环境跑通只是第一步要上线还需要考虑很多问题。这里分享几个关键的生产级配置和避坑经验。4.1 子应用资源加载与Public Path难题这是部署时最容易出问题的地方。在开发环境我们通过devServer解决了跨域和资源加载。但在生产环境子应用通常会被打包成静态文件部署在CDN或单独的服务器上。问题子应用打包后JS、CSS、图片等资源的路径是固定的如/static/js/main.abc123.js。当这个资源被主应用从不同路径如https://main-app.com/app/react加载时浏览器会向https://main-app.com/app/react/static/js/...发起请求导致404。解决方案动态设置webpack的publicPath。对于Webpack 5我们可以在入口文件最顶部动态设置__webpack_public_path__// 在子应用src/index.js的最顶部添加 if (window.__POWERED_BY_QIANKUN__) { // eslint-disable-next-line no-undef __webpack_public_path__ window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; }window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__是qiankun在加载子应用时自动注入的变量它的值通常是子应用entry的地址。这样子应用的所有资源请求都会基于这个正确的基地址。同时在craco.config.js中我们已经将output.publicPath设为了autoWebpack 5会自动处理这种运行时动态public path的情况。对于Webpack 4可能需要更复杂的配置。4.2 路由模式与状态同步微前端中路由是一个需要精心设计的部分。qiankun支持两种主要的集成模式主应用控制路由推荐就像我们上面的demo主应用使用vue-router或react-router子应用的激活由activeRule决定。子应用内部的路由最好是记忆路由或基于基础路径的路由。记忆路由子应用内部路由使用hash模式如#/user。这样子应用的路由变化不会影响主应用的URL完全由子应用自己管理。缺点是URL不直观且浏览器前进后退行为需要额外处理。基础路径路由子应用使用browserHistory但需要设置一个basename。例如子应用知道自己的激活规则是/react那么它内部的所有路由都应以/react为前缀。这需要主应用和子应用对路由规则有明确的约定。子应用自带路由主应用只负责加载和卸载子应用子应用内部有完整的路由系统。这要求主应用的路由器能“让出”部分URL空间给子应用实现起来更复杂容易冲突。我的经验是对于大多数项目采用“主控路由 子应用Hash路由”是最简单、冲突最少的方案。如果对URL美观有要求并且团队沟通顺畅可以采用“主控路由 子应用Basename路由”方案并建立严格的路由命名规范。4.3 预加载与性能优化当用户点击导航菜单时再去加载子应用资源可能会有一个明显的延迟。qiankun提供了预加载功能。在主应用启动qiankun时进行配置import { start, registerMicroApps } from qiankun; start({ prefetch: true, // 开启预加载默认为 true // prefetch: all // 预加载所有应用 // prefetch: [app1, app2] // 预加载指定应用 });开启prefetch: true后qiankun会在浏览器空闲时利用requestIdleCallback自动去加载那些注册了但尚未激活的子应用资源。这样当用户真正切换过去时资源可能已经缓存好了体验会流畅很多。4.4 全局状态管理与通信进阶基础的initGlobalState对于简单的数据共享够用但对于复杂场景你可能需要更强大的状态管理方案。方案一自定义Event Bus主应用和子应用都基于一个共享的、轻量级的事件总线例如mitt或EventEmitter进行通信。这比qiankun内置的全局状态更灵活可以传递事件而不仅仅是数据。方案二共享状态库如果主应用和子应用都使用相同的状态库如Redux、Mobx、Pinia、Vuex可以考虑创建一个共享的Store实例。这个Store实例由主应用创建并通过qiankun的props传递给子应用。子应用直接连接和使用这个Store。这种方式耦合度稍高但状态管理最专业、最强大。方案三基于Props传递在注册子应用时可以通过props字段将数据或方法传递给子应用。这种方式是单向的、一次性的适合传递配置或回调函数。registerMicroApps([ { name: react-app, entry: //localhost:3001, container: #subapp-container, activeRule: /react, props: { // 传递主应用的用户信息或公共方法 userInfo: mainAppUserInfo, onLogout: () { /* 主应用的登出逻辑 */ } } }, ]);在子应用的mount生命周期中可以通过props参数接收到这些数据。5. 常见报错排查与性能调优心得在实际项目中你一定会遇到各种报错。这里我总结几个最典型的。5.1 “Application died in status LOADING_SOURCE_CODE”这个错误通常意味着qiankun在加载子应用入口HTML时失败了。检查1子应用服务是否可访问。确保子应用的开发服务器已启动且entry地址如//localhost:3001在浏览器中能直接打开。检查2跨域问题。确保子应用的开发服务器如webpack-dev-server配置了正确的CORS头。我们在craco.config.js中已经配置了headers: { Access-Control-Allow-Origin: * }。检查3子应用入口HTML是否正确导出了生命周期。打开子应用的entry地址查看页面源代码确认全局变量window.子应用名是否存在并且包含了bootstrap,mount,unmount三个函数。5.2 样式丢失或混乱现象子应用加载后样式完全没生效或者样式污染了主应用。排查检查子应用的CSS文件是否被正确加载。打开浏览器开发者工具的“网络”选项卡查看CSS文件是否返回200状态码。如果CSS文件加载了但没生效可能是样式隔离导致的。尝试在主应用启动qiankun时关闭样式隔离{ sandbox: { strictStyleIsolation: false } }看样式是否恢复。如果恢复了说明是样式隔离配置问题。对于使用UI库如Ant Design, Element的子应用如果样式异常可能是因为UI库的样式是全局引入的被qiankun的样式隔离影响了。可以考虑将UI库的样式通过link标签外链而不是打包进JS。5.3 子应用内部路由跳转失败现象在子应用内点击路由链接URL变了但页面没刷新或者跳转到了错误的页面。排查确认子应用的路由模式。如果是browserHistory必须正确设置basename并且主应用服务器需要配置对所有子应用路由的Fallback返回主应用入口HTML。如果是hash路由确保跳转使用的是hash模式如#/page。检查子应用的路由组件是否被正确渲染。在子应用的mount生命周期中确保将container正确传递给ReactDOM.render或Vue.mount。5.4 内存泄漏与性能监控微前端应用由于动态加载和卸载更容易产生内存泄漏。一个常见的泄漏点是事件监听器。子应用在mount时绑定了全局事件如window.addEventListener(resize, ...)但在unmount时没有移除。最佳实践在子应用的unmount生命周期中务必进行彻底的清理工作卸载React/Vue根实例。清除所有定时器setInterval,setTimeout。移除所有全局事件监听器。重置任何可能修改的全局状态如果沙箱是Snapshot模式qiankun会帮你做一部分。对于性能除了开启预加载还可以考虑按需加载不是所有子应用都需要一开始就注册。可以根据用户权限或菜单配置动态调用loadMicroApp来加载应用。资源监控利用浏览器的PerformanceObserverAPI监控子应用加载过程中的资源加载耗时、JS执行时间等找出性能瓶颈。微前端的引入必然会带来一定的复杂度qiankun的价值在于它通过一套经过大量实践检验的解决方案帮你封装了其中最棘手、最易出错的部分。从原理理解到实战配置再到生产部署和问题排查每一个环节都需要仔细考量。我的建议是在决定全面采用微前端之前先用一个简单的业务模块进行试点把整个流程跑通把可能遇到的坑都踩一遍再评估它是否真的适合你的团队和项目。技术选型没有银弹qiankun是微前端领域一个非常优秀的工具但用好它依然需要你对它的原理和细节有足够的掌控。
返回列表