ui-package-standards.md 27 KB

UI包开发规范

版本信息

版本 日期 描述 作者
1.1 2025-12-04 添加Radix UI组件测试环境修复规范(基于故事008.007经验) James
1.0 2025-12-03 基于史诗008经验创建UI包规范 Claude Code

概述

UI包是独立的前端模块包,用于封装特定业务功能的React组件、API客户端和状态管理逻辑。每个UI包作为一个独立的npm包发布,可以被主应用或其他UI包引用。

包结构规范

标准目录结构

packages/<module-name>-ui/
├── package.json                    # 包配置
├── tsconfig.json                   # TypeScript配置
├── vite.config.ts                  # Vite构建配置
├── src/
│   ├── index.ts                    # 主入口文件
│   ├── components/                 # React组件
│   │   ├── <ComponentName>.tsx     # 组件实现
│   │   ├── <ComponentName>.test.tsx # 组件测试
│   │   └── index.ts                # 组件导出
│   ├── api/                        # API客户端
│   │   ├── <module>Client.ts       # RPC客户端管理器
│   │   └── index.ts                # API导出
│   ├── hooks/                      # 自定义Hooks
│   │   ├── use<HookName>.ts        # Hook实现
│   │   └── index.ts                # Hook导出
│   ├── types/                      # TypeScript类型定义
│   │   ├── index.ts                # 类型导出
│   │   └── <type>.ts               # 具体类型定义
│   └── utils/                      # 工具函数
│       └── index.ts                # 工具导出
├── __tests__/                      # 测试文件
│   └── integration/                # 集成测试
└── README.md                       # 包文档

package.json配置

{
  "name": "@d8d/<module-name>-ui",
  "version": "1.0.0",
  "description": "UI包描述",
  "type": "module",
  "main": "src/index.ts",
  "types": "src/index.ts",
  "exports": {
    ".": {
      "types": "./src/index.ts",
      "import": "./src/index.ts",
      "require": "./src/index.ts"
    },
    "./components": {
      "types": "./src/components/index.ts",
      "import": "./src/components/index.ts",
      "require": "./src/components/index.ts"
    },
    "./hooks": {
      "types": "./src/hooks/index.ts",
      "import": "./src/hooks/index.ts",
      "require": "./src/hooks/index.ts"
    },
    "./api": {
      "types": "./src/api/index.ts",
      "import": "./src/api/index.ts",
      "require": "./src/api/index.ts"
    }
  },
  "files": [
    "src"
  ],
  "scripts": {
    "build": "unbuild",
    "dev": "tsc --watch",
    "test": "vitest run",
    "test:watch": "vitest",
    "test:coverage": "vitest run --coverage",
    "lint": "eslint src --ext .ts,.tsx",
    "typecheck": "tsc --noEmit"
  },
  "dependencies": {
    "@d8d/shared-types": "workspace:*",
    "@d8d/shared-ui-components": "workspace:*",
    "@d8d/<module-name>-module": "workspace:*",
    "@hookform/resolvers": "^5.2.1",
    "@tanstack/react-query": "^5.90.9",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "date-fns": "^4.1.0",
    "hono": "^4.8.5",
    "lucide-react": "^0.536.0",
    "react": "^19.1.0",
    "react-dom": "^19.1.0",
    "react-hook-form": "^7.61.1",
    "sonner": "^2.0.7",
    "tailwind-merge": "^3.3.1",
    "zod": "^4.0.15"
  },
  "devDependencies": {
    "@testing-library/jest-dom": "^6.8.0",
    "@testing-library/react": "^16.3.0",
    "@testing-library/user-event": "^14.6.1",
    "@types/node": "^22.10.2",
    "@types/react": "^19.2.2",
    "@types/react-dom": "^19.2.3",
    "@typescript-eslint/eslint-plugin": "^8.18.1",
    "@typescript-eslint/parser": "^8.18.1",
    "eslint": "^9.17.0",
    "jsdom": "^26.0.0",
    "typescript": "^5.8.3",
    "unbuild": "^3.4.0",
    "vitest": "^4.0.9"
  },
  "peerDependencies": {
    "react": "^19.1.0",
    "react-dom": "^19.1.0"
  }
}

RPC客户端实现规范

客户端管理器模式

每个UI包必须实现一个ClientManager类来管理RPC客户端生命周期:

// src/api/<module>Client.ts
import { <module>Routes } from '@d8d/<module-name>-module';
import { rpcClient } from '@d8d/shared-ui-components/utils/hc'

