mcpField
P
phase-6-ui-integration

phase-6-ui-integration

github.com/ww-w-ai开发60649 次安装4 周前更新
代码仓库

关于 phase-6-ui-integration

用于实现应用界面并集成后端 API 的开发指南,涵盖 UI 状态、API 客户端与服务层结构、错误处理和集成检查。

主要功能

  1. 01页面与功能实现指引
  2. 02API 客户端和服务层模式
  3. 03状态管理建议
  4. 04加载与错误处理
  5. 05API 集成检查清单

适用场景

  1. 01基于设计系统构建页面
  2. 02将 UI 功能连接到后端 API
  3. 03组织 API 调用和错误处理
  4. 04通过日志检查 UI 与 API 行为

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/ww-w-ai/bkit-claude-code --skill phase-6-ui-integration

安装后在 Claude Code 中输入/phase-6-ui-integration确认已加载。

详细介绍

阶段 6:UI 实现 + API 集成

实际 UI 实现和 API 集成

目的

使用设计系统组件实现实际页面,并与 API 集成。

本阶段要做什么

  1. 页面实现:开发每个屏幕
  2. 状态管理:处理客户端状态
  3. API 集成:调用后端 API
  4. 错误处理:处理加载和错误状态

交付物

src/
├── pages/              # Page components
│   ├── index.tsx
│   ├── login.tsx
│   └── ...
├── features/           # Feature-specific components
│   ├── auth/
│   ├── product/
│   └── ...
└── hooks/              # API call hooks
    ├── useAuth.ts
    └── useProducts.ts

docs/03-analysis/
└── ui-qa.md            # QA results

PDCA 应用

  • 计划:定义要实现的屏幕/功能
  • 设计:组件结构、状态管理设计
  • 执行:UI 实现 + API 集成
  • 检查:Zero Script QA
  • 改进:修复错误并进入阶段 7

各级别的应用方式

级别应用方式
入门级仅静态 UI(不集成 API)
动态级完整集成
企业级完整集成 + 优化

API 客户端架构

为什么需要集中式 API 客户端?
问题(分散的 API 调用)解决方案(集中式客户端)
错误处理逻辑重复通用错误处理程序
身份验证令牌处理分散自动注入令牌
响应格式不一致标准化响应类型
端点变更时需要多处修改单点管理
测试/模拟困难易于替换模拟对象
3 层 API 客户端结构
┌─────────────────────────────────────────────────────────┐
│                    UI Components                         │
│              (pages, features, hooks)                    │
├─────────────────────────────────────────────────────────┤
│                    Service Layer                         │
│         (Domain-specific API call functions)             │
│    authService, productService, orderService, ...        │
├─────────────────────────────────────────────────────────┤
│                    API Client Layer                      │
│         (Common settings, interceptors, error handling)  │
│              apiClient (axios/fetch wrapper)             │
└─────────────────────────────────────────────────────────┘
文件夹结构
src/
├── lib/
│   └── api/
│       ├── client.ts           # API client (axios/fetch wrapper)
│       ├── interceptors.ts     # Request/response interceptors
│       └── error-handler.ts    # Error handling logic
├── services/
│   ├── auth.service.ts         # Auth-related APIs
│   ├── product.service.ts      # Product-related APIs
│   └── order.service.ts        # Order-related APIs
├── types/
│   ├── api.types.ts            # Common API types
│   ├── auth.types.ts           # Auth domain types
│   └── product.types.ts        # Product domain types
└── hooks/
    ├── useAuth.ts              # Hooks using Service
    └── useProducts.ts

API 客户端实现

1. 基础 API 客户端 (lib/api/client.ts)
// lib/api/client.ts
import { ApiError, ApiResponse } from '@/types/api.types';

const BASE_URL = process.env.NEXT_PUBLIC_API_URL || '/api';

interface RequestConfig extends RequestInit {
  params?: Record<string, string>;
}

class ApiClient {
  private baseUrl: string;

  constructor(baseUrl: string) {
    this.baseUrl = baseUrl;
  }

