From f13f809a3f6b7cf1ef0c2b41963bb5259aee985e Mon Sep 17 00:00:00 2001 From: liquid Date: Thu, 5 Feb 2026 09:08:53 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=B7=BB=E5=8A=A0=E7=BC=96=E7=A0=81?= =?UTF-8?q?=E8=A7=84=E5=88=99=20-=20SW=20=E5=88=9D=E5=A7=8B=E5=8C=96?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CLAUDE.md: 添加第 23 条规则 - 使用 swChannelClient.ensureReady() 作为统一入口 - CODING_RULES.md: 添加详细规则和示例代码 --- CLAUDE.md | 1 + docs/CODING_RULES.md | 153 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 154 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index a92c45d7..e0257451 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -171,6 +171,7 @@ Service Worker (后台执行) 20. **同步数据格式一致性**:`tasks.json` 结构是 `{ completedTasks: Task[] }` 不是数组,画板文件是 `board_{id}.json` 不是 `.drawnix` 21. **RPC 超时与重连**:关键 RPC 调用(如工作流提交)需设置合理超时(15-30秒),超时时主动重新初始化 SW 连接并重试 22. **降级路径功能一致性**:降级到主线程直接调用 API 时,必须保持与 SW 模式相同的功能行为(如 LLM API 日志记录),否则调试工具会漏掉这些调用 +23. **SW 初始化统一入口**:使用 `swChannelClient.ensureReady()` 作为统一入口,避免在各处重复初始化逻辑;任务队列用 `swTaskQueueService.initialize()` ### React 规则 diff --git a/docs/CODING_RULES.md b/docs/CODING_RULES.md index ce62bfb2..0191a233 100644 --- a/docs/CODING_RULES.md +++ b/docs/CODING_RULES.md @@ -1458,6 +1458,52 @@ const submitToSW = async (workflow) => { **原因**: Service Worker 的 `workflowHandler` 需要收到 `TASK_QUEUE_INIT` 消息后才会初始化。如果在 SW 初始化前提交工作流,消息会被暂存到 `pendingWorkflowMessages`,等待配置到达。若配置永远不到达(如 `swTaskQueueService.initialize()` 未被调用),工作流就永远不会开始执行,步骤状态保持 `pending`。 +### Service Worker 初始化统一入口 + +**场景**: 需要确保 SW 客户端已初始化并配置好后再进行 API 调用 + +❌ **错误示例**: +```typescript +// 错误:在各处重复 SW 初始化逻辑 +async function callSWApi() { + if (shouldUseSWTaskQueue()) { + if (!swChannelClient.isInitialized()) { + const settings = geminiSettings.get(); + if (settings.apiKey && settings.baseUrl) { + await settingsManager.waitForInitialization(); + await swChannelClient.initialize(); + await swChannelClient.init({ + geminiConfig: { apiKey: settings.apiKey, baseUrl: settings.baseUrl }, + videoConfig: { baseUrl: settings.baseUrl }, + }); + } + } + // 使用 SW... + } +} +``` + +✅ **正确示例**: +```typescript +// 正确:使用统一入口 +async function callSWApi() { + const useSW = await swChannelClient.ensureReady(); + + if (useSW) { + // 使用 SW... + } else { + // 降级到主线程... + } +} +``` + +**统一入口说明**: +- `swChannelClient.ensureReady()`: 统一的异步初始化入口,内部处理 URL 参数检查、设置验证、初始化和配置 +- `swTaskQueueService.initialize()`: 任务队列服务初始化,内部调用 `ensureReady()` + 设置 visibility 监听器 +- `shouldUseSWTaskQueue()`: 同步检查函数,仅用于判断模式,不进行初始化 + +**原因**: 分散的 SW 初始化逻辑会导致代码重复、维护困难,且容易遗漏某些检查步骤。使用统一入口可以确保所有初始化逻辑集中管理,便于维护和调试。 + ### Service Worker 更新提示在开发模式下被跳过 **场景**: 在 localhost 本地测试 Service Worker 更新提示功能 @@ -7071,3 +7117,110 @@ async deleteBoard(id: string) { - 热更新(HMR)可能导致旧的订阅被替换 - 关键业务逻辑不应依赖于"某处可能存在的订阅者" +### 自定义菜单/按钮组件的焦点样式和键盘导航 + +**场景**: 实现自定义菜单、下拉列表等交互组件时 + +❌ **错误示例**: +```scss +// 错误:没有处理焦点样式,浏览器会显示默认的蓝色边框 +.menu-item { + background-color: transparent; + border: 1px solid transparent; + // 缺少 outline 处理 +} +``` + +```tsx +// 错误:菜单没有键盘导航支持 +const Menu = ({ children }) => { + return ( +
+ {children} +
+ ); +}; +``` + +✅ **正确示例**: +```scss +// 正确:移除默认焦点边框,用 focus-visible 提供键盘导航反馈 +.menu-item { + background-color: transparent; + border: 1px solid transparent; + outline: none; // 移除浏览器默认蓝色焦点边框 + + &:hover { + background-color: var(--color-surface-primary-container); + } + + &:focus-visible { + // 仅键盘导航时显示视觉反馈(鼠标点击不会触发) + background-color: var(--color-surface-primary-container); + } +} +``` + +```tsx +// 正确:完整的键盘导航和无障碍支持 +const Menu = ({ children, onClose }) => { + const containerRef = useRef(null); + + const getFocusableItems = useCallback(() => { + return Array.from( + containerRef.current?.querySelectorAll( + 'button.menu-item:not([disabled])' + ) || [] + ); + }, []); + + const handleKeyDown = useCallback((event: React.KeyboardEvent) => { + const items = getFocusableItems(); + const currentIndex = items.findIndex(item => item === document.activeElement); + + switch (event.key) { + case 'ArrowDown': + event.preventDefault(); + items[(currentIndex + 1) % items.length]?.focus(); + break; + case 'ArrowUp': + event.preventDefault(); + items[(currentIndex - 1 + items.length) % items.length]?.focus(); + break; + case 'Escape': + event.preventDefault(); + onClose?.(); + break; + } + }, [getFocusableItems, onClose]); + + // 菜单打开时自动聚焦第一项 + useEffect(() => { + requestAnimationFrame(() => { + getFocusableItems()[0]?.focus(); + }); + }, []); + + return ( +
+ {children} +
+ ); +}; + +// MenuItem 添加 role="menuitem" +const MenuItem = ({ onClick, children }) => ( + +); +``` + +**关键点**: +- `outline: none` 移除浏览器默认蓝色焦点边框 +- `focus-visible` 仅在键盘导航时提供视觉反馈,鼠标点击不会触发 +- 支持 ↑↓ 箭头键循环切换焦点 +- 支持 Escape 关闭菜单 +- 菜单打开时自动聚焦第一项 +- 添加 `role="menu"` 和 `role="menuitem"` 支持无障碍 +