export class <Module>ClientManager {
  private static instance: <Module>ClientManager;
  private client: ReturnType<typeof rpcClient<typeof <module>Routes>> | null = null;

  private constructor() {}

  public static getInstance(): <Module>ClientManager {
    if (!<Module>ClientManager.instance) {
      <Module>ClientManager.instance = new <Module>ClientManager();
    }
    return <Module>ClientManager.instance;
  }

  // 初始化客户端
  public init(baseUrl: string = '/'): ReturnType<typeof rpcClient<typeof <module>Routes>> {
    return this.client = rpcClient<typeof <module>Routes>(baseUrl);
  }

  // 获取客户端实例
  public get(): ReturnType<typeof rpcClient<typeof <module>Routes>> {
    if (!this.client) {
      return this.init()
    }
    return this.client;
  }

  // 重置客户端(用于测试或重新初始化)
  public reset(): void {
    this.client = null;
  }
}

// 导出单例实例
const <module>ClientManager = <Module>ClientManager.getInstance();

// 导出默认客户端实例(延迟初始化)
export const <module>Client = <module>ClientManager.get()

export {
  <module>ClientManager
}

API导出文件

// src/api/index.ts
export {
  <Module>ClientManager,
  <module>ClientManager,
  <module>Client
} from './<module>Client';

组件开发规范

组件结构

// src/components/<ComponentName>.tsx
import React from 'react';
import { useQuery, useMutation } from '@tanstack/react-query';
import { <module>ClientManager } from '../api/<module>Client';
import type { <Module>Response, <Module>SearchParams } from '../types';

interface <ComponentName>Props {
  // 组件属性定义
}

export const <ComponentName>: React.FC<<ComponentName>Props> = (props) => {
  // 使用客户端管理器获取客户端实例
  const client = <module>ClientManager.get();

  // 数据查询示例
  const { data, isLoading } = useQuery({
    queryKey: ['<module>', searchParams],
    queryFn: async () => {
      const res = await client.index.$get({
        query: {
          page: searchParams.page,
          pageSize: searchParams.limit,
          keyword: searchParams.search
        }
      });
      if (res.status !== 200) throw new Error('获取数据失败');
      return await res.json();
    }
  });

  // 数据变更示例
  const mutation = useMutation({
    mutationFn: async (data: CreateRequest) => {
      const res = await client.index.$post({ json: data });
      if (res.status !== 201) throw new Error('创建失败');
      return await res.json();
    },
    onSuccess: () => {
      // 成功处理
    },
    onError: (error) => {
      // 错误处理
    }
  });

  return (
    // 组件JSX
  );
};

export default <ComponentName>;

表单组件模式规范(基于史诗008经验)

1. 条件渲染独立Form组件

规范:当组件需要支持创建和编辑两种表单模式时,必须使用条件渲染两个独立的Form组件,避免在单个Form组件上动态切换props。

// ✅ 正确:条件渲染两个独立的Form组件
{isCreateForm ? (
  <Form {...createForm}>
    {/* 创建表单内容 */}
  </Form>
) : (
  <Form {...updateForm}>
    {/* 编辑表单内容 */}
  </Form>
)}

// ❌ 错误:在单个Form组件上动态切换props(可能导致类型不兼容)
<Form {...(isCreateForm ? createForm : updateForm)}>
  {/* 表单内容 */}
</Form>

2. 参考现有模式

规范:参考PlatformManagement.tsx的表单处理模式,确保一致性。

3. 表单状态管理

规范:创建表单和编辑表单分别使用独立的useForm实例,避免状态混淆。

const createForm = useForm({
  resolver: zodResolver(CreateSchema),
  defaultValues: createDefaultValues
});

const updateForm = useForm({
  resolver: zodResolver(UpdateSchema),
  defaultValues: updateDefaultValues
});

组件导出

// src/components/index.ts
export { <ComponentName> } from './<ComponentName>';
export type { <ComponentName>Props } from './<ComponentName>';

类型定义规范

类型文件结构

// src/types/index.ts
import type { InferResponseType, InferRequestType } from 'hono';
import type { <module>Routes } from '@d8d/<module-name>-module';
import { <module>Client } from '../api/<module>Client';

