面试题手册

梳理高频技术问题,帮助你按主题复习和查漏补缺。

前端阅读 05月27日 20:05

Garfish 支持哪些子应用加载方式,如何根据场景选择合适的加载策略?

Garfish 子应用的加载方式主要分为路由驱动自动加载和手动控制加载两种模式,配合内置的预加载与缓存机制,可以覆盖从核心业务到低频功能的全场景需求。一、两种核心加载模式1. 路由驱动自动加载通过 Garfish.run() 注册子应用并配置 activeWhen 路由匹配规则,Garfish 会自动劫持路由,当浏览器 URL 命中时加载并挂载对应子应用。这是最常用的方式,适合子应用与路由强关联的场景。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 切换复用同一子应用等。import Garfish from 'garfish';// 手动加载子应用const app = await Garfish.loadApp('vue-app', { domGetter: '#container', entry: 'http://localhost:3000', cache: true,});// 首次渲染调用 mount,后续切换调用 showapp.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 阶段就提前拉取首屏需要的核心子应用:import Garfish from 'garfish';// 先注册子应用Garfish.registerApp({ name: 'react', entry: 'http://localhost:3000',});// 预加载 react 子应用的入口资源和子资源Garfish.preloadApp('react');预加载的资源存储在独立内存中,真正加载子应用时不会再发起资源请求,直接复用已缓存的静态资源。关闭预加载如果不需要预加载(如子应用体积大且访问频率低),可以在 Garfish.run() 中配置:Garfish.run({ disablePreloadApp: true, // 关闭预加载 // ...});三、缓存机制Garfish 默认开启子应用缓存(cache: true),已加载的子应用实例不会在切换时销毁,而是保留在内存中。再次激活时调用 show() 而非 mount(),显著减少重复渲染开销。可以进一步配置缓存策略:const app = await Garfish.loadApp('vue-app', { cache: true, cacheOptions: { maxAge: 15 * 60 * 1000, // 缓存有效期 15 分钟 },});如果子应用存在内存泄漏问题或需要每次重新初始化,可以关闭缓存:Garfish.run({ apps: [ { name: 'problematic-app', activeWhen: '/problem', entry: 'http://localhost:4000', cache: false, // 每次切换都销毁并重建 }, ],});四、加载生命周期钩子Garfish 提供了 beforeLoad 和 afterLoad 钩子,可以在子应用加载前后执行自定义逻辑,比如埋点统计、权限校验、加载态展示等:Garfish.run({ beforeLoad(appInfo) { console.log('子应用开始加载:', appInfo.name); showLoadingSpinner(); }, afterLoad(appInfo) { console.log('子应用加载完成:', appInfo.name); hideLoadingSpinner(); },});五、如何根据场景选择加载策略场景一:常规路由级子应用选择:路由驱动自动加载 + 默认预加载 + 默认缓存这是最典型的微前端接入方式。子应用与路由一一对应,Garfish 自动处理加载、挂载、卸载的全流程:Garfish.run({ basename: '/', domGetter: '#subApp', apps: [ { name: 'crm', activeWhen: '/crm', entry: 'http://localhost:3001' }, { name: 'oa', activeWhen: '/oa', entry: 'http://localhost:3002' }, ],});场景二:首屏核心子应用需要极速加载选择:路由驱动自动加载 + 手动 preloadApp 提前拉取在主应用 HTML 阶段就预加载首屏核心子应用,确保用户进入时资源已经就绪:// 在主应用最早执行的脚本中预加载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 加载比如侧边栏中嵌入的子应用、弹窗中加载的子应用,路由不变但需要动态挂载:const sidebarApp = await Garfish.loadApp('sidebar-widget', { domGetter: '#sidebar', entry: 'http://localhost:3003', cache: true,});await sidebarApp.mount();场景四:低频大型子应用选择:路由驱动自动加载 + 关闭预加载 + 关闭缓存低频使用的子应用不需要预加载占用带宽,也不需要缓存占用内存:Garfish.run({ disablePreloadApp: true, // 如需全部关闭 apps: [ { name: 'admin-panel', activeWhen: '/admin', entry: 'http://localhost:3004', cache: false, }, ],});场景五:多实例同类型子应用选择:手动 loadApp 加载 + 不同容器需要在同一页面同时展示多个同类型子应用实例时,路由驱动无法满足,必须手动控制: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 源码中的网络检测逻辑,但不建议这样做。
前端阅读 05月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彻底销毁:unmountprovider 函数:Garfish 生命周期的入口Garfish 子应用必须导出一个 provider 函数,它的返回值才是真正的生命周期对象:// 子应用入口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 控制显隐: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 还提供主应用侧的插件钩子,用于拦截加载过程: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 中拿不到容器 DOMmount 回调中的 dom 参数是 Garfish 创建的容器,需要在 dom 内查找挂载点:mount({ dom }) { // 错误:document.getElementById('app') // 正确:在 Garfish 提供的 dom 内查找 const container = dom.querySelector('#sub-app-root'); ReactDOM.render(<App />, container);}2. unmount 后仍然有内存泄漏定时器和全局事件监听不会随 DOM 移除而自动清理: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。
前端阅读 05月27日 20:03