  private async request<T>(
    endpoint: string,
    config: RequestConfig = {}
  ): Promise<ApiResponse<T>> {
    const { params, ...init } = config;

    // URL parameter handling
    const url = new URL(`${this.baseUrl}${endpoint}`);
    if (params) {
      Object.entries(params).forEach(([key, value]) => {
        url.searchParams.append(key, value);
      });
    }

    // Default header settings
    const headers = new Headers(init.headers);
    if (!headers.has('Content-Type')) {
      headers.set('Content-Type', 'application/json');
    }

    // Automatic auth token injection
    const token = this.getAuthToken();
    if (token) {
      headers.set('Authorization', `Bearer ${token}`);
    }

    try {
      const response = await fetch(url.toString(), {
        ...init,
        headers,
      });

      return this.handleResponse<T>(response);
    } catch (error) {
      throw this.handleNetworkError(error);
    }
  }

  private async handleResponse<T>(response: Response): Promise<ApiResponse<T>> {
    const data = await response.json();

    if (!response.ok) {
      throw new ApiError(
        data.error?.code || 'UNKNOWN_ERROR',
        data.error?.message || 'An error occurred',
        response.status,
        data.error?.details
      );
    }

    return data as ApiResponse<T>;
  }

  private handleNetworkError(error: unknown): ApiError {
    if (error instanceof TypeError && error.message === 'Failed to fetch') {
      return new ApiError('NETWORK_ERROR', 'Please check your network connection.', 0);
    }
    return new ApiError('UNKNOWN_ERROR', 'An unknown error occurred.', 0);
  }

  private getAuthToken(): string | null {
    if (typeof window === 'undefined') return null;
    return localStorage.getItem('auth_token');
  }

  // HTTP method wrappers
  get<T>(endpoint: string, params?: Record<string, string>) {
    return this.request<T>(endpoint, { method: 'GET', params });
  }

  post<T>(endpoint: string, body?: unknown) {
    return this.request<T>(endpoint, {
      method: 'POST',
      body: JSON.stringify(body),
    });
  }

  put<T>(endpoint: string, body?: unknown) {
    return this.request<T>(endpoint, {
      method: 'PUT',
      body: JSON.stringify(body),
    });
  }

  patch<T>(endpoint: string, body?: unknown) {
    return this.request<T>(endpoint, {
      method: 'PATCH',
      body: JSON.stringify(body),
    });
  }

  delete<T>(endpoint: string) {
    return this.request<T>(endpoint, { method: 'DELETE' });
  }
}

export const apiClient = new ApiClient(BASE_URL);
2. 通用类型定义 (types/api.types.ts)
// types/api.types.ts

// ===== Standard API Response Format (matches Phase 4) =====

/** Success response */
export interface ApiResponse<T> {
  data: T;
  meta?: {
    timestamp: string;
    requestId?: string;
  };
}

/** Paginated response */
export interface PaginatedResponse<T> extends ApiResponse<T[]> {
  pagination: {
    page: number;
    limit: number;
    total: number;
    totalPages: number;
  };
}

/** Error response */
export interface ApiErrorResponse {
  error: {
    code: string;
    message: string;
    details?: Array<{
      field: string;
      message: string;
    }>;
  };
}

// ===== Error Class =====

export class ApiError extends Error {
  constructor(
    public code: string,
    message: string,
    public status: number,
    public details?: Array<{ field: string; message: string }>
  ) {
    super(message);
    this.name = 'ApiError';
  }

  /** Check if validation error */
  isValidationError(): boolean {
    return this.code === 'VALIDATION_ERROR' && !!this.details;
  }

  /** Check if auth error */
  isAuthError(): boolean {
    return this.status === 401 || this.code === 'UNAUTHORIZED';
  }

  /** Check if forbidden error */
  isForbiddenError(): boolean {
    return this.status === 403 || this.code === 'FORBIDDEN';
  }

  /** Check if not found error */
  isNotFoundError(): boolean {
    return this.status === 404 || this.code === 'NOT_FOUND';
  }
}

// ===== Common Error Codes =====