// 使用导出的client进行类型推导
export type <Module>Response = InferResponseType<typeof <module>Client.index.$get>;
export type Create<Module>Request = InferRequestType<typeof <module>Client.index.$post>;
export type Update<Module>Request = InferRequestType<typeof <module>Client[':id']['$put']>;

// 搜索参数类型
export interface <Module>SearchParams {
  page: number;
  limit: number;
  search?: string;
  // 其他搜索参数
}

// 组件属性类型
export interface <ComponentName>Props {
  // 属性定义
}

类型推断最佳实践(基于史诗008经验)

1. 使用RPC推断类型

规范:必须使用RPC推断类型,而不是直接导入schema类型,避免Date/string类型不匹配问题。

// ✅ 正确:使用RPC推断类型(推荐)
export type <Module>ListItem = <Module>ListResponse['data'][0];

// ❌ 错误:直接导入schema类型(可能导致Date/string不匹配)
import type { <Module> } from '@d8d/<module-name>-module/schemas';

2. 参考现有UI包模式

规范:参考现有UI包(如广告管理UI)的类型定义模式,确保一致性。

// 广告管理UI模式参考
export type AdvertisementResponse = InferResponseType<typeof advertisementClient.index.$get, 200>['data'][0];

3. 处理混合路由模式

规范:当模块使用自定义路由与CRUD路由混合时,必须通过查看后端模块集成测试确认正确的路由结构。

// 示例:渠道模块的getChannel路由结构
// 根据后台集成测试,路由结构是 getChannel[':id'].$get
export type ChannelDetailResponse = InferResponseType<typeof channelClient.getChannel[':id']['$get'], 200>;

4. 避免复杂的条件类型

规范:使用简单的类型索引而不是复杂的条件类型,提高代码可读性。

// ✅ 正确:简单类型索引
export type <Module>ListItem = <Module>ListResponse['data'][0];

// ❌ 避免:复杂的条件类型
export type <Module>Item = <Module>ListResponse extends { data: infer T } ? T extends Array<infer U> ? U : never : never;

状态管理规范

React Query配置

// 在组件中使用React Query进行状态管理
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

// 查询示例
const { data, isLoading, error, refetch } = useQuery({
  queryKey: ['<module>', id],
  queryFn: async () => {
    const res = await client[':id'].$get({ param: { id } });
    if (res.status !== 200) throw new Error('获取详情失败');
    return await res.json();
  },
  enabled: !!id, // 条件查询
});

// 变更示例
const queryClient = useQueryClient();
const mutation = useMutation({
  mutationFn: async (data: UpdateRequest) => {
    const res = await client[':id']['$put']({
      param: { id },
      json: data
    });
    if (res.status !== 200) throw new Error('更新失败');
    return await res.json();
  },
  onSuccess: () => {
    // 使相关查询失效
    queryClient.invalidateQueries({ queryKey: ['<module>'] });
    queryClient.invalidateQueries({ queryKey: ['<module>', id] });
  }
});

测试规范

测试文件结构

packages/<module-name>-ui/
├── tests/
│   ├── integration/                    # 集成测试
│   │   └── <component-name>.integration.test.tsx
│   ├── unit/                          # 单元测试
│   │   └── <hook-name>.test.tsx
│   └── components/                    # 组件测试
│       └── <ComponentName>.test.tsx

Mock响应工具函数

// 在测试文件中使用的标准mock响应函数
const createMockResponse = (status: number, data?: any) => ({
  status,
  ok: status >= 200 && status < 300,
  body: null,
  bodyUsed: false,
  statusText: status === 200 ? 'OK' : status === 201 ? 'Created' : status === 204 ? 'No Content' : 'Error',
  headers: new Headers(),
  url: '',
  redirected: false,
  type: 'basic' as ResponseType,
  json: async () => data || {},
  text: async () => '',
  blob: async () => new Blob(),
  arrayBuffer: async () => new ArrayBuffer(0),
  formData: async () => new FormData(),
  clone: function() { return this; }
});

测试选择器优化规范(基于史诗008经验)

1. 优先使用test ID

规范:必须为关键交互元素添加data-testid属性,避免使用文本查找导致的测试冲突。

// 在组件中添加test ID
<DialogTitle data-testid="create-<module>-modal-title">创建<Module></DialogTitle>
<Button data-testid="search-button">搜索</Button>

// 在测试中使用test ID
const modalTitle = screen.getByTestId('create-<module>-modal-title');
const searchButton = screen.getByTestId('search-button');

2. 避免文本选择器冲突

