Files
TrueGrowth/docs/STARTUP_AND_TOOLBAR_LESSONS.md

358 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 启动拆包与工具栏状态经验总结
更新日期2026-04-21
## 这轮最值得记住的规律
### 1. 启动进度只能有一个“百分比真源”
这轮 loading 卡顿感的根因,不是进度条本身,而是进度来源混杂:
- HTML 启动壳自己在写阶段百分比
- 主线程初始化阶段也在写百分比
- Service Worker 又在回传资源进度
结果就是进度条会“跳”“卡”“倒像是更慢了”。
更稳的做法是:
- `SW` 未就绪前,只做短时间平滑模拟,让用户知道系统在工作
- `SW` 一旦开始回传进度,就由 `SW` 接管后半段百分比
- 其余阶段只更新文案,不再写数值
一句话:文案可以多源,百分比只能单源。
### 2. 懒加载要区分“触发器”和“内容体”
这轮多个两击问题,本质都和这个边界有关:
- 可以延后的是抽屉内容、弹窗内容、工具窗口内容
- 不该延后的是用户第一下要点到的按钮、触发器、壳层容器
如果把触发器本身也放进懒加载链路,第一次点击就可能只完成“挂载按钮/挂载容器”,第二次点击才真正打开内容,用户会直接感知成 bug。
推荐原则:
- 按钮、抽屉骨架、输入框外壳常驻
- 重内容、重依赖、低频模块按需挂载
- 首次点击允许局部 loading但不允许“点击无响应”
### 3. 依赖编辑器上下文的组件,不能被随意挪出树外
这轮 `AI 图片生成``useBoard must be used inside <Plait>`,本质是把依赖 `Plait` 上下文的对话框,移到了错误的挂载层级。
经验是:
- 凡是依赖 `useBoard()``useDrawnix()`、选择态、画布上下文的组件
- 在做延迟加载、拆层、抽离壳层时
- 先确认它依赖的 Provider / Board Context 还在不在上游
尤其是:
- 对话框懒加载
- 抽屉懒加载
- “核心画布壳层 / 延后功能层”拆分
如果组件依赖的是“当前画布实例”,那它的懒加载位置必须仍在画布上下文树内。
### 4. `setState(updater)` 里不要顺手改别的面板状态
左侧“项目 / 工具箱 / 任务面板”要点两下的问题,根因不是点击事件丢了,而是 toggle 逻辑写成了:
-`setState(prev => ...)` 的 updater 里
- 顺手调用 `closeAllDrawers()`
- 于是一次点击里混进了多份状态更新
这种写法在互斥面板场景里很容易让首击状态被冲掉。
更稳的方式是显式顺序化:
1. 先判断目标当前是否已打开
2. 如果要打开,先关闭其它互斥面板
3. 最后再打开目标面板
一句话updater 里只算当前值,不做跨状态副作用。
### 5. 左侧常驻工具栏的顺序,不能跟“最近激活顺序”绑定
这轮另一类体验问题是:左侧常驻工具点一下就换位。
根因不是图标组件,也不是 `selected` 高亮,而是工具栏数据源按 `activationOrder` 排序:
- 工具一打开 / 一恢复,激活顺序就刷新
- 工具栏再按这个顺序重排
- 用户看到的就是“按钮自己跑了”
对常驻工具栏来说,用户心智更接近“固定栏位”,不是“最近使用列表”。
所以更合理的规则是:
- pinned 工具始终保持既有栏位顺序
- 打开后只替换本栏位的状态
- 非 pinned 的临时实例再单独排到后面
一句话:常驻栏看重空间记忆,不看重最近激活。
### 6. 版本更新后的懒加载 chunk 失效,要有“一次性自恢复”
这次 `DrawnixDeferredRuntime` 的报错:
- `Failed to fetch dynamically imported module`
本质不是业务逻辑错误,而是用户页面停留在旧版本,部署后旧 HTML / 旧运行时还在引用已经失效的 hash chunk。
这类问题的特点是:
- 用户刷新前一切正常
- 一旦触发某个 `React.lazy()` 模块才爆
- 报错看起来像功能坏了,实际是版本切换窗口里的静态资源失配
更稳的处理方式不是把所有懒加载都改成手写 loader而是补一层轻量兜底
-`ErrorBoundary` 里识别动态导入 / chunk 失效错误
- 按“当前版本 + 当前模块”做一次性重试标记,避免死循环
- 自动刷新一次,并带时间戳参数绕过旧缓存
- 应用成功启动后再清理恢复参数,避免 URL 污染
一句话:发布后 chunk 失效不是“要不要刷新”的问题,而是“要不要帮用户只刷新一次并避免循环”的问题。
## 这轮适合沉淀成默认规则的做法
- 启动进度:前半段可模拟,后半段由资源进度单源接管
- 懒加载:只懒内容,不懒触发器
- 组件拆层:先确认上下文依赖,再移动挂载位置
- 互斥面板:显式开关,不在 updater 内联副作用
- 常驻工具栏:固定顺序优先于最近激活
- 版本切换:懒加载 chunk 失效时自动恢复一次,但必须有防循环保护
## 后续开发检查清单
- 这个 loading 百分比现在到底是谁在写?
- 这个入口第一次点击,是打开内容,还是只完成挂载?
- 这个懒加载组件是否依赖画布 / Provider 上下文?
- 如果新版本刚发布,这个懒加载失败是不是“旧页面引用失效 chunk”而不是业务异常
- 这个 toggle 是否在 updater 里偷偷改了别的 state
- 这个工具栏是“固定栏位”还是“最近使用列表”?排序规则是否和用户心智一致?
## 一句话总结
这轮真正值钱的经验不是“把 bug 修掉了”,而是明确了 6 条边界:
- 百分比单源
- 触发器常驻
- 上下文不丢
- toggle 不串副作用
- 常驻栏位不乱跳
- 失效 chunk 只自恢复一次
---
## 2026-05-17首屏壳层拆分与 chunk 守门经验
### 现象
- 首屏慢不只来自代码体积,也可能来自运行时静态环和 chunk 归属错误。
- 这轮真正卡住构建的,不是业务组件本身,而是 `ai-chat``tool-windows` 之间被常量文件、服务入口、懒加载边一起拉成了静态回边。
- 纯工具函数如果和会反向依赖 `data/image` 的运行时模块放在同一个文件里,循环会在源码层先炸,然后再在 chunk 层放大。
### 架构变更
- `apps/web/src/main.tsx` 只保留最早期恢复与 bootstrap 入口,后续应用初始化下沉到 `bootstrap.tsx`
- `Drawnix` 拆成首屏壳层和延后功能层,任务恢复、自动插入、字体/视频恢复等副作用交给 idle 启动器。
- 动态导入失败恢复独立到 `lazy-asset-recovery.ts`,避免首屏入口顺手背上重恢复逻辑。
- 图片自然尺寸这类轻量能力单独下沉到纯工具模块,`utils/image.ts` 只保留导出和编辑逻辑。
- `tool-ids.ts` 这类纯常量不能被目录级 `tool-windows` 规则误收编,否则很容易把轻量调用侧拖回重 chunk。
- 构建后校验不再只看入口 HTML还要看入口静态依赖链、chunk SCC 和首屏 500KB 预算。
### 经验
- 先修运行时循环,再谈 chunk 瘦身。
- manualChunks 是分包守门,不是架构补丁。
- 纯常量、纯工具、纯类型文件尽量留在低层共享入口,别让它们进入重运行时桶。
- 首屏建议目标 200-300KB 未压缩500KB 作为硬上限。
- 构建校验脚本最好同时守三件事入口链、chunk 环、首屏体积。
- 当环只剩纯常量误入重 chunk 时,先给低层入口加分包例外,再考虑继续拆模块。
### 复盘清单
- 这个文件是不是既做纯工具,又回引了运行时服务?
- 这个常量是不是被错误分到重 chunk 里了?
- 这个懒加载边界是不是只该延后内容,不该拖延触发器?
- 构建守门脚本有没有把“能跑”升级成“能跑且不超预算”?
后面继续做启动性能和工具系统时,只要这 6 条不破,很多体验问题都会提前消失。
---
## 2026-04-22SW 空闲预取与常驻工具条恢复经验
### 现象
- 左侧常驻工具 icon 只有在点开工具抽屉后才出现
- `SW` 日志显示已经激活,但页面侧 `SWChannelClient` 多次初始化超时
- 开发态 `idle-prefetch-manifest.json` 缺失,`prefetchIdleGroups` 直接中止
### 根因
- 页面把“有 `controller`”误当成“有可用 `Service Worker`”,但实际存在 `ready.active` 已激活、页面仍未被 controller 接管的窗口期
- 常驻工具条显示链路和完整 `tool-window runtime` 启用链路绑死,导致“显示 icon”也被重运行时阻塞
- UI 又额外依赖 `tool-windows` 分组预取完成信号,开发态 manifest 缺失时就会一直卡住
- `idle prefetch` 单次跑批如果人为截断,完成状态就会长期失真,最后只能靠超时兜底
### 最终方案
- `SWChannelClient` 初始化放宽为:`controller``navigator.serviceWorker.ready.active` 任一可用即可建通道
- 常驻工具条拆成两段:
icon 显示单独启用;点击 icon 时再按需启完整 `tool-window runtime`
- `toolWindowService` 恢复 launcher 状态时优先读取完整工具定义,保证首屏图标和标题完整
- `SW``activate` 后自动消费 idle prefetch 默认分组,并把完成状态广播给页面
- 页面监听 `SW_IDLE_PREFETCH_STATUS`,但只把它当优化信号;超时后直接放行常驻工具条,避免功能被优化链路反向阻塞
- `idle prefetch` 仍用小并发,但不能再裁剪总任务数,否则完成分组永远不闭环
### 这次最值得固化的规则
- `Service Worker` 可用性判断要区分:
`controller` 是“当前页已受控”,`ready.active` 是“后台已有可用 worker”两者不是一回事
- 首屏可见能力和重运行时能力要解耦:
先让用户看到、点得到,再做按需增强
- 预取状态只能作为加速信号,不能作为功能显示前置条件
- 开发态缺 manifest、生产态资源回源失败都要保证 UI 有可退化路径
### 排障顺序建议
1. 先看 `navigator.serviceWorker.controller``navigator.serviceWorker.ready.active` 是否分叉
2. 再看显示链路是否被 runtime 初始化、manifest、预取完成状态串联阻塞
3. 最后才看图标数据源、launcher 恢复、工具定义是否缺字段
---
## 2026-04-26SW 关闭场景下的通道初始化超时经验
### 现象
- 控制台持续出现 `[SWChannelClient] Attempt 4 failed: Error: SW activation timeout`
- 调试状态里 `supported: true`,但 `controller: null``registration: null`
- 本地开发默认关闭 SW 或 URL 带 `?sw=0` 时仍然触发了 `swChannelClient.initialize()`
### 根因
- 页面只用“浏览器支持 `serviceWorker`”判断是否启动 SW 诊断链路,没有复用真正的 SW 启用开关
- `sw-console-capture` 为了尽早捕获日志,会主动初始化 `swChannelClient`,结果在 SW 未注册时反复等待不存在的 `controllerchange`
- SW 通道自身没有识别 `?sw=0`,其他调用点即使绕过主入口,也可能重新进入初始化重试
- 日志队列在通道不可用时只进不出,虽然单条较小,但长期打开页面会形成不必要内存占用
### 固化规则
- SW 相关入口必须共享同一个开关语义:
生产和本地调试默认启用;`?sw=0` 显式关闭;本地默认启动命令必须同步 watch 最新 `sw.js`
- 调试、崩溃、控制台捕获这类旁路能力,不能因为“想诊断 SW”而绕过 SW 开关主动拉起通道
- `SWChannelClient.initialize()` 自身也要兜底识别 `?sw=0`,避免未来新增调用点造成重复超时
- 离线日志队列必须有容量上限,通道不可用时丢弃最旧项,不能无限积压
- 控制台捕获要过滤 `[SWChannelClient]``[ServiceWorkerChannel]` 这类自身通道日志,避免错误日志再次驱动初始化与上报
### 排障顺序建议
1. 先看当前 URL 是否带 `?sw=0`,以及本地是否正在 watch 最新 `sw.js`
2. 再看是否存在 `registration`;如果没有注册,不要继续等 `controllerchange`
3. 最后排查是谁调用了 `swChannelClient.initialize()`,优先检查控制台捕获、崩溃上报、调试面板等旁路链路
---
## 2026-04-27左侧浮岛工具栏与抽屉联动经验
### 现象
- 用户浏览器或系统存在左侧边缘标签时,固定贴边工具栏容易误触冲突。
- 直接把工具栏整体外移虽然能避让,但看起来像布局 bug破坏“贴边悬浮岛”的原始观感。
- 将工具栏改成可拖动后,拖动过程中任务队列抽屉会被关闭态动画带出来。
- 任务队列抽屉从屏幕边缘大幅滑入,动画过重;抽屉打开后,左侧画布底色和抽屉底色割裂。
- 顶部拖动把手、底部固定按钮区如果只改父容器圆角,局部背景仍会把圆角视觉盖平。
### 根因
- “避让侧边标签”是用户个体环境问题,不适合用全局默认外移解决。
- 工具栏位置、抽屉位置、提示浮层位置、画布 fit-frame 避让如果各自写死宽度,拖动后必然错位。
- 关闭态抽屉的 `transform` 依赖工具栏右边界变量时,拖动工具栏会触发关闭态抽屉过渡,形成“被拖出来”的错觉。
- 任务队列抽屉和工具箱共用基础抽屉,但任务队列内容更重,应该用更轻的专属 motion。
- 抽屉只从工具栏右侧开始绘制时,工具栏左侧仍露出画布底色;抽屉色和画布色差会被用户感知成断层。
### 最终方案
- 默认仍贴屏幕左边缘,保持悬浮岛观感;只提供顶部拖动把手,让需要避让的用户手动拖入内侧。
- 工具栏横向位置用 `localStorage` 轻量持久化,双击把手复位到默认贴边位置。
- 统一维护 `--aitu-toolbar-left``--aitu-toolbar-right-edge`抽屉、提示、fit-frame 都读同一套右边界。
- 拖动过程中只更新 CSS 变量和 DOM style拖动结束后再落 React state减少高频重渲染。
- 关闭态抽屉从 `translateX(-100%)` 隐藏,打开态再平移到工具栏右边界;拖动期间给根节点加 `aitu-toolbar-dragging` 禁用抽屉 transition。
- 任务队列抽屉使用轻量淡入:
`160ms transform + 120ms opacity`,关闭态保持 `visibility: hidden``pointer-events: none`
- 抽屉打开时用同色 `box-shadow` 向左铺到屏幕边缘,工具栏 z-index 高于抽屉,让工具栏仍像浮在抽屉上。
- 工具栏底部固定区也要继承底部圆角,避免底部按钮区背景把父容器圆角盖平。
### 固化规则
- 默认位置优先保留产品最自然的视觉形态;兼容特殊环境时优先给用户可调能力,不要把所有用户都推到次优默认。
- 可拖动 UI 的高频 pointer move 不要每帧依赖 React state能用 CSS 变量同步的位置,优先走 CSS 变量。
- 所有依赖工具栏边界的组件必须共享同一来源,不能散落硬编码宽度。
- 隐藏态元素如果跟随一个高频变化变量,要么禁用 transition要么让隐藏态不依赖这个变量。
- 抽屉“内容从哪里开始”和“背景铺到哪里”可以分离:内容对齐工具栏,背景可延展到屏幕边缘。
- tooltip 只负责展示提示,不应接收 pointer events 挡住工具按钮点击。
- 改圆角时要检查内部固定区域、底部区域、滚动区域背景是否覆盖父容器弧度。
### 回归检查清单
- 默认桌面视口:工具栏 `left = 0`,右边界 CSS 变量等于工具栏实际右边界。
- 拖动把手:拖动时任务抽屉不外露,松手后位置持久化,双击可复位。
- 任务队列:首次打开有轻量动画,关闭态不可见且不可点击。
- 工具箱 / 项目抽屉 / 任务队列:打开后左边缘贴齐工具栏右边界。
- 抽屉打开后:工具栏左侧到屏幕边缘为抽屉同色背景,工具栏仍在上层。
- 移动端:不显示拖动把手,默认收起形态不变。
---
## 2026-04-27右侧停靠工具栏与双向抽屉经验
### 现象
- 工具栏只允许拖到左侧内缘时,右手用户或右侧工作流无法获得同等避让能力。
- 工具栏拖到右侧后,项目/工具箱/任务抽屉如果仍向右展开,会被屏幕边界截断。
- 对话抽屉、小地图、缩放控件如果只按“右屏幕边缘”定位,会被右侧工具栏遮挡。
- 对话抽屉收起态如果只平移,不做 `visibility` / `pointer-events` 隐藏,右侧仍会露出面板边缘。
- 抽屉内容对齐工具栏后,抽屉背景没有铺满到屏幕边缘时,会出现画布色和抽屉色断层。
### 根因
- 工具栏位置从“左边界变量”升级为“双侧停靠语义”后,依赖它的浮层也必须知道当前停靠方向。
- 右侧停靠时,遮挡区域不是工具栏右边界,而是 `viewportWidth - toolbarLeft`
- 抽屉宽度、抽屉展开方向、导航控件避让、fit-frame 可视区计算,不能各自用硬编码边距。
- 关闭态元素跟随工具栏位置时,仅靠 `transform` 容易在边缘露出残影或接收交互。
### 最终方案
- 工具栏可拖到屏幕最右侧,超过屏幕中线后在根节点标记 `aitu-toolbar-dock-right`
- 继续用轻量 CSS 变量同步布局,不新增配置实体:
`--aitu-toolbar-left``--aitu-toolbar-right-edge``--aitu-toolbar-right-dock-width``--aitu-toolbar-right-avoidance``--aitu-toolbar-side-panel-max-width`
- 右侧停靠时,项目/工具箱/任务抽屉向左展开,关闭态从屏幕外隐藏,打开态左边缘贴齐工具栏左侧。
- 右侧停靠的抽屉用同色 `box-shadow` 向屏幕边缘延展背景,内容对齐和背景铺满分离处理。
- `SideDrawer` 的 resize 方向按停靠侧切换;右侧停靠时拖左边缘调整宽度。
- `ChatDrawer` 右侧停靠时让出工具栏宽度,打开态背景铺到屏幕右边缘,关闭态使用 `opacity + visibility + pointer-events` 彻底隐藏。
- `ViewNavigation` / 小地图同时避让工具栏占位和打开的对话抽屉fit-frame 按左右遮挡分别计算可视区。
- 顶部拖拽把手默认态进一步弱化,只在 hover、focus、dragging 时增强。
### 固化规则
- 可拖动悬浮工具栏一旦支持跨屏停靠,所有依赖边界的组件都要读统一 CSS 变量,不再假设工具栏永远在左侧。
- 判断抽屉展开方向应基于工具栏中心点所在半屏,而不是用户最后一次拖动方向。
- 右侧停靠时,导航控件的避让值要叠加工具栏宽度和已打开抽屉宽度,避免小地图被抽屉压住。
- 关闭态抽屉必须同时处理视觉隐藏和交互隐藏:`visibility: hidden``pointer-events: none`、必要时配合 `opacity`
- 抽屉背景延展可以用同色阴影铺屏,但不要让内容区域跟着铺满,否则会破坏对齐和可读性。
- 改动右侧面板时,要同步回归 ChatDrawer、ViewNavigation、Minimap、fit-frame 和各类 toolbar-right 抽屉。
### 回归检查清单
- 工具栏拖到右边缘:工具栏不出屏,刷新后位置保留。
- 右侧工具栏打开项目/工具箱/任务抽屉:抽屉向左展开,不被屏幕裁切,背景铺到右侧屏幕边缘。
- 对话抽屉关闭态:面板不可见且不可点击,只保留把手。
- 对话抽屉打开态:右侧让出工具栏宽度,背景延伸到屏幕右侧。
- 对话抽屉打开 + 小地图展开:小地图和缩放控件避开“工具栏 + 对话抽屉”组合宽度。
- fit-frame工具栏/抽屉在左侧或右侧时Frame 都居中到剩余可视区。