export const ERROR_CODES = {
  // Client errors
  VALIDATION_ERROR: 'VALIDATION_ERROR',
  UNAUTHORIZED: 'UNAUTHORIZED',
  FORBIDDEN: 'FORBIDDEN',
  NOT_FOUND: 'NOT_FOUND',
  CONFLICT: 'CONFLICT',

  // Server errors
  INTERNAL_ERROR: 'INTERNAL_ERROR',
  SERVICE_UNAVAILABLE: 'SERVICE_UNAVAILABLE',

  // Network errors
  NETWORK_ERROR: 'NETWORK_ERROR',
  TIMEOUT_ERROR: 'TIMEOUT_ERROR',
} as const;

export type ErrorCode = typeof ERROR_CODES[keyof typeof ERROR_CODES];

服务层模式

按领域拆分服务
// services/auth.service.ts
import { apiClient } from '@/lib/api/client';
import { User, LoginRequest, LoginResponse, SignupRequest } from '@/types/auth.types';

export const authService = {
  /** Login */
  login(credentials: LoginRequest) {
    return apiClient.post<LoginResponse>('/auth/login', credentials);
  },

  /** Signup */
  signup(data: SignupRequest) {
    return apiClient.post<User>('/auth/signup', data);
  },

  /** Logout */
  logout() {
    return apiClient.post<void>('/auth/logout');
  },

  /** Get current user info */
  getMe() {
    return apiClient.get<User>('/auth/me');
  },

  /** Refresh token */
  refreshToken() {
    return apiClient.post<LoginResponse>('/auth/refresh');
  },
};

// services/product.service.ts
import { apiClient } from '@/lib/api/client';
import { Product, ProductFilter, CreateProductRequest } from '@/types/product.types';
import { PaginatedResponse } from '@/types/api.types';

export const productService = {
  /** Get product list */
  getList(filter?: ProductFilter) {
    const params = filter ? {
      page: String(filter.page || 1),
      limit: String(filter.limit || 20),
      ...(filter.category && { category: filter.category }),
      ...(filter.search && { search: filter.search }),
    } : undefined;

    return apiClient.get<PaginatedResponse<Product>>('/products', params);
  },

  /** Get product details */
  getById(id: string) {
    return apiClient.get<Product>(`/products/${id}`);
  },

  /** Create product */
  create(data: CreateProductRequest) {
    return apiClient.post<Product>('/products', data);
  },

  /** Update product */
  update(id: string, data: Partial<CreateProductRequest>) {
    return apiClient.patch<Product>(`/products/${id}`, data);
  },

  /** Delete product */
  delete(id: string) {
    return apiClient.delete<void>(`/products/${id}`);
  },
};

错误处理模式

全局错误处理程序
// lib/api/error-handler.ts
import { ApiError, ERROR_CODES } from '@/types/api.types';
import { toast } from 'sonner'; // or another toast library

interface ErrorHandlerOptions {
  showToast?: boolean;
  redirectOnAuth?: boolean;
  customMessages?: Record<string, string>;
}

export function handleApiError(
  error: unknown,
  options: ErrorHandlerOptions = {}
): void {
  const { showToast = true, redirectOnAuth = true, customMessages = {} } = options;

  if (!(error instanceof ApiError)) {
    console.error('Unexpected error:', error);
    if (showToast) {
      toast.error('An unknown error occurred.');
    }
    return;
  }

  // Use custom message if available
  const message = customMessages[error.code] || error.message;

  // Handle by error type
  switch (error.code) {
    case ERROR_CODES.UNAUTHORIZED:
      if (redirectOnAuth && typeof window !== 'undefined') {
        localStorage.removeItem('auth_token');
        window.location.href = '/login';
      }
      break;

    case ERROR_CODES.FORBIDDEN:
      if (showToast) toast.error('You do not have permission.');
      break;

    case ERROR_CODES.NOT_FOUND:
      if (showToast) toast.error('The requested resource was not found.');
      break;

    case ERROR_CODES.VALIDATION_ERROR:
      // Validation errors are handled by form
      break;

    case ERROR_CODES.NETWORK_ERROR:
      if (showToast) toast.error('Please check your network connection.');
      break;

    default:
      if (showToast) toast.error(message);
  }

  // Error logging (development environment)
  if (process.env.NODE_ENV === 'development') {
    console.error(`[API Error] ${error.code}:`, {
      message: error.message,
      status: error.status,
      details: error.details,
    });
  }
}
在 Hooks 中处理错误
// hooks/useProducts.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { productService } from '@/services/product.service';
import { handleApiError } from '@/lib/api/error-handler';
import { ProductFilter } from '@/types/product.types';