规范:当页面中有多个相同文本元素时,必须使用test ID代替getByText()

// ❌ 错误:可能找到错误的元素
const createButton = screen.getByText('创建');

// ✅ 正确:使用唯一的test ID
const createButton = screen.getByTestId('create-<module>-button');

3. 命名约定

规范:test ID命名使用kebab-case格式:{action}-{element}-{purpose}

// 示例命名
data-testid="create-channel-modal-title"
data-testid="edit-channel-button-1"
data-testid="delete-confirm-dialog-title"

4. Radix UI组件测试环境修复(基于故事008.007经验)

规范:在测试环境中使用Radix UI组件(特别是Select、DropdownMenu等)时,必须添加必要的DOM API mock。

问题:Radix UI组件在测试环境中可能缺少某些DOM API(如scrollIntoView),导致测试失败。

解决方案:在测试setup文件中添加必要的mock。

// tests/setup.ts
import '@testing-library/jest-dom';
import { vi } from 'vitest';

// Mock sonner
vi.mock('sonner', () => ({
  toast: {
    success: vi.fn(),
    error: vi.fn(),
    warning: vi.fn(),
    info: vi.fn()
  }
}));

// Mock scrollIntoView for Radix UI components
Element.prototype.scrollIntoView = vi.fn();

Select组件test ID规范:为Radix UI Select组件的选项添加test ID,避免文本查找冲突。

// 在组件中为SelectItem添加test ID
<SelectContent>
  <SelectItem value="all" data-testid="order-status-option-all">全部状态</SelectItem>
  <SelectItem value={OrderStatus.DRAFT} data-testid="order-status-option-draft">草稿</SelectItem>
  <SelectItem value={OrderStatus.CONFIRMED} data-testid="order-status-option-confirmed">已确认</SelectItem>
</SelectContent>

// 在测试中使用test ID查找Select选项
expect(screen.getByTestId('order-status-option-all')).toBeInTheDocument();
expect(screen.getByTestId('order-status-option-draft')).toBeInTheDocument();
expect(screen.getByTestId('order-status-option-confirmed')).toBeInTheDocument();

组件集成测试

// tests/integration/<component-name>.integration.test.tsx
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { <ComponentName> } from '../../src/components/<ComponentName>';
import { <module>ClientManager, <module>Client } from '../../src/api/<module>Client';

// Mock RPC客户端
vi.mock('../../src/api/<module>Client', () => {
  const mock<Module>Client = {
    index: {
      $get: vi.fn(() => Promise.resolve(createMockResponse(200, {
        data: [
          {
            id: 1,
            name: '测试数据',
            // 其他字段
          }
        ],
        pagination: {
          page: 1,
          pageSize: 10,
          total: 1,
          totalPages: 1
        }
      }))),
      $post: vi.fn(() => Promise.resolve(createMockResponse(201, {
        id: 2,
        name: '新建数据'
      }))),
    },
    ':id': {
      $get: vi.fn(() => Promise.resolve(createMockResponse(200, {
        id: 1,
        name: '测试数据详情'
      }))),
      $put: vi.fn(() => Promise.resolve(createMockResponse(200, {
        id: 1,
        name: '更新后的数据'
      }))),
      $delete: vi.fn(() => Promise.resolve(createMockResponse(204))),
    },
  };

  const mock<Module>ClientManager = {
    get: vi.fn(() => mock<Module>Client),
  };

  return {
    <module>ClientManager: mock<Module>ClientManager,
    <module>Client: mock<Module>Client,
  };
});

// Mock其他依赖
vi.mock('sonner', () => ({
  toast: {
    success: vi.fn(),
    error: vi.fn(),
    info: vi.fn(),
    warning: vi.fn(),
  }
}));

describe('<ComponentName>集成测试', () => {
  const queryClient = new QueryClient({
    defaultOptions: {
      queries: {
        retry: false,
      },
    },
  });

  beforeEach(() => {
    vi.clearAllMocks();
    queryClient.clear();
  });

  it('渲染组件并加载数据', async () => {
    render(
      <QueryClientProvider client={queryClient}>
        <<ComponentName> />
      </QueryClientProvider>
    );

    await waitFor(() => {
      expect(screen.getByText('测试数据')).toBeInTheDocument();
    });
  });

  it('创建新数据', async () => {
    render(
      <QueryClientProvider client={queryClient}>
        <<ComponentName> />
      </QueryClientProvider>
    );

    const createButton = screen.getByText('新建');
    fireEvent.click(createButton);

    const nameInput = screen.getByLabelText('名称');
    fireEvent.change(nameInput, { target: { value: '新数据' } });

    const submitButton = screen.getByText('提交');
    fireEvent.click(submitButton);

    await waitFor(() => {
      expect(<module>Client.index.$post).toHaveBeenCalledWith({
        json: expect.objectContaining({ name: '新数据' })
      });
    });
  });
});