Garfish 的错误处理和降级机制是如何工作的?

Garfish 通过沙箱自动降级、生命周期错误捕获、资源加载容错三层机制保证微前端稳定性。沙箱降级是核心。默认启用基于 Proxy 的 VM 沙箱,代理 window 对象隔离子应用全局变量,支持多实例并行。浏览器不支持 Proxy 时自动降级为快照沙箱——挂载前保存 window 全量快照,卸载后恢复原状,只支持单实例切换。降级过程对业务透明,框架内部自动判断。生命周期层面,Garfish 在 beforeLoad、afterLoad、beforeMount、afterMount、beforeUnmount、afterUnmount 六个阶段提供钩子,任一阶段异常均触发全局 error 事件。主应用统一监听即可捕获所有子应用错误: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 脚本导致全局变量污染另一个子应用的案例很常见。
前端阅读 05月27日 20:03

Garfish 的路由管理系统如何工作,如何实现主子应用的路由协同?

Garfish 的路由管理系统是微前端架构中最关键的基础设施之一——主应用需要知道何时加载/卸载子应用,子应用需要知道自己的路由空间在哪,两者必须无缝协同才能实现"像单页应用一样"的用户体验。本文将从路由劫持原理、basename 自动计算、路由分发机制、主子路由同步四个层面,拆解 Garfish 路由系统的完整工作流程。路由劫持:一切从拦截浏览器路由开始Garfish 在执行 Garfish.run() 时,会立即对浏览器的路由行为进行劫持。具体做法是重写 window.history.pushState 和 window.history.replaceState,同时监听 popstate 和 hashchange 事件。// 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', }, ],});劫持的目的有两个:感知路由变化:每次 URL 变化时,Garfish 都能第一时间捕获到新的路径。接管路由控制权:根据新的路径判断应该激活哪个子应用、销毁哪个子应用,而不是让浏览器默认行为接管。这意味着在 Garfish 运行之后,所有路由跳转都经过 Garfish 的路由管理层,主应用不再直接操控浏览器路由,而是通过 Garfish 间接操控。路由匹配与子应用分发当路由劫持捕获到 URL 变化后,Garfish 进入路由匹配阶段。核心逻辑是遍历 apps 配置,用每个子应用的 activeWhen 规则与当前路径做匹配:// activeWhen 支持字符串、正则、函数三种形式{ name: 'react-app', activeWhen: '/react', // 字符串前缀匹配}{ name: 'admin-app', activeWhen: /^\/admin/, // 正则匹配}{ name: 'special-app', activeWhen: (path) => path.startsWith('/special'), // 函数匹配}匹配成功后,Garfish 执行以下流程:检查子应用状态:如果子应用已加载且当前激活,则仅更新子应用路由;如果未加载,则触发子应用加载。加载子应用资源:根据 entry 配置(HTML 入口或 JS 入口)请求子应用资源,创建沙箱环境,执行子应用代码。调用 render 生命周期:将 dom、basename 等信息通过 provider 的 render 函数传递给子应用。卸载非活跃子应用:对不再匹配的子应用调用 destroy 生命周期,清理 DOM 和事件监听。关键细节:不要使用根路径 / 作为 activeWhen,否则该子应用在任何路径下都会被激活,导致其他子应用永远无法加载。basename 自动计算:路由隔离的核心机制basename 是 Garfish 实现路由隔离的关键。子应用的 basename 计算公式为:子应用 basename = 主应用 basename + activeWhen例如,主应用 basename 为 /,子应用 activeWhen 为 /react,则子应用收到的 basename 为 /react。如果主应用 basename 改为 /portal,子应用的 basename 自动变为 /portal/react。// 子应用 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. 路由跳转方式// 方式一:使用 Garfish.router(推荐)Garfish.router.push('/react/dashboard');// 自动带上全局 basename,跳转到正确的完整路径// 方式二:使用子应用框架路由// 需要手动添加 basename 前缀history.push(`${basename}/dashboard`);Garfish.router.push() 会自动拼接全局 basename 作为路径前缀,确保跳转目标正确。而使用框架自带路由跳转时,必须手动添加 basename,否则路径会缺少前缀。2. autoRefreshApp 控制Garfish.run({ autoRefreshApp: true, // 默认 true // ...});autoRefreshApp: true(默认):路由变化时自动刷新子应用视图,子应用内部路由跳转完全正常。autoRefreshApp: false:路由变化时不自动刷新子应用,子应用子路由只能通过 Garfish.router 跳转,但子应用一级路由仍可使用框架路由。3. 路由守卫Garfish 提供 beforeEach 和 afterEach 钩子,用于在路由变化时执行拦截逻辑: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 互斥,不要使用根路径作为激活条件。最佳实践总结主应用必须使用 History 路由模式,Hash 模式不被 Garfish 路由系统支持。子应用必须使用 basename,且从 provider.render 参数中动态获取,不要硬编码。跨应用跳转统一使用 Garfish.router.push(),避免手动拼接路径出错。activeWhen 规则保持互斥,禁止使用根路径 /,避免多个子应用同时匹配。autoRefreshApp 保持默认 true,除非有明确的性能优化需求。路由守卫用于权限控制,复杂的业务逻辑放在应用内部,守卫层只做拦截和重定向。Garfish 的路由系统通过劫持、匹配、隔离、同步四层机制,解决了微前端架构中最棘手的路由协同问题。理解这套机制,才能在实际项目中避免路由冲突、白屏、状态丢失等常见坑。
前端阅读 05月27日 20:01

