Files
2026-06-30 23:26:27 +08:00

1139 lines
35 KiB
Markdown
Raw Permalink 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.
# 代码编写规范
本文档定义了 Testing Tools 浏览器扩展项目的编码规范和最佳实践。所有代码贡献者应遵循这些规范以保持代码库的一致性和可维护性。
## 1. TypeScript 规范
### 1.1 类型定义:`interface` vs `type`
- **`interface`**:用于组件 Props、对象结构、Context 类型等可扩展结构
- **`type`**:用于联合类型、工具类型、不可扩展的类型别名
```typescript
// ✅ interface — 组件 Props / 对象结构
export interface GlobalSnackbarProps {
message: string;
open: boolean;
onClose: () => void;
severity?: SnackbarSeverity;
}
// ✅ interface — 继承 HTML 属性
interface LiveClockProps extends React.HTMLAttributes<HTMLDivElement> {
unit: UnitType;
onUseNow: (val: number) => void;
}
// ✅ type — 联合类型
export type ThemeMode = 'light' | 'dark' | 'system';
export type PageType = 'dashboard' | 'timestamp' | 'storageCleaner' | ...;
// ✅ type — 工具类型
export type ResolvedThemeMode = 'light' | 'dark';
```
### 1.2 泛型使用
广泛使用泛型约束,结合 `extends` 进行类型守卫:
```typescript
// ✅ 泛型 + extends 约束
export interface SwitchOption<T extends string | number = string> {
value: T;
label: React.ReactNode;
}
// ✅ 泛型 + StorageSchema 键约束
async get<K extends keyof StorageSchema>(
key: K,
defaultValue?: StorageSchema[K],
): Promise<StorageSchema[K] | undefined> { ... }
// ✅ 泛型 Hook
export const useStorageState = <K extends keyof StorageSchema>(
key: K,
defaultValue: StorageSchema[K],
validator?: (val: unknown) => val is StorageSchema[K],
) => { ... }
```
### 1.3 类型守卫
优先使用类型守卫函数(`val is Type` 谓词),避免 `as` 强转:
```typescript
// ✅ 类型守卫谓词函数
const isValidMode = (v: unknown): v is ThemeMode => VALID_MODES.includes(v as ThemeMode);
const isValidPage = (page: unknown): page is PageType => {
return typeof page === 'string' && (getAllFeatureKeys() as string[]).includes(page);
};
// ✅ 安全的 as 断言,仅在类型守卫验证后使用
export function isSupportedImageType(mimeType: string): boolean {
return (SUPPORTED_IMAGE_TYPES as readonly string[]).includes(mimeType);
}
```
### 1.4 导出模式
| 场景 | 导出方式 | 示例 |
| ----------- | ------------------------------------------------ | --------------------------------- |
| 页面组件 | `export default function ComponentName()` | `src/pages/Timestamp/index.tsx` |
| 业务组件 | `const X = React.memo(...)` + `export default X` | `LiveClock.tsx`, `ResultView.tsx` |
| UI 原子组件 | `React.forwardRef(...)` + `export { X }` | `src/components/ui/button.tsx` |
| 工具函数 | `export function xxx()` | `src/utils/clipboard.ts` |
| 自定义 Hook | `export function useXxx()` | `src/utils/useStorageState.ts` |
| 类型/接口 | `export interface` / `export type` | `src/types/storage.d.ts` |
```typescript
// ✅ 页面组件 — default export
export default function Index() { ... }
// ✅ 需要 memo 的组件 — 箭头函数 + React.memo + displayName
const LiveClock = React.memo(({ ... }: LiveClockProps) => { ... });
LiveClock.displayName = 'LiveClock';
export default LiveClock;
// ✅ UI 组件 — forwardRef + 命名导出
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(...);
Button.displayName = 'Button';
export { Button, buttonVariants };
```
---
## 2. React 组件规范
### 2.1 组件定义方式
- **标准组件**:使用 `function` 声明
- **需要 memo 的组件**:使用箭头函数 + `React.memo`
- **需要 ref 的组件**:使用 `React.forwardRef`
- **错误边界**:使用 Class 组件(React 要求)
```typescript
// ✅ 标准页面组件
export default function Index() { ... }
// ✅ 需要 memo 的组件
const LiveClock = React.memo(({ unit, onUseNow, className, ...props }: LiveClockProps) => {
...
});
LiveClock.displayName = 'LiveClock';
export default LiveClock;
// ✅ 需要 ref 的组件
const TextInputArea = forwardRef<HTMLTextAreaElement, TextInputAreaProps>((props, ref) => {
...
});
TextInputArea.displayName = 'TextInputArea';
export default TextInputArea;
// ✅ Class 组件(仅用于 ErrorBoundary
export class ErrorBoundary extends Component<Props, State> { ... }
```
### 2.2 Props 模式
- 使用 `interface` 定义 Props
- 继承 `React.HTMLAttributes` 以支持原生属性透传
- 使用 `Omit` 排除冲突属性
- 解构 `className``...rest props`
```typescript
// ✅ 继承 HTML 属性 + className 透传
interface ResultViewProps extends React.HTMLAttributes<HTMLDivElement> {
result: string;
mode: 'ts2dt' | 'dt2ts';
unit: UnitType;
zone: string;
showEmptyPlaceholder?: boolean;
}
// 使用时解构 className 和 rest props
const ResultView = React.memo(({
result, mode, unit, zone,
showEmptyPlaceholder = false,
className, ...props
}: ResultViewProps) => {
return <div className={cn('flex flex-col w-full', className)} {...props}>...</div>;
});
// ✅ Omit 排除冲突属性
export interface TextInputAreaProps extends Omit<
React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'onChange'
> { ... }
```
### 2.3 状态管理
- **本地状态**`useState` + 惰性初始化
- **衍生状态**`useMemo` 响应式计算管线
- **持久化状态**Chrome Storage + `localStorage` 快照
- **全局状态**React Context
```typescript
// ✅ useState + 惰性初始化
const [input, setInput] = useState(() => String(Date.now()));
// ✅ useMemo 响应式计算管线(零延迟,无需手动 convert 按钮)
const conversionPipeline = useMemo(() => {
const rawInput = input.trim();
if (!rawInput) return { result: '', error: '' };
// ... 自动计算结果
}, [input, mode, unit, zone, t]);
// ✅ Chrome Storage 持久化状态
export const useStorageState = <K extends keyof StorageSchema>(
key: K, defaultValue: StorageSchema[K], validator?: ...
) => { ... }
```
### 2.4 副作用模式
- **取消标志**:防止异步竞态
- **ref 回调指针**:保持回调最新避免依赖膨胀
- **事件监听 cleanup**:始终在 cleanup 中移除监听器
- **定时器 cleanup**:始终在 cleanup 中清除定时器
```typescript
// ✅ 取消标志模式 — 防止异步竞态
useEffect(() => {
let cancelled = false;
storageUtil.get(THEME_MODE_KEY, 'system').then((saved) => {
if (cancelled) return;
if (isValidMode(saved)) { ... }
});
return () => { cancelled = true; };
}, [updateResolved]);
// ✅ ref 回调指针 — 保持回调最新避免依赖膨胀
const onUseNowRef = useRef(onUseNow);
useEffect(() => { onUseNowRef.current = onUseNow; }, [onUseNow]);
// ✅ setInterval + cleanup
useEffect(() => {
const tickId = setInterval(tick, 200);
return () => clearInterval(tickId);
}, [unit]);
// ✅ 事件监听 cleanup
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => { ... };
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
```
### 2.5 `memo` / `useCallback` / `useMemo` 使用
| 场景 | 使用方式 |
| ---------------------------------- | ------------- |
| 高频渲染组件(列表子项、实时时钟) | `React.memo` |
| 事件处理函数、回调引用 | `useCallback` |
| 响应式计算管线、衍生数据 | `useMemo` |
| 避免重复创建对象/集合 | `useMemo` |
```typescript
// ✅ React.memo — 高频更新组件
const LiveClock = React.memo(({ ... }) => { ... });
const ResultView = React.memo(({ ... }) => { ... });
// ✅ useCallback — 事件处理
const handleUseNow = useCallback((now: number) => {
if (mode === 'ts2dt') {
setInput(String(unit === 'ms' ? now : Math.floor(now / 1000)));
} else {
setInput(dayjs(now).tz(zone).format(DATE_FORMAT));
}
}, [mode, unit, zone]);
// ✅ useMemo — 避免重复创建集合
const visibleSet = useMemo(() => new Set<string>(visiblePages), [visiblePages]);
```
---
## 3. 导入规范
### 3.1 导入顺序
按来源分组,顺序如下:
1. React 核心
2. 第三方库(图标、UI 库等)
3. 业务 Provider / Context
4. 配置 / 存储
5. 本地页面组件
6. UI 组件
7. 工具函数 / Hook
8. 类型
9. 常量
```typescript
// 1. React 核心
import React, { useEffect, useMemo, useRef, useState } from 'react';
// 2. 第三方库
import { ArrowLeft, ExternalLink, Globe } from 'lucide-react';
import { toast } from 'sonner';
// 3. 业务 Provider
import { useRouter } from '@/providers/RouterProvider';
import { useThemeMode } from '@/providers/ThemeModeProvider';
// 4. 配置 / 存储
import { FeatureConfig, FEATURES } from '@/config/features';
import { storageUtil } from '@/utils/chromeStorage';
// 5. 本地组件
import TextMode from './TextMode';
import { ZONES } from './constants';
// 6. UI 组件
import SwitchButtonGroup from '@/components/SwitchButtonGroup';
import { Button } from '@/components/ui/button';
// 7. 工具函数 / Hook
import { cn } from '@/lib/utils';
import { useStorageState } from '@/utils/useStorageState';
// 8. 类型
import type { PageType, StorageSchema } from '@/types/storage';
```
### 3.2 路径别名
- `@/` 映射到项目根目录
- **跨目录导入**:使用 `@/` 绝对别名
- **同目录导入**:使用相对路径 `./`
```typescript
// ✅ 绝对别名导入 — 跨目录
import { cn } from '@/lib/utils';
import { storageUtil } from '@/utils/chromeStorage';
import type { StorageSchema } from '@/types/storage';
import { Button } from '@/components/ui/button';
// ✅ 相对导入 — 仅限同目录
import TextMode from './TextMode';
import { ZONES } from './constants';
import { useTimestampConverter } from './useTimestampConverter';
```
---
## 4. 样式规范
### 4.1 `cn()` 工具函数
统一使用 `cn()` 合并 Tailwind 类名(来自 `clsx` + `tailwind-merge`),导入自 `@/lib/utils`
```typescript
import { cn } from '@/lib/utils';
// ✅ 条件类名 + 合并外部 className
<div className={cn(
'flex items-center gap-3 px-3 h-10 rounded-lg border border-border/80 bg-secondary/50',
className, // 外部传入的覆盖
)} {...props}>
// ✅ 错误状态变体
<Input className={cn(
'font-mono font-semibold h-10 shadow-sm placeholder:text-muted-foreground/60',
error && 'border-destructive focus-visible:ring-destructive',
)} />
// ✅ 选中/未选中状态
className={cn(
'flex-1 inline-flex items-center justify-center font-medium whitespace-nowrap transition-all',
sizeClasses[size],
isSelected
? 'bg-background text-foreground shadow-sm font-semibold'
: 'hover:bg-background/50 hover:text-foreground/80',
buttonClassName,
)}
```
### 4.2 主题 / 暗色模式
使用 shadcn/ui 的 CSS 变量语义化类名,**禁止硬编码颜色值**:
```typescript
// ✅ 语义化颜色 token — 亮/暗模式自适应
<div className="min-h-screen bg-background text-foreground antialiased selection:bg-primary/20">
<div className="p-5 rounded-xl border border-border bg-card text-card-foreground shadow-sm">
// ✅ 暗色模式特殊处理
'fixed ... bg-white dark:bg-gray-900 p-6 ...'
// ✅ 需要固定颜色的特殊场景(如二维码白色背景保护)
<div className="p-3 bg-white rounded-lg shadow-sm border border-border/40">
```
**常用语义化 token**
| 用途 | 类名 |
| ---- | ------------------------------------------------------------------ |
| 背景 | `bg-background`, `bg-card`, `bg-muted`, `bg-secondary` |
| 文字 | `text-foreground`, `text-card-foreground`, `text-muted-foreground` |
| 边框 | `border-border`, `border-border/80` |
| 主色 | `text-primary`, `bg-primary`, `border-primary` |
| 危险 | `text-destructive`, `bg-destructive`, `border-destructive` |
### 4.3 响应式设计
移动优先,使用 `sm:` / `md:` / `lg:` 断点:
```typescript
// ✅ Grid 自适应布局
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 items-stretch">
// ✅ Dashboard 自动填充网格
<div className="grid grid-cols-1 sm:grid-cols-[repeat(auto-fill,minmax(290px,1fr))] auto-rows-auto gap-3.5 p-3.5 w-full h-auto">
// ✅ 弹性方向切换
<div className="flex flex-col sm:flex-row items-stretch gap-3 w-full">
// ✅ 内边距响应式
<div className="p-4 sm:p-6 space-y-4">
```
---
## 5. 错误处理
### 5.1 工具函数:可恢复错误返回可判断结果
可恢复的解析/校验错误应返回可判断的结果,避免工具层直接弹 Toast。确需保留底层异常的函数
(如 `formatJson` / `minifyJson`)必须在页面 Hook 或 UI 层捕获并转换为用户提示:
```typescript
// ✅ 可恢复校验返回错误消息,调用方据此展示 UI
export function validateJson(text: string): string | null {
if (!text.trim()) {
return null;
}
try {
JSON.parse(text.trim());
return null;
} catch (e) {
return e instanceof SyntaxError ? e.message : 'Invalid JSON';
}
}
```
### 5.2 UI 层:try-catch + Toast
UI 层异步操作使用 try-catch,通过 `sonner``toast` 显示错误:
```typescript
import { toast } from 'sonner';
const handleCopy = useCallback(async () => {
try {
await navigator.clipboard.writeText(value);
toast.success('复制成功');
} catch {
toast.error('复制失败');
}
}, [value]);
```
### 5.3 Promise 异常隔离
对不关心返回值的异步操作,使用 `void` + `.catch()` 隔离异常:
```typescript
// ✅ void + .catch 模式
void storageUtil.set(THEME_MODE_KEY, next).catch((err) => {
console.error('[Theme Storage Error] Failed to persistent theme state:', err);
});
// ✅ async 函数调用 + .catch
loadConfig().catch(console.error);
```
### 5.4 ErrorBoundary
在应用顶层使用 `ErrorBoundary` 组件捕获子组件树异常:
```typescript
import ErrorBoundary from '@/components/ErrorBoundary';
<ErrorBoundary>
<RouterContainer />
</ErrorBoundary>
```
---
## 6. 命名规范
### 6.1 文件命名
| 类型 | 命名模式 | 示例 |
| ----------- | -------------------------- | -------------------------------------------------------- |
| 页面目录 | PascalCase | `Timestamp/`, `Base64Converter/`, `StorageCleaner/` |
| 页面入口 | `index.tsx` | `src/pages/Timestamp/index.tsx` |
| 组件文件 | PascalCase `.tsx` | `TopBar.tsx`, `LiveClock.tsx`, `ResultView.tsx` |
| 自定义 Hook | camelCase `.ts` | `useTimestampConverter.ts`, `useStorageCleaner.ts` |
| 工具函数 | camelCase `.ts` | `chromeStorage.ts`, `base64Converter.ts`, `clipboard.ts` |
| 测试文件 | 与源文件同名 `.test.ts(x)` | `jwt.test.ts`, `SwitchButtonGroup.test.tsx` |
| 类型文件 | camelCase `.d.ts` | `storage.d.ts` |
| 常量文件 | camelCase `.ts` | `constants.ts` |
### 6.2 变量 / 函数命名
```typescript
// ✅ camelCase — 变量、函数、Hook
const conversionPipeline = useMemo(...);
const handleUseNow = useCallback(...);
export function useTimestampConverter(): UseTimestampConverterReturn { ... }
export function textToBase64(text: string): TextToBase64Result { ... }
// ✅ PascalCase — 组件、类型、接口
const LiveClock = React.memo(...);
export interface GlobalSnackbarProps { ... }
export type ThemeMode = 'light' | 'dark' | 'system';
// ✅ SCREAMING_SNAKE_CASE — 常量
const SEARCH_HISTORY_LIMIT = 10;
const THEME_MODE_KEY = 'app/themeMode' as const;
export const MAX_FILE_SIZE = 10 * 1024 * 1024;
export const SUPPORTED_IMAGE_TYPES = [...] as const;
// ✅ 布尔值 — is/has/should 前缀
const isControlled = controlledValue !== undefined;
const isDashboard = currentPage === 'dashboard';
const hasError = true;
```
### 6.3 事件处理函数
使用 `handle` 前缀命名组件内事件处理函数:
```typescript
const handleUseNow = useCallback((now: number) => { ... }, []);
const handleSelectFeature = (feature: FeatureConfig) => { ... };
const handleFileChange = useCallback((file: File) => { ... }, []);
const handleClean = useCallback(async () => { ... }, []);
```
---
## 7. 测试规范
### 7.1 文件组织
测试文件放在源代码同级的 `__tests__/` 目录下:
```
src/utils/__tests__/jwt.test.ts
src/utils/__tests__/base64Converter.test.ts
src/utils/__tests__/useStorageState.test.ts
src/components/__tests__/SwitchButtonGroup.test.tsx
src/components/__tests__/ErrorBoundary.test.tsx
src/pages/Timestamp/__tests__/index.test.tsx
```
### 7.2 describe / it 命名
`describe` 使用模块/函数名,`it` 使用中文描述行为("应该..."):
```typescript
// ✅ 中文 describe + 中文 it
describe('textToBase64', () => {
it('应该编码 ASCII 文本', () => { ... });
it('应该编码中文文本', () => { ... });
it('应该编码空字符串', () => { ... });
});
// ✅ 中文 describe + 中文 it(组件测试)
describe('SwitchButtonGroup 组件', () => {
it('应渲染所有选项按钮', () => { ... });
it('应高亮当前选中的按钮', () => { ... });
it('点击未选中按钮时应触发 onChange 并传入选中值', () => { ... });
});
```
### 7.3 Mock 模式
- 使用 `vi.mock()` 进行模块级 Mock
- 使用 `vi.fn()` 进行函数级 Mock
- 使用 `vi.useFakeTimers()` 控制时间
- **避免重复 mock `vitest.setup.ts` 中已有的内容**chrome API、matchMedia 等)
```typescript
// ✅ 模块级 Mock
vi.mock('@/utils/chromeStorage', () => ({
storageUtil: {
get: vi.fn(),
set: vi.fn(() => Promise.resolve()),
},
}));
// ✅ 函数级 Mock + 断言
const handleChange = vi.fn();
render(<SwitchButtonGroup value="a" options={options} onChange={handleChange} />);
fireEvent.click(screen.getByRole('button', { name: /选项B/i }));
expect(handleChange).toHaveBeenCalledTimes(1);
expect(handleChange).toHaveBeenCalledWith('b');
// ✅ 定时器 Mock
beforeEach(() => {
vi.useFakeTimers();
vi.clearAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
vi.useRealTimers();
});
```
### 7.4 断言模式
使用 Testing Library 的 DOM 查询 + Vitest 匹配器:
```typescript
// ✅ 语义化查询
expect(screen.getByRole('button', { name: /选项A/i })).toBeInTheDocument();
expect(screen.getByTestId('normal-content')).toHaveTextContent('正常内容');
expect(screen.queryByText('糟糕,出了点问题')).not.toBeInTheDocument();
// ✅ CSS 类断言
expect(button).toHaveClass('bg-background', 'text-foreground', 'shadow-sm');
// ✅ 异步断言
await waitFor(() => {
expect(result.current[0]).toBe(false);
expect(result.current[2]).toBe(true);
});
// ✅ renderHook 测试自定义 Hook
const { result } = renderHook(() => useStorageState('qrCode/urlExpanded', true));
expect(result.current[0]).toBe(true);
```
---
## 8. 自定义 Hook 规范
### 8.1 命名和结构
- 使用 `use` 前缀命名
- 定义返回值接口类型
- 添加 JSDoc 注释
```typescript
// ✅ 完整的 Hook 结构
/**
* 自定义 Hook:处理右键菜单传递的数据
*
* @param options - 配置选项
* @example
* useContextMenuData({ featureKey: 'jwt', onData: handlePayload });
*/
export function useContextMenuData({ featureKey, onData }: UseContextMenuDataOptions): void {
const checkAndConsumeData = useCallback(async () => { ... }, [featureKey, onData]);
useEffect(() => { checkAndConsumeData(); }, [checkAndConsumeData]);
}
// ✅ 返回值接口定义
export interface UseTimestampConverterReturn {
mode: 'ts2dt' | 'dt2ts';
input: string;
result: string;
error: string;
setMode: (mode: 'ts2dt' | 'dt2ts') => void;
setInput: (value: string) => void;
handleUseNow: (now: number) => void;
}
export function useTimestampConverter(): UseTimestampConverterReturn { ... }
```
### 8.2 Hook 存放位置
- **全局通用 Hook**:放在 `src/utils/` 目录下
- **页面专属 Hook**:与页面组件同目录
```
src/utils/useStorageState.ts — Chrome Storage 状态持久化
src/utils/syncSnapshot.ts — localStorage 快照读取
src/utils/useContextMenuData.ts — 右键菜单数据
src/utils/useDebounce.ts — 防抖
src/pages/Timestamp/useTimestampConverter.ts — 页面级 Hook
src/pages/StorageCleaner/useStorageCleaner.ts — 页面级 Hook
```
---
## 9. UI 文案规范
项目已移除 `chrome.i18n`,所有 UI 文案直接在代码中使用中文。
### 9.1 功能元数据
功能名称与描述在 `config/featureMeta.ts``FEATURES` 数组中定义:
```typescript
{
key: 'timestamp',
label: '时间戳转换',
description: '日期与时间戳互转',
defaultVisible: true,
}
```
页面组件通过 `config/pageLoaders/` 按需加载,不在 `FEATURES` 中引用。
Dashboard 卡片、TopBar 搜索等功能从此处读取 `label` / `description`
### 9.2 页面与组件文案
- 页面标题、按钮、提示信息等直接在 JSX 或 `constants.ts` 中写中文
- 错误消息可在 Hook 中定义,或使用常量映射
- Toast 通知使用 `sonner``toast()`,文案写在调用处或常量中
```typescript
// ✅ 页面组件 — 直接写中文
export default function Index() {
return <h1 className="font-bold text-sm">时间戳转换</h1>;
}
// ✅ 常量文件 — 可复用文案
export const ERROR_MESSAGES = {
invalidInput: '输入格式无效',
conversionFailed: '转换失败',
} as const;
```
### 9.3 添加新功能文案
1.`config/features.tsx` 填写 `label``description`
2. 在页面组件、`constants.ts` 或 Hook 中编写 UI 文案
3. 扩展名称与描述在 `wxt.config.ts``manifest` 中维护
---
## 10. 存储规范
### 10.1 StorageSchema
所有 Chrome Storage 键必须在 `types/storage.d.ts``StorageSchema` 中声明:
```typescript
export interface StorageSchema {
'app/currentRoute': PageType;
'app/popupRoute': PageType;
'app/theme': string;
'app/themeMode': 'light' | 'dark' | 'system';
'storageCleaner/preferences': StorageCleanerPreferences;
// ...
}
```
### 10.2 存储操作
使用 `utils/chromeStorage.ts` 的类型安全封装:
```typescript
import { storageUtil } from '@/utils/chromeStorage';
import type { StorageSchema } from '@/types/storage';
// ✅ 读取
const theme = await storageUtil.get('app/theme', 'default');
// ✅ 写入
await storageUtil.set('app/theme', 'dark');
// ✅ 删除
await storageUtil.remove('app/theme');
```
### 10.3 持久化状态 Hook
使用 `useStorageState` 自动同步 Chrome Storage
```typescript
import { useStorageState } from '@/utils/useStorageState';
const [themeMode, setThemeMode, isInitialized] = useStorageState(
'app/themeMode',
'system',
isValidMode, // 可选的类型守卫
);
```
### 10.4 首屏快照与初始化防覆盖
Chrome Storage 读取是异步的。项目通过 `localStorage` 快照(键名 `snapshot/{storageKey}`)提供同步初始值,消除首屏闪烁。
| 模块 | 快照工具 | 防覆盖机制 |
| ------------------- | ------------------ | ----------------------------------------------------------------------------------------- |
| `RouterProvider` | `syncSnapshot.ts` | `canPersistRef`(加载成功后才写入)、`hasUserNavigatedRef`(用户导航后不被 storage 覆盖) |
| `useStorageState` | `syncSnapshot.ts` | `loadSucceededRef``userModifiedRef` 为 true 时才写入 |
| `ThemeModeProvider` | `themeSnapshot.ts` | `hasUserSetMode`(用户切换主题后不被 storage 覆盖) |
新增持久化状态时,应遵循相同模式:同步快照作初始 state → 异步加载 storage → 加载成功或用户修改后才允许写入。
---
## 11. 页面开发规范
### 11.1 目录结构(按复杂度分级)
#### 简单页面(单一功能,无子模式)
适用于 Timestamp、Jwt、TextStatistics、RightClickRestorer 等:
```
src/pages/FeatureName/
├── index.tsx # 页面入口组件(default export
├── useFeatureName.ts # 业务逻辑 Hook(命名导出)
├── constants.ts # 常量定义(可选,命名导出)
├── SubComponent.tsx # 子组件(可选,default export
└── __tests__/
└── index.test.tsx # 页面集成测试
```
#### 中等页面(含多个子模式/标签页切换)
适用于 Base64Converter、StorageCleaner 等:
```
src/pages/FeatureName/
├── index.tsx # 页面入口(模式路由 + 顶层布局)
├── useFeatureName.ts # 业务逻辑 Hook(命名导出)
├── SubModeA.tsx # 子模式组件
├── SubModeB.tsx # 子模式组件
├── SubComponent.tsx # 可复用子组件
└── __tests__/
├── index.test.tsx
└── SubModeA.test.tsx
```
#### 复杂页面(Context + 多组件协作)
适用于 QrCode、JsonTools 等:
```
src/pages/FeatureName/
├── index.tsx # 页面入口(Provider + 布局)
├── types.ts # 页面专属类型定义
├── constants.ts # 常量(可选)
├── contexts/ # React Context 定义
│ └── FeatureContext.ts
├── hooks/ # 页面专属 Hooks
│ └── useFeature.ts
├── components/ # 页面专属子组件
│ ├── PanelA.tsx
│ └── PanelB.tsx
└── __tests__/
├── index.test.tsx
└── useFeature.test.ts
```
#### 特殊情况(单个文件即可)
功能极简的页面(如 Dashboard),仅需 `index.tsx` 一个文件。当 `index.tsx` 超过 **150 行**时,应拆分为 UI + Hook 模式。
---
### 11.2 页面入口组件(`index.tsx`)规范
#### 组件命名
- 页面入口组件**统一使用 `Index` 作为函数名**,通过 `export default` 导出
- 使用 `export default function Index()` 而非匿名默认导出
- **禁止**混用 `XxxPage` 命名(当前 `RightClickRestorerPage``DashboardPage` 不合规范,应统一为 `Index`
```typescript
// ✅ 正确
export default function Index() { ... }
// ❌ 错误 — 命名不一致
export default function RightClickRestorerPage() { ... }
export default function DashboardPage() { ... }
```
#### 组件职责
`index.tsx` 只负责两件事:
1. **调用业务 Hook** 获取状态和操作方法
2. **渲染 UI 布局**(纯展示,无业务逻辑)
```typescript
// ✅ 标准页面入口模板
import { useFeatureName } from './useFeatureName';
export default function Index() {
const { state, actions } = useFeatureName();
return (
<div className="p-4 w-full flex flex-col space-y-4 select-none">
{/* 纯 UI 渲染,文案直接写中文 */}
</div>
);
}
```
#### 禁止在 `index.tsx` 中编写的内容
-`useState` / `useMemo` / `useCallback`(应放在 Hook 中)
- ❌ 数据转换/格式化逻辑
- ❌ 异步请求/副作用
- ❌ 超过 3 行的条件判断逻辑
---
### 11.3 业务 Hook 规范(`useFeatureName.ts`
#### 命名
- 文件名:`useXxx.ts`(驼峰命名)
- Hook 函数名:`useXxx()`
- 返回值接口:`UseXxxReturn`
```typescript
// ✅ 标准 Hook 结构
export interface UseTimestampConverterReturn {
mode: 'ts2dt' | 'dt2ts';
input: string;
result: string;
error: string;
setMode: (mode: 'ts2dt' | 'dt2ts') => void;
setInput: (value: string) => void;
handleUseNow: (now: number) => void;
}
export function useTimestampConverter(): UseTimestampConverterReturn {
// 所有业务逻辑在此
}
```
#### Hook 内部结构(推荐顺序)
```typescript
export function useFeatureName(): UseFeatureNameReturn {
// 1. 基础 stateuseState
const [mode, setMode] = useState<Mode>('default');
const [input, setInput] = useState('');
// 2. 持久化 stateuseStorageState
const [pageMode, setPageMode] = useStorageState('feature/pageMode', 'default', isValidMode);
// 3. 衍生数据(useMemo)— 响应式计算管线
const result = useMemo(() => {
// 自动计算,无需手动点击"转换"按钮
}, [input, mode]);
// 4. 事件处理(useCallback
const handleAction = useCallback(() => { ... }, [deps]);
// 5. 副作用(useEffect)— 防抖、初始化、清理
useEffect(() => { ... }, [deps]);
// 7. 右键菜单数据(页面需要时)
useContextMenuData({ featureKey: 'featureName', onData: handleContextMenuData });
// 8. 返回
return { mode, input, result, setMode, setInput, handleAction };
}
```
#### 防抖模式
当输入框需要防抖时,在 Hook 中实现:
```typescript
// ✅ 防抖管道 — 在 useMemo 前定义
const [input, setInput] = useState('');
const [debouncedInput, setDebouncedInput] = useState('');
useEffect(() => {
const handle = setTimeout(() => setDebouncedInput(input), 250);
return () => clearTimeout(handle);
}, [input]);
// 后续 useMemo 使用 debouncedInput 而非 input
const result = useMemo(() => compute(debouncedInput), [debouncedInput]);
```
---
### 11.4 常量文件规范(`constants.ts`
- 仅在常量超过 **3 个**或需要**导出类型**时创建
- 使用 `as const` 确保字面量类型
-`as const` 数组派生联合类型
```typescript
// ✅ 标准常量文件
export const DATE_FORMAT = 'YYYY/MM/DD HH:mm:ss';
export const ZONES = ['Asia/Shanghai', 'America/New_York', 'Europe/London'] as const;
export type UnitType = 'ms' | 's';
export type ZoneType = (typeof ZONES)[number];
```
---
### 11.5 子组件规范
#### 何时拆分子组件
- `index.tsx` 超过 **150 行**
- 存在可复用的 UI 片段(如卡片、面板、结果展示区)
- 需要 `React.memo` 优化的高频渲染区域
#### 子组件 Props 模式
```typescript
// ✅ 继承 HTML 属性 + 业务 Props
interface ResultViewProps extends React.HTMLAttributes<HTMLDivElement> {
result: string;
mode: 'ts2dt' | 'dt2ts';
showEmptyPlaceholder?: boolean;
}
// ✅ 使用 React.memo + displayName
const ResultView = React.memo(
({ result, mode, showEmptyPlaceholder = false, className, ...props }: ResultViewProps) => {
// 文案直接写中文或使用 constants
// ...
},
);
ResultView.displayName = 'ResultView';
export default ResultView;
```
#### 子组件内可以使用 Hook
子组件可以独立调用 `useRouter``useThemeMode``toast` 等全局 Hook/API**不需要**通过 props 从父组件传递。
---
### 11.6 存储键命名规范
页面使用的 Storage 键必须遵循 kebab-case 格式:`{功能名}/{用途}`
```typescript
// ✅ 正确
'base64Converter/pageMode';
'base64Converter/fileMode/direction';
'jsonTools/pageMode';
'qrCode/urlExpanded';
// ❌ 错误
'base64ConverterPageMode';
'json_tools_page_mode';
```
`types/storage.d.ts``StorageSchema` 中声明所有键。
---
### 11.7 模式切换通用模式
当页面有多个子模式(标签页切换),统一使用以下模式:
```typescript
// ✅ 标准模式切换
const VALID_MODES = ['modeA', 'modeB'] as const;
type PageMode = (typeof VALID_MODES)[number];
const isValidMode = (val: unknown): val is PageMode =>
typeof val === 'string' && (VALID_MODES as readonly string[]).includes(val);
export default function Index() {
const [pageMode, setPageMode] = useStorageState('feature/pageMode', 'modeA', isValidMode);
return (
<div className="p-4 w-full flex flex-col space-y-4 select-none">
<SwitchButtonGroup
value={pageMode}
options={[
{ value: 'modeA', label: '模式 A' },
{ value: 'modeB', label: '模式 B' },
]}
onChange={(v: PageMode) => setPageMode(v)}
size="small"
/>
{pageMode === 'modeA' ? <PanelA /> : <PanelB />}
</div>
);
}
```
---
### 11.8 页面布局约定
- 所有页面根元素使用统一的外层容器:
```
<div className="p-4 w-full flex flex-col space-y-4 select-none">
```
- 不需要 `<div className="min-h-screen bg-background ...">` — 该样式已由 `AppRoot` 提供
- 不需要 `min-h-[500px]` 或固定高度(除非确有必要)
- 卡片容器:`rounded-xl border border-border bg-card text-card-foreground shadow-sm`
- 使用 `space-y-4` 管理纵向间距,不要手动 `mb-4`
---
### 11.9 页面开发检查清单
新增功能页面时,逐项确认:
1. ✅ 在 `types/storage.d.ts` 添加 `PageType` 联合类型
2. ✅ 在 `config/features.tsx` 注册 `FEATURES` 配置(key、label、description、icon、三种渲染模式组件)
3. ✅ 创建页面目录,使用 `Index` 作为组件名
4. ✅ 业务逻辑提取到 `useXxx.ts` Hookindex.tsx 不超过 150 行)
5. ✅ 需要持久化的 UI 状态使用 `useStorageState`
6. ✅ 常量 ≥3 个时提取到 `constants.ts`
7. ✅ 在页面组件或 `constants.ts` 中编写 UI 文案
8. ✅ 创建 `__tests__/index.test.tsx` 测试文件
9. ✅ 如需新权限,更新 `wxt.config.ts` 的 `manifest.permissions`
10. ✅ 运行 `npm run lint && npm run typecheck && npm run test` 全部通过
---
### 11.10 目录职责总览
| 目录 | 职责 |
| -------------------- | ------------------------------------------------------------ |
| `src/config/` | 应用配置(功能定义、路由映射) |
| `src/entrypoints/` | 扩展入口点(popup、options、sidepanel、background、content |
| `src/pages/` | 功能页面组件(懒加载) |
| `src/components/` | 可复用 UI 组件 |
| `src/components/ui/` | shadcn/ui 基础组件(button、dialog、select 等) |
| `src/providers/` | React ContextRouter、Theme 等) |
| `src/hooks/` | 自定义 React Hooks |
| `src/utils/` | 工具函数与服务抽象 |
| `src/types/` | TypeScript 类型声明 |
| `src/lib/` | 通用工具函数与生成器库(cn、utils、generators |
| `public/` | 静态资源(图标等) |
---
## 12. 代码风格
### 12.1 Prettier 配置
- 行宽:100 字符
- 引号:单引号
- 尾逗号:all
- 换行符:LF
- 分号:是
### 12.2 ESLint 规则
- 禁止使用 `any`(测试文件除外)
- 未使用变量/参数:使用 `_` 前缀(如 `_unused`
- React 19 JSX Runtime:无需手动导入 React
- 使用 `typescript-eslint` 的 `projectService: true`
### 12.3 注释规范
- **文件级注释**:使用 JSDoc `@module` 格式(如 `GlobalSnackbar.tsx`
- **函数注释**:使用 JSDoc,包含 `@param`、`@returns`、`@example`
- **行内注释**:仅在需要澄清复杂逻辑时使用
- **禁止注释显而易见的代码**