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 控制显隐:
javascriptexport 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)。
完整生命周期流程图
shell首次加载: 下载子应用 JS → 执行沙箱隔离 → 调用 provider() → bootstrap() → mount() 路由切换(非缓存): 旧子应用 unmount() → 新子应用 mount() 路由切换(缓存模式): 旧子应用 hide() → 新子应用 mount() 或 show() 属性更新: 主应用 setProps() → 子应用 update() 彻底销毁: unmount() → 清理沙箱 → 释放内存
插件级生命周期钩子
除子应用生命周期外,Garfish 还提供主应用侧的插件钩子,用于拦截加载过程:
javascriptGarfish.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 内查找挂载点:
javascriptmount({ dom }) { // 错误:document.getElementById('app') // 正确:在 Garfish 提供的 dom 内查找 const container = dom.querySelector('#sub-app-root'); ReactDOM.render(<App />, container); }
2. unmount 后仍然有内存泄漏
定时器和全局事件监听不会随 DOM 移除而自动清理:
javascriptlet 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。