什么是 Garfish 微前端框架,它的核心特点和应用场景是什么?

Garfish 是字节跳动开源的微前端框架,主要解决大型前端应用在跨团队协作、技术栈多样化和独立部署方面的痛点。与 qiankun 等方案相比,Garfish 在沙箱隔离和依赖共享上有独特设计,适合对隔离性和性能有更高要求的企业级场景。Garfish 的核心架构Garfish 的整体架构由以下核心模块组成:Loader(加载器):负责子应用资源的获取和解析,支持异步加载和预加载策略Sandbox(沙箱):隔离子应用的 JavaScript 执行环境,防止全局变量污染Router(路由):管理子应用的路由注册、匹配和切换Store(状态管理):提供跨应用的通信机制下面通过一个最小接入示例说明 Garfish 的基本用法: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 推荐的方式。// 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 等),避免重复加载同一库的多个副本:Garfish.run({ apps: [ { name: 'app1', entry: 'http://localhost:8081', props: { react: require('react'), // 共享 React 实例 }, }, ],});依赖共享能显著降低整体包体积和加载时间,对于同时运行多个 React 子应用的场景尤为明显。3. 预加载策略Garfish 内置智能预加载机制,会在浏览器空闲时提前获取子应用资源:自动记录用户访问习惯,为高频使用的子应用增加预加载权重支持 prefetch 配置项,可自定义预加载行为预加载的资源包括 HTML、JS、CSS 等子应用入口依赖Garfish.run({ prefetch: true, // 开启预加载 // 也可传入函数自定义预加载逻辑 // prefetch: (apps) => apps.filter(app => app.name === 'high-priority-app'),});4. 框架无关Garfish 对子应用的技术栈没有限制,React、Vue、Angular、Svelte 等均可接入。子应用只需导出固定的生命周期钩子:// 子应用需要导出的生命周期export function provider({ dom, basename, props }) { return { mount() { /* 挂载逻辑 */ }, unmount() { /* 卸载逻辑 */ }, update() { /* 可选,接收父应用传参更新 */ }, };}5. 多实例支持不同于部分微前端方案只允许单个子应用运行,Garfish 支持在同一页面中同时运行多个子应用实例:// 同一页面同时挂载两个子应用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 的框架无关特性允许各模块选择最合适的技术方案。接入注意事项子应用改造:需要导出 provider 生命周期函数,并在打包配置中设置 libraryTarget: 'umd'跨域配置:子应用需配置 CORS 头,允许主应用跨域获取资源环境变量:子应用中访问 window 时需注意沙箱代理,避免直接操作导致泄漏公共路径:子应用的静态资源路径需正确配置 publicPath,防止资源加载失败总结Garfish 作为字节跳动出品的微前端框架,在沙箱隔离、依赖共享和多实例方面有独到优势。如果你的项目对应用隔离有较高要求,或者需要在一个页面中同时运行多个子应用,Garfish 是值得考虑的方案。但在社区生态和文档完善度上,它目前仍落后于 qiankun,团队在选型时需要权衡技术优势与社区支持之间的取舍。
前端阅读 02月21日 16:02