export function useProducts(filter?: ProductFilter) {
  return useQuery({
    queryKey: ['products', filter],
    queryFn: () => productService.getList(filter),
    // Auto error handling
    throwOnError: false,
    meta: {
      errorHandler: (error: unknown) => handleApiError(error),
    },
  });
}

export function useCreateProduct() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: productService.create,
    onSuccess: () => {
      // Invalidate cache
      queryClient.invalidateQueries({ queryKey: ['products'] });
    },
    onError: (error) => {
      handleApiError(error, {
        customMessages: {
          CONFLICT: 'Product name already exists.',
        },
      });
    },
  });
}

客户端与服务端类型共享

保持类型一致性的方法
Method 1: Shared Package (Monorepo)
├── packages/
│   └── shared-types/       # Common types
│       ├── api.types.ts
│       ├── auth.types.ts
│       └── product.types.ts
├── apps/
│   ├── web/                # Frontend
│   └── api/                # Backend

Method 2: Auto-generate Types from API Spec
├── openapi.yaml            # OpenAPI spec
└── scripts/
    └── generate-types.ts   # Type auto-generation script

Method 3: tRPC / GraphQL CodeGen
└── Auto-infer types from schema
类型定义示例
// types/auth.types.ts (client-server shared)

export interface User {
  id: string;
  email: string;
  name: string;
  role: 'user' | 'admin';
  createdAt: string;
  updatedAt: string;
}

export interface LoginRequest {
  email: string;
  password: string;
}

export interface LoginResponse {
  user: User;
  token: string;
  expiresAt: string;
}

export interface SignupRequest {
  email: string;
  password: string;
  name: string;
  termsAgreed: boolean;
}

API 集成模式

基础模式 (fetch)
async function getProducts() {
  const response = await fetch('/api/products');
  if (!response.ok) throw new Error('Failed to fetch');
  return response.json();
}
React Query 模式
function useProducts() {
  return useQuery({
    queryKey: ['products'],
    queryFn: getProducts,
  });
}
SWR 模式
function useProducts() {
  return useSWR('/api/products', fetcher);
}

状态管理指南

Server state (API data) → React Query / SWR
Client state (UI state) → useState / useReducer
Global state (auth, etc.) → Context / Zustand
Form state → React Hook Form

Zero Script QA 应用

Validate UI behavior with logs:

[UI] Login button clicked
[STATE] isLoading: true
[API] POST /api/auth/login
[RESPONSE] { token: "...", user: {...} }
[STATE] isLoading: false, isLoggedIn: true
[NAVIGATE] → /dashboard
[RESULT] ✅ Login successful

API 集成检查清单

架构
  • 构建 API 客户端层

    • 集中式 API 客户端 (lib/api/client.ts)
    • 自动注入身份验证令牌
    • 通用请求头设置
  • 拆分服务层

    • 按领域划分服务文件(auth、product、order 等)
    • 每个服务仅使用 apiClient
  • 类型一致性

    • 通用 API 类型定义 (ApiResponse、ApiError)
    • 按领域划分类型定义 (Request、Response)
    • 确定服务端与客户端类型共享方式
错误处理
  • 错误代码标准化

    • 错误代码与阶段 4 的 API 规范一致
    • 为每个错误代码定义用户提示信息
  • 全局错误处理程序

    • 身份验证错误时重定向
    • 网络错误处理
    • Toast 通知
  • 表单验证错误处理

    • 显示字段对应的错误消息
    • 与服务端验证错误集成
编码规范
  • API 调用规则

    • 组件中不得直接调用 fetch
    • 必须遵循 hooks → services → apiClient 的顺序
    • 防止重复调用相同数据(缓存)
  • 命名规则

    • 服务:{domain}.service.ts
    • Hooks:use{Domain}{Action}.ts
    • 类型:{domain}.types.ts

模板

参见 templates/pipeline/phase-6-ui.template.md

下一阶段

阶段 7:SEO/安全 → 功能已完成,现在进行优化并加强安全性

评论

支持 Markdown