标签

Garfish

Garfish 是一个轻量级的微前端框架,旨在帮助开发者实现多应用的集成与协作,提升大型复杂前端项目的开发效率和维护性。它通过沙箱技术隔离不同子应用的运行环境,保证各个微应用之间的独立性和安全性,同时支持多种加载方式,包括异步加载和预加载,优化整体性能。Garfish 兼容主流前端框架如 React、Vue 和 Angular,方便开发者在不同技术栈之间灵活切换和组合。它提供了丰富的生命周期钩子,方便开发者管理微应用的加载、挂载和卸载过程,支持路由同步和状态共享,增强子应用之间的协同能力。Garfish 还注重开发体验,具备良好的调试工具和友好的错误提示,帮助快速定位问题。通过使用 Garfish,企业能够实现前端架构的模块化和解耦,促进团队并行开发,降低项目复杂度,提高系统的可扩展性和可维护性,适用于大型互联网应用和多团队协作的场景。

Garfish
前端5月31日 17:12
Garfish 沙箱隔离如何实现?快照沙箱和 Proxy 沙箱怎么选?Garfish 沙箱隔离主要解决子应用污染全局环境的问题。微前端里,每个子应用都可能写 `window`、注册事件、启动定时器、插入样式或改写全局对象;如果没有隔离,A 应用卸载后的副作用会影响 B 应用。Garfish 的思路是运行时拦截全局访问,卸载时清理可追踪副作用,但它不是安全容器,不能替代鉴权、CSP 和代码审查。 ## 快照沙箱:成本低,但更像事后恢复 快照沙箱会在子应用挂载前记录一份全局环境,运行后对比变化,卸载时恢复。它的好处是理解简单,对旧浏览器更友好,也适合一次只激活一个子应用的老系统改造。边界是它不阻止运行期间的污染;如果多个子应用同时活跃,某个应用写入的全局变量可能已经被另一个应用读到了。 ## Proxy 沙箱:隔离更强,适合多实例 Proxy 沙箱给子应用提供代理后的全局对象。写入 `window.foo` 时,数据优先落在子应用自己的记录里;读取时,再按规则从沙箱或真实 window 中取。它更适合现代浏览器和多个子应用同时存在的场景。 ```js function createSandbox(rawWindow) { const fakeWindow = Object.create(null) return new Proxy(fakeWindow, { get(target, key) { return key in target ? target[key] : rawWindow[key] }, set(target, key, value) { target[key] = value return true } }) } ``` 真实实现还要处理 `this` 指向、不可配置属性、白名单变量和动态脚本执行上下文。踩坑点是某些第三方库强依赖真实 window,Proxy 下会出现边界行为,需要白名单透传或改为主应用共享依赖。 ## 严格隔离还要清理副作用 只隔离变量不够。事件监听、定时器、WebSocket、全局弹层和样式标签都可能留下副作用。子应用应在生命周期中主动释放资源,不要完全依赖框架兜底。 ```js let timer export function mount() { timer = window.setInterval(refresh, 5000) window.addEventListener('resize', handleResize) } export function unmount() { clearInterval(timer) window.removeEventListener('resize', handleResize) } ``` 还有一个容易忽略的边界是异步任务。子应用卸载后,之前发出的请求可能仍会回调并修改 DOM 或状态,因此要用 AbortController、请求序号或 mounted 标记做保护。否则用户快速切换路由时,会出现旧页面数据覆盖新页面的诡异问题。这类问题不一定被沙箱捕获,因为请求回调属于业务逻辑。框架能帮你隔离全局副作用,但无法判断哪个接口结果已经过期。 ## 追问 ### 快照沙箱和 Proxy 沙箱怎么选? 只运行一个子应用、还要兼容旧环境时,快照沙箱更容易落地。多个子应用同时运行,或团队希望更强隔离时,应优先考虑 Proxy 沙箱。取舍点是兼容性、隔离强度和第三方库适配成本。 ### 沙箱能保证子应用安全吗? 不能。Garfish 沙箱主要防止应用间副作用污染,不是浏览器级安全边界。恶意代码如果能执行,仍可能发请求、读可访问数据或操作 DOM,所以敏感能力必须靠权限、CSP 和后端校验控制。 ### 为什么卸载后还有内存泄漏? 因为泄漏常来自业务自己创建的定时器、订阅、闭包引用和未关闭连接。沙箱能记录一部分全局副作用,但无法理解每个业务异步任务的意图。排查时可以反复进入和离开子应用,观察 listener、timer、DOM 节点数量是否持续上涨。 ### 第三方库在沙箱里异常怎么办? 先确认它是否依赖真实 `window`、全局单例或不可配置属性。能改配置就让它使用子应用容器,不能改时再考虑白名单透传。不要因为一个库异常就整体关闭沙箱,否则隔离边界会被打穿。
前端5月31日 17:12
Garfish 主子应用如何通信?状态共享有哪些边界和坑?Garfish 主子应用通信不要一开始就设计成“大而全的全局 store”。更稳的做法是按数据生命周期拆分:挂载时需要的上下文用 props,跨应用通知用事件总线,长期共享的登录态、主题、权限用平台服务封装。这样子应用仍能独立开发和部署,主应用也不会变成所有业务状态的垃圾桶。 ## props 传稳定上下文 props 适合传用户信息读取函数、API 前缀、路由 basename、权限快照和平台服务入口。它简单、可测试,子应用挂载时就能拿到,不需要等待异步事件。边界是 props 不适合高频变化数据,比如未读数、协同编辑状态或实时价格;这些数据用 props 传,会让刷新和同步逻辑很难维护。 ```js Garfish.run({ apps: [{ name: 'order', entry: 'https://cdn.example.com/order/', activeWhen: '/order', props: { basename: '/order', services: { auth: { getToken: () => auth.getToken(), logout: () => auth.logout() }, theme: { get: () => theme.current } } } }] }) ``` 这里传函数比传固定 token 更安全。登录刷新后,子应用再次调用 `getToken` 能拿到新值,不会继续使用旧凭证。 ## 事件总线传“发生了什么” 事件适合通知主题切换、登录过期、订单创建成功、侧边栏收起这类动作。它的优点是解耦,一对多传播很方便。踩坑点是事件不是状态存储,晚挂载的子应用可能错过历史事件,所以关键状态仍要能主动读取。 ```js export const platformBus = { emitTheme(theme) { Garfish.channel.emit('platform:theme-change', { theme }) }, onTheme(handler) { Garfish.channel.on('platform:theme-change', handler) return () => Garfish.channel.off('platform:theme-change', handler) } } ``` 每次订阅都要在卸载时取消,否则重复进入子应用后会出现多次触发,看起来像接口重复请求,实际是监听器泄漏。 ## 共享状态要少而稳定 真正值得共享的状态通常只有登录、权限、主题、语言、租户和少量平台配置。表格筛选、弹窗开关、详情页临时数据应留在子应用内部。共享越多,子应用越难独立运行,灰度发布和回滚也越麻烦。 通信契约最好放在单独包里维护,至少包含事件名、payload 类型和服务方法签名。这样主应用升级时,旧子应用能通过 TypeScript 或测试提前发现不兼容,而不是上线后才暴露。对于跨团队协作,契约比口头约定更可靠。如果担心类型包升级影响发布,可以让契约保持向后兼容:新增字段可以,删除字段要经过灰度期。事件名也不要随业务文案变化而变化,最好用稳定的领域语义。 ## 追问 ### props、事件总线和共享服务怎么取舍? props 用来给子应用启动参数,事件总线用来通知变化,共享服务用来读取长期状态。判断标准是数据是否需要被晚挂载应用读取;如果需要,就不能只靠事件。实践里常见组合是 props 传服务入口,事件做变更通知,服务保存当前值。 ### 子应用之间能直接通信吗? 技术上可以,但不建议作为主链路。A 应用直接依赖 B 应用的事件名,会让两个团队发布节奏绑在一起。更好的边界是由主应用定义平台事件或领域服务,子应用只依赖稳定契约。 ### token 应该作为 props 直接传吗? 短期可以,但长期更推荐传 `getToken` 函数或认证服务。直接传字符串容易在刷新、退出登录、切换账号后变成旧值。踩坑最多的是子应用自己缓存 token,主应用已经退出,它还在继续发请求。 ### 如何避免通信导致内存泄漏? 订阅事件、WebSocket、轮询都必须在 `unmount` 或组件卸载钩子里释放。不要用匿名函数订阅后无法 off,也不要在 render 中重复订阅。排查重复触发时,先看监听器数量,再看接口层。
前端5月31日 17:12
Garfish 样式隔离怎么做?Shadow DOM 和 scoped CSS 如何取舍?Garfish 的样式隔离不是把 CSS 变成“绝对安全”,而是给每个子应用划出清晰作用域,减少全局 reset、组件库类名、运行时 style 标签互相覆盖。实际落地时,最常见的是 scoped CSS、Shadow DOM 和动态样式清理三类方案。它们解决的问题不同,取舍也不同:scoped CSS 兼容性好,Shadow DOM 隔离更强,动态清理负责避免卸载后的残留污染。 ## scoped CSS 适合多数业务系统 scoped CSS 通常通过构建工具给选择器加容器前缀,例如把 `.button` 变成 `[data-garfish-app="order"] .button`。它不改变 DOM 结构和事件模型,对 Ant Design、Element Plus 这类组件库比较友好,所以中后台系统优先选它。边界是它只能处理被构建工具改写过的样式,管不住第三方库运行时直接插到 `document.head` 的全局样式。 ```js module.exports = { plugins: { 'postcss-prefix-selector': { prefix: '[data-garfish-app="order"]', transform(prefix, selector) { if (/^(html|body)/.test(selector)) return selector return `${prefix} ${selector}` } } } } ``` ## Shadow DOM 隔离更强,但更挑场景 Shadow DOM 使用浏览器原生边界,外部样式不容易影响子应用,子应用样式也不容易泄漏出去。它适合边界很清楚的独立模块,比如嵌入式配置台、低频运营后台或独立小工具。踩坑点是弹窗、Tooltip、全局 message、主题变量和 E2E 选择器都可能要额外适配;如果组件库大量依赖 `document.body` 挂载弹层,Shadow DOM 的维护成本会明显上升。 ```js const host = document.querySelector('#subapp') const root = host.attachShadow({ mode: 'open' }) root.appendChild(document.createElement('div')) ``` ## 卸载清理决定长期稳定性 很多线上样式问题不是首次加载发生,而是切换几次子应用后才出现。原因通常是旧应用的 `<style>`、`<link>`、弹层 DOM 没有移除。Garfish 可以管理部分资源生命周期,但子应用仍应在 `unmount` 中清理自己创建的节点和组件库全局实例。 ```js const links = [] export function mount() { const link = document.createElement('link') link.rel = 'stylesheet' link.href = '/order/index.css' document.head.appendChild(link) links.push(link) } export function unmount() { links.splice(0).forEach(node => node.remove()) } ``` ## 追问 ### scoped CSS 和 Shadow DOM 应该怎么选? 多数业务系统先选 scoped CSS,因为它改造成本低,和现有组件库、埋点、自动化测试的冲突少。Shadow DOM 更适合子应用边界稳定、弹层少、主题依赖少的场景。取舍点不是谁更先进,而是隔离强度和接入成本能否平衡。 ### 为什么弹窗样式经常隔离失败? 很多组件库默认把 Modal、Popover、Message 挂到 `document.body`,它已经脱离了子应用容器。scoped CSS 的前缀匹配不到它,Shadow DOM 内部样式也覆盖不到它。解决时要统一配置弹层容器,例如 `getPopupContainer`,不要只修某一个组件。 ### 主应用主题变量要不要给子应用用? 如果主题是平台能力,应该用 CSS 变量透传,例如 `--color-primary`、`--font-size-base`。但主应用不要直接覆盖子应用内部类名,否则会形成隐性耦合。边界是共享稳定 token,不共享具体 DOM 结构。 ### 样式隔离会影响性能吗? scoped CSS 的主要成本在构建期,运行时影响通常很小。Shadow DOM 也不是主要瓶颈,真正要注意的是重复加载组件库样式和卸载后残留节点。不要为了省一点 CSS 体积放弃隔离,否则线上排查污染更贵。
前端5月31日 17:12
Garfish 微前端加载慢该怎么优化?Garfish 性能优化要分两段看:子应用加载前,重点是入口、资源、缓存和预加载;子应用运行后,重点是卸载清理、渲染开销、共享依赖和监控定位。不要一上来就堆 preload,真正有效的做法是先量化首屏、子应用加载耗时、资源体积和错误率,再针对瓶颈处理。微前端的性能问题经常不是 Garfish 本身慢,而是每个子应用都带一份 React、组件库和图表库,最后门户像同时打开了几个完整系统。 ## 先把加载链路量出来 主应用应该记录从路由命中到子应用 mount 完成的耗时,并区分资源下载、脚本执行和渲染时间。没有数据时,优化很容易变成猜谜:有人说缓存,有人说分包,最后谁都证明不了收益。建议在 `beforeLoad`、`afterLoad`、`mount` 前后打点,并把应用名、版本、网络状态一起上报。边界是埋点不能阻塞加载,失败时要静默降级。 ```ts const perf = new Map<string, number>(); Garfish.run({ beforeLoad(app) { perf.set(app.name, performance.now()); }, afterMount(app) { const cost = performance.now() - (perf.get(app.name) || performance.now()); report('garfish_subapp_mount_cost', { app: app.name, cost }); }, }); ``` ## 资源体积和缓存怎么控? 子应用要做路由级分包,首屏只加载必要 chunk,大型图表、编辑器、地图组件延后加载。静态资源使用带 hash 的文件名,HTML 或 manifest 保持短缓存,JS/CSS 走长缓存,这样既能更新入口,又能复用稳定资源。取舍是缓存越激进,发布回滚越依赖版本管理;如果没有版本化入口,用户可能拿到新 HTML 加旧 JS。常见坑是所有子应用都打包同一套依赖,导致首屏下载重复,应该通过 externals、共享依赖或构建约束处理。 ```js // nginx 示例:入口短缓存,hash 资源长缓存 location /app/index.html { add_header Cache-Control "no-cache"; } location ~* \.(js|css)$ { add_header Cache-Control "public, max-age=31536000, immutable"; } ``` ## 预加载不是越多越好 Garfish 可以在空闲时间预加载高概率访问的子应用,但预加载会占网络、CPU 和内存。更稳的策略是只预加载当前用户角色最可能进入的 1-2 个应用,并在弱网、低端设备或首屏未稳定时跳过。边界是移动端和海外网络下,盲目预加载可能让当前页面更慢。踩坑最多的是把所有子应用启动时一起 preload,看似后续切换快了,首屏却被拖垮。 ## 运行时性能别忽略卸载 微前端页面切换频繁,如果子应用卸载不彻底,内存会慢慢涨,定时器和事件监听也会重复执行。React root、Vue app、全局事件、WebSocket、轮询、AbortController 都要在 `unmount` 里处理。取舍是生命周期代码会稍微啰嗦,但比线上定位“偶发卡顿”省事。边界是有些全局资源本来就该复用,比如登录态和主题,不要为了清理把共享能力也销毁掉。 ## 追问 ### Garfish 加载慢时先查什么? 先查子应用入口是否慢、资源是否过大、是否重复下载公共依赖,再看脚本执行和渲染耗时。不要一开始就怀疑框架,因为很多慢是构建和资源策略造成的。取舍是网络层优化见效快,但如果 JS 执行太重,缓存再好也解决不了卡顿。边界是本地开发环境的耗时不能代表生产,需要看真实用户监控。 ### 预加载应该按什么规则开启? 按访问概率、用户角色和设备条件来开。比如用户进入工作台后,大概率访问报表,就可以在浏览器空闲时预加载报表子应用。取舍是提前消耗资源换切换速度,适合高频路径,不适合所有路径。踩坑是忽略弱网和低端设备,导致当前页面交互被预加载拖慢。 ### 共享依赖一定能提升性能吗? 不一定。共享 React、Vue、组件库可以减少重复下载,但会增加版本协调成本。取舍是稳定基础依赖适合共享,业务库或变化频繁的包不一定适合。边界是多个子应用依赖不同大版本时,强行共享可能引入兼容问题,比重复下载更危险。 ### 如何避免切换页面后越来越卡? 重点检查 `unmount` 是否清理事件、定时器、订阅、WebSocket 和未完成请求。可以用 Performance 和 Memory 面板反复切换路由,看 DOM 节点和监听器是否持续增长。取舍是清理逻辑需要统一封装,否则每个子应用各写各的容易漏。常见坑是只卸载组件树,忘了组件外创建的全局副作用。 ### 性能预算怎么定才合理? 不要拍脑袋定一个绝对值,要按业务场景、用户网络和设备分层。比如后台门户可以容忍首次进入稍慢,但应用内切换应该稳定;高频运营页面则首屏资源要更克制。取舍是预算太严会拖慢开发,太松又没有约束力。建议把入口 HTML、首屏 JS、子应用 mount 耗时和错误率纳入流水线或发布看板。
前端5月31日 17:12
Garfish、qiankun 和 single-spa 该怎么选?Garfish、qiankun 和 single-spa 都能做微前端,但它们解决问题的层级不同。single-spa 更像底座,只管应用注册、激活和生命周期;qiankun 在它之上补齐沙箱、资源加载和工程实践;Garfish 也提供开箱能力,同时更强调运行时加载、插件化和相对轻量的接入体验。选型时别只看“谁更先进”,要看团队是否需要高度定制、是否已有历史应用、是否能承担框架生态和运维成本。 ## 三者核心差异是什么? 如果团队刚开始做微前端,single-spa 的自由度最高,但很多事情要自己补,比如资源加载、样式隔离、全局变量污染处理和错误兜底。qiankun 的优势是成熟,资料多,历史项目验证多,适合需要稳定社区经验的团队。Garfish 的优势在于配置更直接,插件机制和应用调度能力比较顺手,适合希望快速把多个独立应用纳入同一门户的场景。取舍也很明显:生态越成熟,约束和历史包袱越多;框架越轻,团队自己补规范的责任越大。 | 维度 | Garfish | qiankun | single-spa | |---|---|---|---| | 定位 | 开箱式微前端框架 | 成熟微前端方案 | 生命周期底座 | | 接入成本 | 中低 | 中 | 高 | | 自定义空间 | 较高 | 中 | 最高 | | 沙箱与隔离 | 内置能力较完整 | 方案成熟 | 需自行实现 | | 社区资料 | 中等 | 较多 | 国际化资料多 | ## Garfish 的优势在哪里? Garfish 对主应用注册、子应用加载、生命周期和沙箱做了较完整封装,配置量比裸用 single-spa 少。对于已有多个 React、Vue 或普通 SPA 的团队,它能让子应用在较少改造下接入。另一个优势是运行时灵活性,入口可以按环境、租户、版本动态切换,这对灰度发布有帮助。边界是框架不能替你解决组织问题,如果应用间契约、样式规范和发布流程没定好,换任何框架都会乱。 ```ts Garfish.run({ apps: [{ name: 'crm', entry: window.__entries__.crm, activeWhen: '/crm', props: { tenantId: getTenantId() }, }], plugins: [monitorPlugin(), authFallbackPlugin()], }); ``` ## 什么时候不适合选 Garfish? 如果团队已经深度使用 qiankun,且现有沙箱、构建、监控和发布链路都稳定,迁移到 Garfish 未必划算。迁移收益必须覆盖测试、适配和培训成本。另一个边界是如果你只是想共享几个组件或工具函数,Module Federation 或普通 npm 包可能更合适,没必要上完整微前端。常见踩坑是把微前端当成性能优化手段,结果增加了运行时加载和治理成本。 ## 追问 ### Garfish 和 qiankun 最大的选择差异是什么? 差异不只是 API,而是生态成熟度和团队治理方式。qiankun 的资料、案例和踩坑经验更多,Garfish 的接入体验和扩展方式更轻。取舍是前者更稳妥,后者在新项目里更容易按自己的平台规则搭起来。边界是如果公司已有 qiankun 基建,除非有明确痛点,否则不要为了换框架而换框架。 ### single-spa 为什么还值得了解? single-spa 是很多微前端方案背后的生命周期模型,理解它能帮助你判断框架到底替你做了什么。直接使用它适合架构团队很强、需要完全定制加载和隔离策略的项目。取舍是自由度换来了工程成本,业务团队可能会被大量底层细节拖住。踩坑点是只接了生命周期,却忘了资源隔离和错误恢复,线上问题会很难定位。 ### Module Federation 能替代 Garfish 吗? 它们关注点不同。Module Federation 更适合模块级共享,比如多个应用共用组件、工具库或业务模块;Garfish 更适合应用级集成,比如把多个独立 SPA 放到同一个门户下。取舍是 Federation 对构建体系要求更高,Garfish 的运行时边界更清楚。边界是两者可以组合使用,但要管好依赖版本,否则共享依赖会变成隐形耦合。 ### 老项目迁移到 Garfish 应该怎么做? 不要一次性全迁,先挑一个边界清晰、依赖少、访问量可控的子应用试点。迁移时先保证独立运行,再接入主应用,最后补监控和灰度。取舍是分阶段会拉长周期,但能把风险压住。常见坑是只改入口和生命周期,不处理全局样式、定时器和跨应用跳转,结果测试环境正常、生产环境互相污染。 ### 面试或评审里怎么回答选型问题? 先讲业务约束,再讲框架能力,不要直接说某个框架更好。比如团队是否多技术栈、是否要独立发布、是否已有基建、是否要求强隔离。边界是微前端不是默认答案,小团队单体 SPA 可能更简单。能说清这些取舍,比背一张对比表更有说服力。
前端5月31日 17:12
Garfish 实际项目怎么落地才不容易失控?Garfish 落地微前端,关键不是把子应用跑起来,而是先把主应用边界、子应用生命周期、发布规则和故障兜底定清楚。主应用只做平台能力:注册应用、路由分发、登录态、主题、全局错误处理和监控埋点;业务逻辑尽量留在子应用里。这样做的取舍是,前期规范会多一点,但后续团队扩张、独立发布、灰度回滚都会轻很多。 ## 主应用应该管什么? 主应用最好保持“薄壳”角色,不要把订单、用户、报表这类业务判断塞进去。它可以统一提供 `userInfo`、权限、语言、主题、接口前缀和埋点方法,再通过 `props` 传给子应用。边界要写清:主应用负责“能不能进入”和“挂在哪里”,子应用负责“进去以后怎么展示”。一个常见坑是主应用为了方便到处暴露全局对象,最后每个子应用都依赖它,独立运行和本地调试会很痛苦。 ```ts import Garfish from 'garfish'; Garfish.run({ basename: '/', apps: [{ name: 'user-center', entry: process.env.USER_CENTER_ENTRY!, activeWhen: '/user', props: { getUser: () => window.__portal_user__, track: (event: string, data?: unknown) => sendLog(event, data), }, }], sandbox: { snapshot: true, fixBaseUrl: true }, }); ``` ## 子应用生命周期怎么写才稳? 子应用要保证 `mount` 可重复执行,`unmount` 能把 DOM、事件、定时器、请求订阅清干净。React、Vue、Svelte 都可以接入,但不要假设自己永远运行在完整页面里。实际项目里最容易踩坑的是全局事件和轮询任务:页面切走后还在请求接口,用户再回来就出现重复订阅。建议把清理函数集中放到一个数组里,卸载时统一执行,避免遗漏。 ```ts const disposers: Array<() => void> = []; export async function mount({ dom, props }) { const onResize = () => props.track('resize'); window.addEventListener('resize', onResize); disposers.push(() => window.removeEventListener('resize', onResize)); root = createRoot(dom.querySelector('#root')!); root.render(<App user={props.getUser()} />); } export async function unmount() { root?.unmount(); disposers.splice(0).forEach(fn => fn()); } ``` ## 发布和回滚要提前约定 每个子应用独立发布,但入口地址必须可控,最好通过环境配置或版本化 manifest 管理。直接把生产 entry 写死在主应用里,看起来简单,回滚时却要重新发主应用。更稳的方式是主应用读取配置中心,子应用发布后只更新版本指针。边界是配置中心也要有缓存和降级,否则它挂了会导致所有子应用加载失败。发布前还要约定资产命名、CDN 缓存、灰度比例和回滚触发条件。比如新版本错误率超过阈值时,先切回旧 entry,再排查子应用代码,而不是让主应用临时加兼容逻辑。 ## 追问 ### 主应用为什么不能承载太多业务逻辑? 因为微前端的收益来自团队和发布边界,而不是页面拆分本身。主应用一旦包含大量业务判断,子应用每次变更都要等主应用配合,独立发布就失效了。取舍是主应用要多做一些平台抽象,但这比后期拆耦成本低得多。踩坑点是“临时加一个判断”很容易变成长期依赖,所以要把例外也纳入评审。 ### 子应用是否必须能独立运行? 最好能独立运行,至少本地开发和基础页面渲染不应依赖主应用。独立运行会增加一层 mock props 或本地启动配置,但可以显著降低联调成本。边界是登录、权限、埋点这类平台能力可以用模拟实现,不必在本地完整复刻。常见坑是子应用直接读取主应用全局变量,离开主应用就白屏。 ### 团队之间怎么约定通信方式? 优先用 props 传稳定能力,用事件总线传一次性通知,少用共享可变状态。共享状态看起来方便,但边界不清时会让数据来源变得混乱。取舍是事件会让调用链不如直接函数清晰,所以事件名、payload 类型和取消订阅规则要写进规范。踩坑最多的是忘记 off,导致重复弹窗、重复请求或内存泄漏。 ### 样式隔离应该一开始就上 Shadow DOM 吗? 不一定。后台类系统用 CSS Modules、BEM 前缀或 scoped CSS 往往够用,Shadow DOM 更适合冲突严重、第三方样式复杂的场景。它的边界是弹窗、下拉、主题变量和全局字体可能需要额外适配。取舍是隔离越强,跨应用统一视觉和调试成本也越高。 ### 接入 Garfish 后如何做质量验收? 验收不只看页面能否打开,还要测路由切换、重复进入、卸载清理、异常加载和灰度回滚。建议 E2E 覆盖跨应用主流程,单测覆盖生命周期函数。边界是不要把所有子应用组合都测一遍,那会拖慢流水线;更实际的是主链路必测、风险应用加测。常见坑是只测首次加载,忽略第二次进入才暴露的重复订阅问题。
前端5月27日 20:05
Garfish 支持哪些子应用加载方式,如何根据场景选择合适的加载策略?Garfish 子应用的加载方式主要分为**路由驱动自动加载**和**手动控制加载**两种模式,配合内置的预加载与缓存机制,可以覆盖从核心业务到低频功能的全场景需求。 ## 一、两种核心加载模式 ### 1. 路由驱动自动加载 通过 `Garfish.run()` 注册子应用并配置 `activeWhen` 路由匹配规则,Garfish 会自动劫持路由,当浏览器 URL 命中时加载并挂载对应子应用。这是最常用的方式,适合子应用与路由强关联的场景。 ```typescript import Garfish from 'garfish'; Garfish.run({ basename: '/', domGetter: '#subApp', apps: [ { name: 'react-app', activeWhen: '/react', entry: 'http://localhost:3000', }, { name: 'vue-app', activeWhen: '/vue', entry: 'http://localhost:8080/index.js', // 也支持 JS 入口 }, ], }); ``` **关键配置项:** | 参数 | 说明 | |------|------| | `activeWhen` | 路由匹配条件,支持字符串、正则或函数 | | `entry` | 子应用入口地址,支持 HTML 入口和 JS 入口两种格式 | | `domGetter` | 子应用挂载的 DOM 容器 | | `basename` | 基础路径,实际传给子应用的 basename 为 `basename + activeWhen` | ### 2. 手动控制加载 通过 `Garfish.loadApp()` 手动加载子应用,灵活控制挂载、显示、隐藏的时机。适合子应用不依赖路由、需要动态挂载到任意容器的场景,比如弹窗内嵌子应用、Tab 切换复用同一子应用等。 ```typescript import Garfish from 'garfish'; // 手动加载子应用 const app = await Garfish.loadApp('vue-app', { domGetter: '#container', entry: 'http://localhost:3000', cache: true, }); // 首次渲染调用 mount,后续切换调用 show app.mounted ? app.show() : await app.mount(); // 隐藏子应用(保留实例,不销毁) await app.hide(); // 完全卸载子应用 await app.unmount(); ``` **`mount()` 与 `show()` 的区别:** `mount()` 是首次渲染,会执行子应用的生命周期;`show()` 是将已挂载的子应用重新显示,跳过生命周期执行,切换更轻量。路由插件内部的核心判断逻辑是:当 `cache` 为 `true` 且 `app.mounted` 为 `true` 时调用 `show()`,否则调用 `mount()`。 ## 二、预加载机制 Garfish 内置了智能预加载能力,在主应用空闲时提前拉取子应用资源,用户真正访问时无需等待网络请求。 ### 自动预加载 默认开启(`disablePreloadApp: false`),Garfish 会在用户端统计子应用的打开频率,打开次数越多的子应用预加载权重越高。在弱网环境和移动端会自动关闭预加载以节省流量。 ### 手动预加载 使用 `Garfish.preloadApp()` 主动触发指定子应用的资源预加载,适合在主应用 HTML 阶段就提前拉取首屏需要的核心子应用: ```typescript import Garfish from 'garfish'; // 先注册子应用 Garfish.registerApp({ name: 'react', entry: 'http://localhost:3000', }); // 预加载 react 子应用的入口资源和子资源 Garfish.preloadApp('react'); ``` 预加载的资源存储在独立内存中,真正加载子应用时不会再发起资源请求,直接复用已缓存的静态资源。 ### 关闭预加载 如果不需要预加载(如子应用体积大且访问频率低),可以在 `Garfish.run()` 中配置: ```typescript Garfish.run({ disablePreloadApp: true, // 关闭预加载 // ... }); ``` ## 三、缓存机制 Garfish 默认开启子应用缓存(`cache: true`),已加载的子应用实例不会在切换时销毁,而是保留在内存中。再次激活时调用 `show()` 而非 `mount()`,显著减少重复渲染开销。 可以进一步配置缓存策略: ```typescript const app = await Garfish.loadApp('vue-app', { cache: true, cacheOptions: { maxAge: 15 * 60 * 1000, // 缓存有效期 15 分钟 }, }); ``` 如果子应用存在内存泄漏问题或需要每次重新初始化,可以关闭缓存: ```typescript Garfish.run({ apps: [ { name: 'problematic-app', activeWhen: '/problem', entry: 'http://localhost:4000', cache: false, // 每次切换都销毁并重建 }, ], }); ``` ## 四、加载生命周期钩子 Garfish 提供了 `beforeLoad` 和 `afterLoad` 钩子,可以在子应用加载前后执行自定义逻辑,比如埋点统计、权限校验、加载态展示等: ```typescript Garfish.run({ beforeLoad(appInfo) { console.log('子应用开始加载:', appInfo.name); showLoadingSpinner(); }, afterLoad(appInfo) { console.log('子应用加载完成:', appInfo.name); hideLoadingSpinner(); }, }); ``` ## 五、如何根据场景选择加载策略 ### 场景一:常规路由级子应用 **选择:路由驱动自动加载 + 默认预加载 + 默认缓存** 这是最典型的微前端接入方式。子应用与路由一一对应,Garfish 自动处理加载、挂载、卸载的全流程: ```typescript Garfish.run({ basename: '/', domGetter: '#subApp', apps: [ { name: 'crm', activeWhen: '/crm', entry: 'http://localhost:3001' }, { name: 'oa', activeWhen: '/oa', entry: 'http://localhost:3002' }, ], }); ``` ### 场景二:首屏核心子应用需要极速加载 **选择:路由驱动自动加载 + 手动 `preloadApp` 提前拉取** 在主应用 HTML 阶段就预加载首屏核心子应用,确保用户进入时资源已经就绪: ```typescript // 在主应用最早执行的脚本中预加载 Garfish.registerApp({ name: 'home', entry: 'http://localhost:3001' }); Garfish.preloadApp('home'); Garfish.run({ domGetter: '#subApp', apps: [{ name: 'home', activeWhen: '/home', entry: 'http://localhost:3001' }], }); ``` ### 场景三:子应用需要挂载到非路由驱动的容器 **选择:手动 `loadApp` 加载** 比如侧边栏中嵌入的子应用、弹窗中加载的子应用,路由不变但需要动态挂载: ```typescript const sidebarApp = await Garfish.loadApp('sidebar-widget', { domGetter: '#sidebar', entry: 'http://localhost:3003', cache: true, }); await sidebarApp.mount(); ``` ### 场景四:低频大型子应用 **选择:路由驱动自动加载 + 关闭预加载 + 关闭缓存** 低频使用的子应用不需要预加载占用带宽,也不需要缓存占用内存: ```typescript Garfish.run({ disablePreloadApp: true, // 如需全部关闭 apps: [ { name: 'admin-panel', activeWhen: '/admin', entry: 'http://localhost:3004', cache: false, }, ], }); ``` ### 场景五:多实例同类型子应用 **选择:手动 `loadApp` 加载 + 不同容器** 需要在同一页面同时展示多个同类型子应用实例时,路由驱动无法满足,必须手动控制: ```typescript const app1 = await Garfish.loadApp('chart', { domGetter: '#chart-container-1', entry: 'http://localhost:3005', }); const app2 = await Garfish.loadApp('chart', { domGetter: '#chart-container-2', entry: 'http://localhost:3005', }); await Promise.all([app1.mount(), app2.mount()]); ``` ## 六、常见问题 **Q: `loadApp` 提示 "Invalid domGetter" 怎么办?** 确保挂载节点已经存在于页面 DOM 中。在 Garfish 开始渲染时如果查询不到挂载节点,就会抛出此错误。可以在组件的 `mounted` 生命周期或 `useEffect` 回调中调用 `loadApp`。 **Q: 子应用切换后状态丢失怎么办?** 默认情况下 `cache: true`,子应用切换时调用 `hide()` 而非 `unmount()`,状态会保留。如果状态丢失,检查是否误将 `cache` 设为 `false`,或子应用内部在 `unmount` 生命周期中手动清理了状态。 **Q: 预加载在移动端不生效?** Garfish 在弱网环境和移动端会自动关闭预加载,这是预期行为。如需强制开启,需修改 Garfish 源码中的网络检测逻辑,但不建议这样做。
前端5月27日 20:04
Garfish 的生命周期钩子有哪些?provider 函数和 show/hide 怎么用?Garfish 子应用的生命周期围绕 provider 函数展开,核心钩子按执行顺序为:bootstrap → mount → update(可选) → unmount,另有 show/hide 用于缓存场景。与 qiankun 的最大区别在于:Garfish 子应用必须导出 provider 函数而非直接导出生命周期函数。 ## 核心钩子及执行顺序 | 钩子 | 触发时机 | 调用次数 | 作用 | |------|----------|----------|------| | bootstrap | 子应用首次加载 | 仅 1 次 | 初始化配置、注入依赖 | | mount | 子应用渲染到容器 | 每次激活 | 挂载 DOM、启动渲染 | | unmount | 子应用从页面移除 | 每次离开 | 清理 DOM、事件、定时器 | | update | 父应用传递 props 变更(可选) | 按需 | 响应属性更新 | | show | 缓存子应用重新显示(可选) | 按需 | 恢复运行状态 | | hide | 缓存子应用被隐藏(可选) | 按需 | 暂停但不销毁 | 执行顺序: - **首次加载**:provider() → bootstrap → mount - **路由切换离开**:unmount(非缓存)或 hide(缓存模式) - **路由切换回来**:mount(非缓存)或 show(缓存模式,跳过 bootstrap) - **属性变更**:update - **彻底销毁**:unmount ## provider 函数:Garfish 生命周期的入口 Garfish 子应用必须导出一个 provider 函数,它的返回值才是真正的生命周期对象: ```javascript // 子应用入口 export function provider({ basename, dom, ...props }) { return { bootstrap() { console.log('[sub-app] bootstrap, basename:', basename); return Promise.resolve(); }, mount({ basename, dom }) { const container = dom.querySelector('#app'); ReactDOM.render(<App basename={basename} />, container); return Promise.resolve(); }, unmount({ dom }) { const container = dom.querySelector('#app'); ReactDOM.unmountComponentAtNode(container); return Promise.resolve(); }, update({ ...newProps }) { // 响应主应用传入的属性变更 return Promise.resolve(); }, }; } ``` 关键点:provider 接收主应用传入的 props(如 basename、dom 容器),在 mount/unmount 中通过参数获取运行时上下文,而非闭包变量。 ## show/hide:缓存模式下的生命周期 当主应用配置 `sandbox.cache = true` 时,子应用不会被销毁,而是通过 show/hide 控制显隐: ```javascript export function provider() { let app = null; return { // ...bootstrap, mount, unmount 省略 show() { // 恢复定时器、重新订阅事件、恢复动画 console.log('[sub-app] show: 恢复运行状态'); return Promise.resolve(); }, hide() { // 暂停定时器、取消事件订阅、暂停动画(不销毁 DOM) console.log('[sub-app] hide: 暂停运行状态'); return Promise.resolve(); }, }; } ``` 缓存模式下 show/hide 与 mount/unmount 互斥:激活走 show(不走 mount),离开走 hide(不走 unmount)。 ## 完整生命周期流程图 ``` 首次加载: 下载子应用 JS → 执行沙箱隔离 → 调用 provider() → bootstrap() → mount() 路由切换(非缓存): 旧子应用 unmount() → 新子应用 mount() 路由切换(缓存模式): 旧子应用 hide() → 新子应用 mount() 或 show() 属性更新: 主应用 setProps() → 子应用 update() 彻底销毁: unmount() → 清理沙箱 → 释放内存 ``` ## 插件级生命周期钩子 除子应用生命周期外,Garfish 还提供主应用侧的插件钩子,用于拦截加载过程: ```javascript Garfish.run({ plugins: [ () => ({ beforeLoad(appInfo) { console.log('即将加载:', appInfo.name); return appInfo; }, afterLoad(appInfo) { console.log('加载完成:', appInfo.name); }, beforeMount(appInfo) { console.log('即将挂载:', appInfo.name); }, afterMount(appInfo) { console.log('挂载完成:', appInfo.name); }, beforeUnmount(appInfo) { console.log('即将卸载:', appInfo.name); }, afterUnmount(appInfo) { console.log('卸载完成:', appInfo.name); }, }), ], }); ``` 这些钩子在主应用侧执行,可用于日志采集、性能监控、权限校验等横切逻辑。 ## 与 qiankun 生命周期的对比 | 对比项 | Garfish | qiankun | |--------|---------|----------| | 导出方式 | provider 函数返回生命周期对象 | 直接导出 bootstrap/mount/unmount | | 缓存钩子 | show/hide | 无(需自行实现) | | 插件钩子 | beforeLoad/afterLoad 等 6 个 | 框架级 beforeLoad/afterMount 等 | | 参数传递 | provider(props) + mount(props) | mount(props) | | 沙箱集成 | 生命周期与沙箱强绑定 | 沙箱独立于生命周期 | ## 常见踩坑与解决方案 **1. mount 中拿不到容器 DOM** mount 回调中的 dom 参数是 Garfish 创建的容器,需要在 dom 内查找挂载点: ```javascript mount({ dom }) { // 错误:document.getElementById('app') // 正确:在 Garfish 提供的 dom 内查找 const container = dom.querySelector('#sub-app-root'); ReactDOM.render(<App />, container); } ``` **2. unmount 后仍然有内存泄漏** 定时器和全局事件监听不会随 DOM 移除而自动清理: ```javascript let timer = null; let resizeHandler = null; mount({ dom }) { timer = setInterval(sendHeartbeat, 5000); resizeHandler = () => recalculateLayout(); window.addEventListener('resize', resizeHandler); // ... }, unmount() { clearInterval(timer); window.removeEventListener('resize', resizeHandler); timer = null; resizeHandler = null; } ``` **3. 缓存模式下 show/hide 未实现导致状态异常** 如果开启缓存但只实现了 mount/unmount,子应用在 hide 后定时器仍在运行、事件仍在监听,切回时可能出现重复绑定。必须配套实现 show/hide。 ## 追问 **Q: Garfish 为什么选择 provider 函数模式,而不是像 qiankun 那样直接导出生命周期?** provider 模式有两个优势:一是每次加载都可以通过 provider 重新创建生命周期实例,避免单例模式下多次挂载的状态污染;二是 provider 在执行时可以拿到主应用传入的 props(如 basename、dom),在闭包中天然拥有运行时上下文,不需要在 mount 中额外合并参数。 **Q: 如果子应用不实现 unmount 会怎样?** 子应用的 DOM 不会从容器中移除,事件监听器和定时器继续运行,路由切换后旧应用的副作用仍在执行,会导致内存泄漏、事件重复触发、UI 叠加渲染等问题。Garfish 不会强制校验 unmount 的实现,这是开发者的责任。 **Q: bootstrap 和 mount 的区别是什么,能不能把初始化逻辑都放在 mount 里?** bootstrap 只执行一次,mount 每次激活都会执行。如果把初始化逻辑(如加载配置、注册全局插件)放在 mount 里,每次路由切回都会重复执行,既浪费性能又可能导致重复注册。正确的做法是:一次性初始化放 bootstrap,每次挂载都需要的渲染逻辑放 mount。
前端5月27日 20:03
Garfish 的错误处理和降级机制是如何工作的?Garfish 通过沙箱自动降级、生命周期错误捕获、资源加载容错三层机制保证微前端稳定性。 沙箱降级是核心。默认启用基于 Proxy 的 VM 沙箱,代理 window 对象隔离子应用全局变量,支持多实例并行。浏览器不支持 Proxy 时自动降级为快照沙箱——挂载前保存 window 全量快照,卸载后恢复原状,只支持单实例切换。降级过程对业务透明,框架内部自动判断。 生命周期层面,Garfish 在 beforeLoad、afterLoad、beforeMount、afterMount、beforeUnmount、afterUnmount 六个阶段提供钩子,任一阶段异常均触发全局 error 事件。主应用统一监听即可捕获所有子应用错误: ```javascript Garfish.router.on('error', (error) => { reportError(error); // 上报监控 showErrorPage(); // 降级 UI }); ``` 资源加载容错:Garfish 用 fetch 拉取子应用 JS/CSS,网络失败触发 afterLoad 错误回调。跨域动态脚本(JSONP 等)被转成 fetch 请求,后端未配 CORS 会报跨域错误。可用 `excludeAssetFilter` 放行特定脚本,但放行的资源会逃逸沙箱执行,副作用难以追踪。 ## 追问 ### Proxy 沙箱和快照沙箱的核心区别? Proxy 沙箱代理 window 读写,每个子应用有独立代理对象,支持多实例并行。快照沙箱在挂载时浅拷贝 window,卸载时还原,只能串行切换。Proxy 隔离更彻底但不兼容 IE;快照兼容性好但全局对象可能被意外修改。 ### 子应用崩溃会拖垮主应用吗? JS 错误被沙箱隔离,不会直接污染主应用状态。但 DOM 副作用可能逃逸——子应用在 document 上绑的事件监听器、插入的全局样式,卸载后不会自动清理。规范做法是在 unmount 钩子中手动移除副作用,或用 Garfish 的 DOM 沙箱自动收集和清理。 ### 沙箱里 sourcemap 行号为什么对不上? Garfish 通过 eval + sourceURL 执行子应用代码,sourceURL 改变了错误堆栈中的文件标识,导致行号偏移,sourcemap 还原指向错误位置。需要用 Garfish 提供的行号修正工具对齐偏移量。 ### excludeAssetFilter 放行的脚本出了问题怎么排查? 放行 = 脱离沙箱,脚本里的全局变量写入、事件绑定都不受管控。排查思路:在放行脚本的入口和出口打日志对比 window 差异;生产环境尽量不放行,让后端加 CORS 头保持资源在沙箱内加载。实际踩坑中,放行 JSONP 脚本导致全局变量污染另一个子应用的案例很常见。
前端5月27日 20:03
Garfish 的路由管理系统如何工作,如何实现主子应用的路由协同?Garfish 的路由管理系统是微前端架构中最关键的基础设施之一——主应用需要知道何时加载/卸载子应用,子应用需要知道自己的路由空间在哪,两者必须无缝协同才能实现"像单页应用一样"的用户体验。 本文将从路由劫持原理、basename 自动计算、路由分发机制、主子路由同步四个层面,拆解 Garfish 路由系统的完整工作流程。 ## 路由劫持:一切从拦截浏览器路由开始 Garfish 在执行 `Garfish.run()` 时,会立即对浏览器的路由行为进行劫持。具体做法是重写 `window.history.pushState` 和 `window.history.replaceState`,同时监听 `popstate` 和 `hashchange` 事件。 ```javascript // Garfish.run() 执行后,路由劫持自动生效 Garfish.run({ basename: '/', domGetter: '#subApp', apps: [ { name: 'react-app', activeWhen: '/react', entry: 'http://localhost:3000', }, { name: 'vue-app', activeWhen: '/vue', entry: 'http://localhost:8080/index.js', }, ], }); ``` 劫持的目的有两个: 1. **感知路由变化**:每次 URL 变化时,Garfish 都能第一时间捕获到新的路径。 2. **接管路由控制权**:根据新的路径判断应该激活哪个子应用、销毁哪个子应用,而不是让浏览器默认行为接管。 这意味着在 Garfish 运行之后,所有路由跳转都经过 Garfish 的路由管理层,主应用不再直接操控浏览器路由,而是通过 Garfish 间接操控。 ## 路由匹配与子应用分发 当路由劫持捕获到 URL 变化后,Garfish 进入路由匹配阶段。核心逻辑是遍历 `apps` 配置,用每个子应用的 `activeWhen` 规则与当前路径做匹配: ```javascript // activeWhen 支持字符串、正则、函数三种形式 { name: 'react-app', activeWhen: '/react', // 字符串前缀匹配 } { name: 'admin-app', activeWhen: /^\/admin/, // 正则匹配 } { name: 'special-app', activeWhen: (path) => path.startsWith('/special'), // 函数匹配 } ``` 匹配成功后,Garfish 执行以下流程: 1. **检查子应用状态**:如果子应用已加载且当前激活,则仅更新子应用路由;如果未加载,则触发子应用加载。 2. **加载子应用资源**:根据 `entry` 配置(HTML 入口或 JS 入口)请求子应用资源,创建沙箱环境,执行子应用代码。 3. **调用 `render` 生命周期**:将 `dom`、`basename` 等信息通过 provider 的 `render` 函数传递给子应用。 4. **卸载非活跃子应用**:对不再匹配的子应用调用 `destroy` 生命周期,清理 DOM 和事件监听。 **关键细节**:不要使用根路径 `/` 作为 `activeWhen`,否则该子应用在任何路径下都会被激活,导致其他子应用永远无法加载。 ## basename 自动计算:路由隔离的核心机制 basename 是 Garfish 实现路由隔离的关键。子应用的 basename 计算公式为: ``` 子应用 basename = 主应用 basename + activeWhen ``` 例如,主应用 `basename` 为 `/`,子应用 `activeWhen` 为 `/react`,则子应用收到的 `basename` 为 `/react`。如果主应用 `basename` 改为 `/portal`,子应用的 `basename` 自动变为 `/portal/react`。 ```javascript // 子应用 provider 配置 export const provider = () => ({ render({ dom, basename }) { // basename = 主应用 basename + activeWhen // 必须将 basename 设置为子应用路由的 base path ReactDOM.render( <BrowserRouter basename={basename}> <App /> </BrowserRouter>, dom ? dom.querySelector('#root') : document.querySelector('#root') ); }, destroy({ dom }) { ReactDOM.unmountComponentAtNode( dom ? dom.querySelector('#root') : document.querySelector('#root') ); }, }); ``` 如果子应用不使用 `basename`,会出现两个严重问题: - **路由冲突**:子应用的路由 `/home` 会和主应用的 `/home` 冲突。 - **路由丢失**:子应用内部跳转时,路径不会带上 `/react` 前缀,导致刷新页面后 Garfish 无法匹配到子应用。 ## 主子应用路由同步 Garfish 的路由同步要解决的核心问题是:子应用既需要能独立运行(开发阶段直接启动),又需要能嵌入主应用运行(生产环境)。Garfish 通过以下机制实现两种模式的平滑切换: ### 1. 路由跳转方式 ```javascript // 方式一:使用 Garfish.router(推荐) Garfish.router.push('/react/dashboard'); // 自动带上全局 basename,跳转到正确的完整路径 // 方式二:使用子应用框架路由 // 需要手动添加 basename 前缀 history.push(`${basename}/dashboard`); ``` `Garfish.router.push()` 会自动拼接全局 basename 作为路径前缀,确保跳转目标正确。而使用框架自带路由跳转时,必须手动添加 `basename`,否则路径会缺少前缀。 ### 2. autoRefreshApp 控制 ```javascript Garfish.run({ autoRefreshApp: true, // 默认 true // ... }); ``` - **`autoRefreshApp: true`**(默认):路由变化时自动刷新子应用视图,子应用内部路由跳转完全正常。 - **`autoRefreshApp: false`**:路由变化时不自动刷新子应用,子应用子路由只能通过 `Garfish.router` 跳转,但子应用一级路由仍可使用框架路由。 ### 3. 路由守卫 Garfish 提供 `beforeEach` 和 `afterEach` 钩子,用于在路由变化时执行拦截逻辑: ```javascript Garfish.router.beforeEach((to, from, next) => { // to: 目标路由信息 // from: 来源路由信息 // next: 继续路由跳转(必须调用) if (to.path.startsWith('/admin') && !isAuthenticated()) { // 重定向到登录页 next('/login'); } else { next(); } }); Garfish.router.afterEach((to, from) => { // 路由跳转完成后的逻辑 trackPageView(to.path); }); ``` ## 路由模式与限制 | 特性 | 支持情况 | 说明 | |------|----------|------| | 主应用 History 模式 | 完全支持 | 推荐使用 | | 主应用 Hash 模式 | 不支持 | Garfish 路由系统仅支持主应用 History 路由 | | 子应用 History 模式 | 支持 | 需正确配置 basename | | 子应用 Hash 模式 | 支持 | 子应用内部可使用 Hash 路由 | ## 常见问题与排错 ### 子应用路由跳转后页面白屏 **原因**:子应用未使用 `basename` 配置路由基础路径。跳转后的路径缺少 `activeWhen` 前缀,Garfish 匹配不到子应用,触发卸载。 **解决**:在子应用 `provider.render` 中将 `basename` 传递给路由组件。 ### 主应用 basename 变更后子应用路由异常 **原因**:部分子应用硬编码了路径前缀,而不是使用动态传入的 `basename`。 **解决**:确保所有子应用路由均基于 `provider.render` 接收的 `basename` 动态构建。 ### 子应用内部路由跳转不生效 **原因**:`autoRefreshApp` 设为 `false`,但子应用使用了框架路由跳转而非 `Garfish.router`。 **解决**:将 `autoRefreshApp` 设为 `true`,或改用 `Garfish.router.push()` 进行跳转。 ### 多个子应用同时激活 **原因**:某个子应用的 `activeWhen` 配置为 `/` 或过于宽泛的正则,导致路径匹配到多个子应用。 **解决**:确保每个子应用的 `activeWhen` 互斥,不要使用根路径作为激活条件。 ## 最佳实践总结 1. **主应用必须使用 History 路由模式**,Hash 模式不被 Garfish 路由系统支持。 2. **子应用必须使用 `basename`**,且从 `provider.render` 参数中动态获取,不要硬编码。 3. **跨应用跳转统一使用 `Garfish.router.push()`**,避免手动拼接路径出错。 4. **`activeWhen` 规则保持互斥**,禁止使用根路径 `/`,避免多个子应用同时匹配。 5. **`autoRefreshApp` 保持默认 `true`**,除非有明确的性能优化需求。 6. **路由守卫用于权限控制**,复杂的业务逻辑放在应用内部,守卫层只做拦截和重定向。 Garfish 的路由系统通过劫持、匹配、隔离、同步四层机制,解决了微前端架构中最棘手的路由协同问题。理解这套机制,才能在实际项目中避免路由冲突、白屏、状态丢失等常见坑。
前端5月27日 20:01
什么是 Garfish 微前端框架,它的核心特点和应用场景是什么?Garfish 是字节跳动开源的微前端框架,主要解决大型前端应用在跨团队协作、技术栈多样化和独立部署方面的痛点。与 qiankun 等方案相比,Garfish 在沙箱隔离和依赖共享上有独特设计,适合对隔离性和性能有更高要求的企业级场景。 ## Garfish 的核心架构 Garfish 的整体架构由以下核心模块组成: - **Loader(加载器)**:负责子应用资源的获取和解析,支持异步加载和预加载策略 - **Sandbox(沙箱)**:隔离子应用的 JavaScript 执行环境,防止全局变量污染 - **Router(路由)**:管理子应用的路由注册、匹配和切换 - **Store(状态管理)**:提供跨应用的通信机制 下面通过一个最小接入示例说明 Garfish 的基本用法: ```js import Garfish from 'garfish'; Garfish.run({ basename: '/', domGetter: '#sub-app', apps: [ { name: 'vue-app', entry: 'http://localhost:8080', activeWhen: '/vue', }, { name: 'react-app', entry: 'http://localhost:3000', activeWhen: '/react', }, ], }); ``` 主应用只需配置子应用的名称、入口地址和激活路由,Garfish 会自动处理加载、挂载和卸载。 ## 六大核心特点详解 ### 1. 沙箱隔离 Garfish 提供两种沙箱模式: - **快照沙箱(Snapshot Sandbox)**:在子应用挂载前快照 window 对象,卸载后恢复。适用于不支持 Proxy 的浏览器,但无法处理动态添加的全局变量。 - **VM 沙箱(Proxy Sandbox)**:基于 ES6 Proxy 实现,为每个子应用创建一个代理 window 对象,真正实现了全局变量的隔离。这是 Garfish 推荐的方式。 ```js // VM 沙箱原理示意 const proxyWindow = new Proxy(window, { get(target, key) { // 优先从子应用自己的状态中读取 return ownState[key] ?? target[key]; }, set(target, key, value) { ownState[key] = value; // 写入子应用独立状态 return true; }, }); ``` VM 沙箱的优势在于多个子应用可以同时运行而互不干扰,这也是 Garfish 支持多实例的基础。 ### 2. 依赖共享 Garfish 支持子应用之间共享公共依赖(如 React、Vue、Lodash 等),避免重复加载同一库的多个副本: ```js Garfish.run({ apps: [ { name: 'app1', entry: 'http://localhost:8081', props: { react: require('react'), // 共享 React 实例 }, }, ], }); ``` 依赖共享能显著降低整体包体积和加载时间,对于同时运行多个 React 子应用的场景尤为明显。 ### 3. 预加载策略 Garfish 内置智能预加载机制,会在浏览器空闲时提前获取子应用资源: - 自动记录用户访问习惯,为高频使用的子应用增加预加载权重 - 支持 `prefetch` 配置项,可自定义预加载行为 - 预加载的资源包括 HTML、JS、CSS 等子应用入口依赖 ```js Garfish.run({ prefetch: true, // 开启预加载 // 也可传入函数自定义预加载逻辑 // prefetch: (apps) => apps.filter(app => app.name === 'high-priority-app'), }); ``` ### 4. 框架无关 Garfish 对子应用的技术栈没有限制,React、Vue、Angular、Svelte 等均可接入。子应用只需导出固定的生命周期钩子: ```js // 子应用需要导出的生命周期 export function provider({ dom, basename, props }) { return { mount() { /* 挂载逻辑 */ }, unmount() { /* 卸载逻辑 */ }, update() { /* 可选,接收父应用传参更新 */ }, }; } ``` ### 5. 多实例支持 不同于部分微前端方案只允许单个子应用运行,Garfish 支持在同一页面中同时运行多个子应用实例: ```js // 同一页面同时挂载两个子应用 Garfish.run({ domGetter: '#container', apps: [ { name: 'sidebar', activeWhen: '/', entry: '...' }, { name: 'main-content', activeWhen: '/', entry: '...' }, ], }); ``` 这在后台管理系统等需要布局嵌套的场景中非常实用。 ### 6. 样式隔离 Garfish 提供多种样式隔离方案: - **CSS Scoped**:为子应用的样式自动添加作用域前缀 - **Shadow DOM**:利用浏览器原生的 Shadow DOM 实现完全隔离 - **CSS Modules**:配合构建工具使用,从源头避免样式冲突 ## Garfish vs Qiankun:如何选择? | 对比维度 | Garfish | Qiankun | |---------|---------|---------| | 出品方 | 字节跳动 | 蚂蚁集团 | | 沙箱方案 | 快照 + VM 双模式 | 快照 + Proxy 双模式 | | 依赖共享 | 原生支持 | 需额外配置 | | 多实例 | 原生支持 | 有限支持 | | 预加载 | 内置智能预加载 | 需手动配置 | | 社区规模 | ~2.9k Stars | ~16k Stars | | 文档完善度 | 中等 | 较完善 | | 适用场景 | 隔离性要求高、需多实例 | 通用场景、快速接入 | 选择建议: - **选 Garfish**:项目需要强隔离、多实例共存、依赖共享,或已在字节生态内 - **选 Qiankun**:追求社区支持、开箱即用,或团队微前端经验较少 ## 典型应用场景 ### 企业级后台管理系统 多个业务团队各自维护独立子应用(权限管理、数据分析、运营工具等),通过 Garfish 统一接入主框架,实现独立开发、独立部署。 ### 电商平台 活动页、商品详情、购物车等模块由不同团队负责,使用 Garfish 的预加载和依赖共享优化首屏性能。 ### 大型 SaaS 产品 不同功能模块(CRM、BI、工单系统)采用不同技术栈,Garfish 的框架无关特性允许各模块选择最合适的技术方案。 ## 接入注意事项 1. **子应用改造**:需要导出 `provider` 生命周期函数,并在打包配置中设置 `libraryTarget: 'umd'` 2. **跨域配置**:子应用需配置 CORS 头,允许主应用跨域获取资源 3. **环境变量**:子应用中访问 `window` 时需注意沙箱代理,避免直接操作导致泄漏 4. **公共路径**:子应用的静态资源路径需正确配置 `publicPath`,防止资源加载失败 ## 总结 Garfish 作为字节跳动出品的微前端框架,在沙箱隔离、依赖共享和多实例方面有独到优势。如果你的项目对应用隔离有较高要求,或者需要在一个页面中同时运行多个子应用,Garfish 是值得考虑的方案。但在社区生态和文档完善度上,它目前仍落后于 qiankun,团队在选型时需要权衡技术优势与社区支持之间的取舍。