Garfish 如何实现主应用与子应用之间的通信和状态共享?

Garfish 提供了多种通信机制,支持主应用与子应用之间、以及子应用之间的数据共享和交互。通信方式1. Props 传递适用场景:主应用向子应用传递配置、用户信息等静态数据特点:单向数据流,简单直接示例:// 主应用配置{ name: 'sub-app', entry: '//localhost:3001', props: { userInfo: { name: 'John', role: 'admin' }, theme: 'dark', apiConfig: { baseUrl: '/api' } }}// 子应用接收export function mount(props) { const { userInfo, theme, apiConfig } = props; // 使用传递的数据}2. 事件总线适用场景:跨应用的事件通知和响应特点:解耦应用间依赖,支持一对多通信示例:// 发布事件Garfish.channel.emit('user-login', { userId: 123 });// 订阅事件Garfish.channel.on('user-login', (data) => { console.log('用户登录:', data.userId);});// 取消订阅Garfish.channel.off('user-login', handler);3. 共享状态适用场景:需要跨应用共享的业务状态特点:集中管理,响应式更新示例:// 定义共享状态Garfish.registerShared({ name: 'userStore', store: { state: { user: null }, mutations: { setUser(state, user) { state.user = user; } } }});// 子应用使用export function mount(props) { const userStore = props.shared.userStore; userStore.mutations.setUser({ name: 'John' });}4. 自定义通信协议适用场景:复杂的业务交互逻辑特点:灵活定制,满足特定需求示例:// 定义通信接口Garfish.defineInterface('auth', { login(credentials) { return fetch('/api/login', { method: 'POST', body: JSON.stringify(credentials) }); }, logout() { return fetch('/api/logout'); }});// 子应用调用export function mount(props) { props.auth.login({ username, password });}状态管理最佳实践1. 状态分层全局状态:用户信息、主题、权限等应用级状态:子应用内部状态组件级状态:组件内部状态2. 状态隔离避免直接访问其他应用的状态通过通信机制传递数据保持应用的独立性3. 状态同步使用事件机制同步状态变化实现状态变更通知避免状态不一致问题4. 状态持久化使用 localStorage 或 sessionStorage实现跨会话的状态保持考虑状态恢复机制通信安全考虑1. 数据验证验证接收到的数据格式和内容防止恶意数据注入实现数据校验机制2. 权限控制限制敏感数据的访问实现基于角色的权限控制审计通信日志3. 错误处理完善的错误捕获和处理提供友好的错误提示实现降级方案性能优化1. 减少通信频率合并多个通信请求使用批量更新实现防抖和节流2. 数据缓存缓存常用数据减少重复请求实现缓存失效策略3. 异步通信使用异步方式处理通信避免阻塞主线程优化用户体验通过合理使用通信机制,可以实现微前端应用间的高效协作和数据共享。
前端阅读 02月21日 16:02

