docs: 添加编码规则 - SW 初始化统一入口

- CLAUDE.md: 添加第 23 条规则 - 使用 swChannelClient.ensureReady() 作为统一入口
- CODING_RULES.md: 添加详细规则和示例代码
This commit is contained in:
liquid committed 2026-02-05 09:08:53 +08:00
1 parent e6a39db673
commit f13f809a3f
2 files changed
+154

No files matched your search

+1
View File
@@ -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 规则
+153
View File
@@ -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 (
<div className="menu">
{children}
</div>
);
};
```
✅ **正确示例**:
```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<HTMLDivElement>(null);
const getFocusableItems = useCallback(() => {
return Array.from(
containerRef.current?.querySelectorAll<HTMLButtonElement>(
'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 (
<div ref={containerRef} role="menu" onKeyDown={handleKeyDown}>
{children}
</div>
);
};
// MenuItem 添加 role="menuitem"
const MenuItem = ({ onClick, children }) => (
<button className="menu-item" onClick={onClick} role="menuitem">
{children}
</button>
);
```
**关键点**:
- `outline: none` 移除浏览器默认蓝色焦点边框
- `focus-visible` 仅在键盘导航时提供视觉反馈,鼠标点击不会触发
- 支持 ↑↓ 箭头键循环切换焦点
- 支持 Escape 关闭菜单
- 菜单打开时自动聚焦第一项
- 添加 `role="menu"` 和 `role="menuitem"` 支持无障碍