Hook单元测试

// tests/unit/use<HookName>.test.tsx
import React from 'react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { renderHook, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { use<HookName> } from '../../src/hooks/use<HookName>';
import { <module>Client } from '../../src/api/<module>Client';

// Mock RPC客户端
vi.mock('../../src/api/<module>Client', () => {
  const mock<Module>Client = {
    index: {
      $get: vi.fn(() => Promise.resolve(createMockResponse(200, {
        data: [{ id: 1, name: '测试数据' }],
        pagination: { total: 1 }
      }))),
    },
  };

  return {
    <module>Client: mock<Module>Client,
  };
});

describe('use<HookName>', () => {
  const queryClient = new QueryClient({
    defaultOptions: {
      queries: {
        retry: false,
      },
    },
  });

  const wrapper = ({ children }: { children: React.ReactNode }) => (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  );

  beforeEach(() => {
    vi.clearAllMocks();
    queryClient.clear();
  });

  it('加载数据', async () => {
    const { result } = renderHook(() => use<HookName>({ page: 1, limit: 10 }), { wrapper });

    await waitFor(() => {
      expect(result.current.isLoading).toBe(false);
    });

    expect(result.current.data).toEqual([
      { id: 1, name: '测试数据' }
    ]);
    expect(<module>Client.index.$get).toHaveBeenCalledWith({
      query: { page: 1, pageSize: 10 }
    });
  });
});

组件单元测试

// tests/components/<ComponentName>.test.tsx
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import { <ComponentName> } from '../../src/components/<ComponentName>';

// Mock子组件
vi.mock('../ChildComponent', () => ({
  ChildComponent: () => <div>Mock子组件</div>
}));

describe('<ComponentName>', () => {
  it('渲染组件', () => {
    render(<<ComponentName> />);
    expect(screen.getByText('组件标题')).toBeInTheDocument();
  });

  it('显示传入的属性', () => {
    render(<<ComponentName> name="测试名称" />);
    expect(screen.getByText('测试名称')).toBeInTheDocument();
  });
});

构建和发布规范

构建配置

UI包在PNPM工作空间中直接使用TypeScript源码,不需要构建步骤。主入口直接指向src/index.ts,TypeScript会自动处理类型检查和编译。

TypeScript配置

{
  "compilerOptions": {
    "target": "ES2020",
    "lib": ["DOM", "DOM.Iterable", "ES2020"],
    "module": "ESNext",
    "skipLibCheck": true,
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true
  },
  "include": ["src"]
}

构建脚本说明

  • "build": "unbuild": 使用unbuild进行构建(可选,用于生产环境发布)
  • "dev": "tsc --watch": 开发模式下监听TypeScript文件变化
  • "typecheck": "tsc --noEmit": 类型检查,不生成输出文件

集成规范

在主应用中使用

// 主应用中的使用示例
import { <ComponentName> } from '@d8d/<module-name>-ui';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <<ComponentName> />
    </QueryClientProvider>
  );
}

环境配置

UI包应该支持以下环境变量配置:

  • VITE_API_BASE_URL: API基础URL(默认为/
  • VITE_APP_ENV: 应用环境(development/production)

错误处理规范

API错误处理

// 统一的错误处理模式
try {
  const res = await client.index.$get({ query: params });
  if (res.status !== 200) {
    const errorData = await res.json();
    throw new Error(errorData.message || '请求失败');
  }
  return await res.json();
} catch (error) {
  if (error instanceof Error) {
    toast.error(error.message);
  } else {
    toast.error('未知错误');
  }
  throw error;
}

组件错误边界

import React, { Component, ErrorInfo, ReactNode } from 'react';

interface Props {
  children: ReactNode;
  fallback?: ReactNode;
}

interface State {
  hasError: boolean;
  error?: Error;
}

export class ErrorBoundary extends Component<Props, State> {
  public state: State = {
    hasError: false
  };

  public static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error };
  }

  public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    console.error('UI组件错误:', error, errorInfo);
  }

  public render() {
    if (this.state.hasError) {
      return this.props.fallback || <div>组件加载失败</div>;
    }

    return this.props.children;
  }
}