Garfish 的性能优化策略有哪些,如何提升微前端应用的加载速度和运行效率?

Garfish 提供了多种性能优化策略,帮助开发者构建高性能的微前端应用。性能优化策略1. 代码分割与懒加载策略:将子应用代码分割成多个 chunk,按需加载实现方式:使用 Webpack 的动态 import配置路由级别的代码分割实现组件级别的懒加载优势:减少初始加载时间,提升首屏性能示例:// 动态导入子应用const SubApp = React.lazy(() => import('./SubApp'));// 路由级别代码分割const routes = [ { path: '/dashboard', component: React.lazy(() => import('./Dashboard')) }];2. 资源预加载策略:提前加载可能需要的资源实现方式:使用 <link rel="preload"> 预加载关键资源配置子应用预加载策略利用空闲时间预加载优势:减少用户等待时间,提升体验示例:// 预加载子应用Garfish.preloadApp('app1');// 预加载资源<link rel="preload" href="/app1.js" as="script">3. 缓存优化策略:利用浏览器缓存和 Service Worker 缓存资源实现方式:配置 HTTP 缓存头使用 Service Worker 缓存静态资源实现子应用缓存机制优势:减少网络请求,提升加载速度示例:// Service Worker 缓存self.addEventListener('install', (event) => { event.waitUntil( caches.open('garfish-cache').then((cache) => { return cache.addAll([ '/app1.js', '/app1.css' ]); }) );});4. 并行加载策略:同时加载多个子应用或资源实现方式:使用 Promise.all 并行加载配置并行加载策略优化资源加载顺序优势:缩短总加载时间示例:// 并行加载多个子应用await Promise.all([ Garfish.loadApp('app1'), Garfish.loadApp('app2'), Garfish.loadApp('app3')]);5. 资源压缩策略:压缩 JavaScript、CSS、图片等资源实现方式:使用 Webpack 压缩插件启用 Gzip 或 Brotli 压缩优化图片格式和大小优势:减少传输数据量,加快加载速度示例:// Webpack 压缩配置module.exports = { optimization: { minimize: true, minimizer: [ new TerserPlugin(), new CssMinimizerPlugin() ] }};性能监控1. 加载性能监控监控子应用加载时间统计资源加载成功率分析加载瓶颈2. 运行时性能监控监控内存使用情况统计 CPU 占用检测性能问题3. 用户体验监控统计首屏渲染时间监控交互响应时间收集用户反馈最佳实践1. 性能预算设定资源大小限制控制加载时间预算定期检查性能指标2. 性能测试使用 Lighthouse 进行性能测试进行压力测试模拟不同网络环境3. 持续优化定期分析性能数据优化热点代码更新依赖版本4. 性能优化工具使用 Chrome DevTools 分析性能利用 webpack-bundle-analyzer 分析包大小使用性能监控工具常见性能问题及解决方案1. 首屏加载慢原因:资源过多、未优化解决方案:代码分割、懒加载、预加载2. 内存泄漏原因:未正确清理资源解决方案:完善生命周期管理、及时清理3. 重复加载原因:缓存策略不当解决方案:优化缓存配置、避免重复请求4. 渲染卡顿原因:计算量大、DOM 操作多解决方案:虚拟列表、防抖节流、优化渲染逻辑通过综合运用这些性能优化策略,可以显著提升微前端应用的性能和用户体验。
前端阅读 02月21日 16:02

