Files
testing-tool/.github/CODING_STANDARDS.md
T
2026-06-30 23:26:27 +08:00

35 KiB
Raw Permalink Blame History

代码编写规范

本文档定义了 Testing Tools 浏览器扩展项目的编码规范和最佳实践。所有代码贡献者应遵循这些规范以保持代码库的一致性和可维护性。

1. TypeScript 规范

1.1 类型定义:interface vs type

  • interface:用于组件 Props、对象结构、Context 类型等可扩展结构
  • type:用于联合类型、工具类型、不可扩展的类型别名
// ✅ 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 进行类型守卫:

// ✅ 泛型 + 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 强转:

// ✅ 类型守卫谓词函数
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
// ✅ 页面组件 — 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 要求)
// ✅ 标准页面组件
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
// ✅ 继承 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
// ✅ 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 中清除定时器
// ✅ 取消标志模式 — 防止异步竞态
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
// ✅ 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. 常量
// 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 路径别名

  • @/ 映射到项目根目录
  • 跨目录导入:使用 @/ 绝对别名
  • 同目录导入:使用相对路径 ./
// ✅ 绝对别名导入 — 跨目录
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

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 变量语义化类名,禁止硬编码颜色值

// ✅ 语义化颜色 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: 断点:

// ✅ 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 层捕获并转换为用户提示:

// ✅ 可恢复校验返回错误消息,调用方据此展示 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,通过 sonnertoast 显示错误:

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() 隔离异常:

// ✅ 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 组件捕获子组件树异常:

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 变量 / 函数命名

// ✅ 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 前缀命名组件内事件处理函数:

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 使用中文描述行为("应该..."):

// ✅ 中文 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 等)
// ✅ 模块级 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 匹配器:

// ✅ 语义化查询
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 注释
// ✅ 完整的 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.tsFEATURES 数组中定义:

{
  key: 'timestamp',
  label: '时间戳转换',
  description: '日期与时间戳互转',
  defaultVisible: true,
}

页面组件通过 config/pageLoaders/ 按需加载,不在 FEATURES 中引用。

Dashboard 卡片、TopBar 搜索等功能从此处读取 label / description

9.2 页面与组件文案

  • 页面标题、按钮、提示信息等直接在 JSX 或 constants.ts 中写中文
  • 错误消息可在 Hook 中定义,或使用常量映射
  • Toast 通知使用 sonnertoast(),文案写在调用处或常量中
// ✅ 页面组件 — 直接写中文
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 填写 labeldescription
  2. 在页面组件、constants.ts 或 Hook 中编写 UI 文案
  3. 扩展名称与描述在 wxt.config.tsmanifest 中维护

10. 存储规范

10.1 StorageSchema

所有 Chrome Storage 键必须在 types/storage.d.tsStorageSchema 中声明:

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 的类型安全封装:

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

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 loadSucceededRefuserModifiedRef 为 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 命名(当前 RightClickRestorerPageDashboardPage 不合规范,应统一为 Index
// ✅ 正确
export default function Index() { ... }

// ❌ 错误 — 命名不一致
export default function RightClickRestorerPage() { ... }
export default function DashboardPage() { ... }

组件职责

index.tsx 只负责两件事:

  1. 调用业务 Hook 获取状态和操作方法
  2. 渲染 UI 布局(纯展示,无业务逻辑)
// ✅ 标准页面入口模板
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
// ✅ 标准 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 内部结构(推荐顺序)

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 中实现:

// ✅ 防抖管道 — 在 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 数组派生联合类型
// ✅ 标准常量文件
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 模式

// ✅ 继承 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

子组件可以独立调用 useRouteruseThemeModetoast 等全局 Hook/API不需要通过 props 从父组件传递。


11.6 存储键命名规范

页面使用的 Storage 键必须遵循 kebab-case 格式:{功能名}/{用途}

// ✅ 正确
'base64Converter/pageMode';
'base64Converter/fileMode/direction';
'jsonTools/pageMode';
'qrCode/urlExpanded';

// ❌ 错误
'base64ConverterPageMode';
'json_tools_page_mode';

types/storage.d.tsStorageSchema 中声明所有键。


11.7 模式切换通用模式

当页面有多个子模式(标签页切换),统一使用以下模式:

// ✅ 标准模式切换
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.tsmanifest.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-eslintprojectService: true

12.3 注释规范

  • 文件级注释:使用 JSDoc @module 格式(如 GlobalSnackbar.tsx
  • 函数注释:使用 JSDoc,包含 @param@returns@example
  • 行内注释:仅在需要澄清复杂逻辑时使用
  • 禁止注释显而易见的代码