开发流程规范

1. 开发前检查清单(基于史诗008经验)

在开始UI包开发前,必须完成以下检查:

1.1 API路径映射验证

规范:必须验证故事中的API路径映射与实际后端路由定义的一致性。

# 检查后端模块的路由定义
cat allin-packages/<module-name>-module/src/routes/*.routes.ts

# 查看后端集成测试确认路由结构
cat allin-packages/<module-name>-module/tests/integration/*.test.ts

1.2 路由结构确认

规范:必须通过查看后台模块集成测试确认正确的路由结构,特别是混合路由模式。

1.3 参考现有UI包

规范:必须参考现有UI包(如广告管理UI、平台管理UI)的实现模式。

2. API调用一致性规范

规范:必须根据实际路由名称修正API调用,确保前端API调用与后端路由定义完全一致。

// ❌ 错误:使用故事中描述但实际不存在的路由
const res = await client.index.$get(...);
const res = await client.channels.$get(...);

// ✅ 正确:使用实际路由名称
const res = await client.getAll<Module>s.$get(...);
const res = await client.search<Module>s.$get(...);

3. 创建新UI包

# 复制模板
cp -r packages/template-ui packages/<module-name>-ui

# 更新包名和依赖
cd packages/<module-name>-ui
# 修改package.json中的name和dependencies

2. 开发组件

# 启动开发模式
pnpm dev

# 运行测试
pnpm test

# 类型检查
pnpm typecheck

# 代码检查
pnpm lint

3. 构建和发布

# 构建包
pnpm build

# 本地测试
# 在主应用中引用本地构建的包进行测试

# 发布到npm(由CI/CD流程处理)

参考实现

现有UI包参考

  • 广告管理UI包: packages/advertisement-management-ui

    • 组件: src/components/AdvertisementManagement.tsx
    • API客户端: src/api/advertisementClient.ts
    • 类型定义: src/types/index.ts
  • 区域管理UI包: packages/area-management-ui

    • 组件: src/components/AreaManagement.tsx
    • API客户端: src/api/areaClient.ts

关键代码片段

// RPC客户端管理器实现参考
import { advertisementRoutes } from '@d8d/advertisements-module';
import { rpcClient } from '@d8d/shared-ui-components/utils/hc'

export class AdvertisementClientManager {
  private static instance: AdvertisementClientManager;
  private client: ReturnType<typeof rpcClient<typeof advertisementRoutes>> | null = null;

  public static getInstance(): AdvertisementClientManager {
    if (!AdvertisementClientManager.instance) {
      AdvertisementClientManager.instance = new AdvertisementClientManager();
    }
    return AdvertisementClientManager.instance;
  }

  public get(): ReturnType<typeof rpcClient<typeof advertisementRoutes>> {
    if (!this.client) {
      return this.init()
    }
    return this.client;
  }
}

版本管理

版本号规则

  • 主版本号: 不兼容的API变更
  • 次版本号: 向后兼容的功能性新增
  • 修订号: 向后兼容的问题修正

变更日志

每个版本更新必须包含变更日志,记录:

  1. 新增功能
  2. 问题修复
  3. 破坏性变更
  4. 依赖更新

性能优化

代码分割

// 使用React.lazy进行代码分割
const LazyComponent = React.lazy(() => import('./HeavyComponent'));

function App() {
  return (
    <React.Suspense fallback={<LoadingSpinner />}>
      <LazyComponent />
    </React.Suspense>
  );
}

组件优化

  • 使用React.memo避免不必要的重渲染
  • 使用useMemouseCallback缓存计算和函数
  • 实现虚拟列表处理大量数据

安全规范

输入验证

  • 所有用户输入必须在前端进行验证
  • 使用Zod schema进行表单验证
  • 敏感数据不存储在客户端状态中

XSS防护

  • 使用React的自动转义机制
  • 避免使用dangerouslySetInnerHTML
  • 对动态内容进行清理

文档要求

每个UI包必须包含:

  1. README.md: 包概述、安装、使用示例
  2. API文档: 组件Props和API接口说明
  3. 示例代码: 完整的使用示例
  4. 变更日志: 版本更新记录

本规范基于史诗008(AllIn UI模块移植)的经验总结,确保UI包开发的一致性和可维护性。