Garfish 与其他微前端框架(如 qiankun、single-spa)相比有哪些优势和劣势?

Garfish 与其他微前端框架(如 qiankun、single-spa、Module Federation)各有特点,选择时需要根据项目需求进行评估。框架对比1. Garfish vs qiankun相似点都基于 single-spa 扩展都提供沙箱隔离机制都支持主流前端框架都有完善的生命周期管理差异点| 特性 | Garfish | qiankun ||------|---------|---------|| 沙箱机制 | 支持快照、代理、严格沙箱 | 主要使用快照沙箱 || 样式隔离 | 支持 Shadow DOM、CSS 作用域 | 主要使用 CSS 作用域 || 路由管理 | 内置路由系统 | 依赖 single-spa 路由 || 性能 | 轻量级,性能开销小 | 相对较重 || 学习曲线 | 较平缓 | 相对陡峭 || 社区活跃度 | 较新,社区较小 | 成熟,社区活跃 |2. Garfish vs single-spa相似点都提供微前端基础能力都支持生命周期管理都支持框架无关差异点| 特性 | Garfish | single-spa ||------|---------|------------|| 易用性 | 开箱即用,配置简单 | 需要手动配置,复杂度高 || 沙箱隔离 | 内置多种沙箱机制 | 需要自行实现 || 样式隔离 | 内置样式隔离方案 | 需要自行实现 || 路由管理 | 内置路由管理 | 需要自行实现 || 文档完善度 | 文档相对简洁 | 文档详细但复杂 |3. Garfish vs Module Federation相似点都支持模块共享都支持独立部署都支持技术栈无关差异点| 特性 | Garfish | Module Federation ||------|---------|-------------------|| 实现方式 | 运行时加载 | 构建时模块共享 || 依赖共享 | 需要手动管理 | 自动共享依赖 || 版本管理 | 需要手动处理 | 自动处理版本冲突 || 构建复杂度 | 相对简单 | 需要配置 Webpack || 适用场景 | 完全独立的应用 | 模块级别的共享 |选择建议选择 Garfish 的场景需要轻量级的微前端解决方案需要多种沙箱隔离机制需要内置的路由和样式隔离团队对微前端有一定了解项目规模中等,不需要过度复杂的方案选择 qiankun 的场景需要成熟的微前端解决方案需要丰富的社区支持和文档团队对 qiankun 有经验项目规模较大,需要稳定可靠的方案选择 single-spa 的场景需要高度定制的微前端方案团队对微前端原理有深入了解需要最大的灵活性愿意投入时间进行配置和优化选择 Module Federation 的场景需要模块级别的共享使用 Webpack 5需要自动依赖管理团队熟悉 Webpack 配置需要细粒度的代码复用迁移策略从其他框架迁移到 Garfish评估现有架构:分析当前微前端实现逐步迁移:先迁移部分子应用保持兼容:确保新旧方案共存测试验证:全面测试迁移效果优化调整:根据实际情况优化配置最佳实践1. 技术选型原则根据团队技术栈选择考虑项目规模和复杂度评估维护成本考虑社区支持和生态2. 混合使用可以结合多个框架的优势根据不同场景选择不同方案保持架构的一致性3. 持续评估定期评估框架的适用性关注框架的更新和发展准备备选方案通过合理选择微前端框架,可以更好地满足项目需求并提升开发效率。
前端阅读 02月19日 17:45

Garfish 的沙箱隔离机制是如何实现的,有哪些隔离策略?

Garfish 的沙箱隔离机制主要通过以下方式实现:1. 快照沙箱(Snapshot Sandbox)在子应用加载前,对当前全局环境(window 对象)进行快照记录所有全局变量和属性子应用运行期间,允许修改全局变量子应用卸载时,恢复到加载前的快照状态2. 代理沙箱(Proxy Sandbox)使用 ES6 Proxy 对 window 对象进行代理拦截对全局变量的读取和写入操作维护一个独立的沙箱环境,每个子应用有自己的全局变量副本通过代理层隔离不同子应用的全局状态3. 严格沙箱(Strict Sandbox)结合快照和代理的优势提供更严格的隔离机制防止子应用间的全局变量污染确保子应用卸载后完全清理副作用隔离范围全局变量隔离:防止子应用间的变量冲突事件监听器隔离:自动清理子应用添加的事件监听定时器隔离:管理 setTimeout、setInterval 等定时器样式隔离:通过 CSS 作用域或 Shadow DOM 隔离样式实现优势性能开销小,不影响应用运行效率支持动态加载和卸载子应用兼容主流浏览器,无需 polyfill提供降级方案,确保在不支持 Proxy 的环境中也能运行沙箱机制确保了微前端架构下各个子应用的独立性和安全性,是实现微前端的关键技术之一。
前端阅读 02月19日 17:45

Garfish 的样式隔离机制是如何实现的,有哪些常见的样式隔离方案?

Garfish 的样式隔离机制确保不同子应用的样式不会相互干扰,是微前端架构中的重要组成部分。样式隔离方案1. CSS 作用域原理:为每个子应用的样式添加唯一前缀或后缀实现方式:使用 PostCSS 插件自动添加作用域通过 CSS Modules 实现局部作用域使用 CSS-in-JS 方案(如 styled-components)优势:简单易用,兼容性好劣势:需要额外的构建配置示例:// PostCSS 配置module.exports = { plugins: [ require('postcss-selector-prefix')({ prefix: '[data-garfish-app="app1"]' }) ]};2. Shadow DOM原理:使用浏览器原生的 Shadow DOM 技术隔离样式实现方式:将子应用挂载到 Shadow DOM 容器中样式只在 Shadow DOM 内部生效自动隔离全局样式优势:完全隔离,浏览器原生支持劣势:部分浏览器兼容性问题,事件冒泡处理复杂示例:// 创建 Shadow DOM 容器const shadowRoot = container.attachShadow({ mode: 'open' });// 将子应用挂载到 Shadow DOMshadowRoot.appendChild(appElement);3. 动态样式表管理原理:在子应用挂载时加载样式,卸载时移除样式实现方式:动态创建和删除 <style> 标签管理样式表的加载和卸载避免样式残留优势:灵活控制,性能较好劣势:需要手动管理样式生命周期示例:// 动态加载样式function loadStylesheet(url) { const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = url; document.head.appendChild(link); return link;}// 卸载时移除样式function unloadStylesheet(link) { document.head.removeChild(link);}4. CSS 命名约定原理:通过命名规范避免样式冲突实现方式:使用 BEM 命名规范为每个子应用设置唯一的类名前缀遵循统一的命名约定优势:无需额外工具,易于理解劣势:依赖开发者自觉,容易出错示例:/* 子应用 app1 的样式 */.app1__header { }.app1__button { }.app1__button--primary { }样式隔离配置Garfish 配置示例Garfish.run({ apps: [ { name: 'app1', entry: '//localhost:3001', sandbox: { strictIsolation: true, styleIsolation: 'shadow-dom' // 或 'scoped-css' } } ]});最佳实践1. 选择合适的隔离方案简单项目:CSS 作用域或命名约定复杂项目:Shadow DOM混合场景:结合多种方案2. 全局样式处理主应用提供全局基础样式子应用避免使用全局选择器使用 CSS 变量管理主题3. 第三方库样式使用作用域化版本手动修改第三方库样式考虑使用样式隔离方案4. 性能优化避免重复加载样式使用样式压缩合理使用 CSS 缓存5. 开发体验提供样式隔离的调试工具支持热更新提供样式冲突检测常见问题解决1. 样式不生效检查样式隔离配置确认样式加载顺序检查选择器优先级2. 样式冲突使用更具体的选择器调整样式隔离方案检查全局样式影响3. 性能问题减少样式文件大小优化样式加载策略使用样式缓存通过合理配置样式隔离机制,可以确保微前端应用的样式独立性和可维护性。