expo-popcore-app/.claude/skills/repo-sdk/references/client-usage.md

15 KiB
Raw Permalink Blame History

客户端使用指南

本文档详细介绍 @repo/sdk 的客户端调用方式及其配置方法。

目录

  1. 推荐方式root.get (DI 风格)
  2. 复用 Schema 和类型
  3. Better Auth 插件风格
  4. API 函数风格
  5. 服务端实现
  6. 错误处理

1. 推荐方式root.get (DI 风格)

强烈推荐使用此方式,保证完整的类型安全和最佳开发体验。

基本用法

import { root } from '@repo/core'
import {
  ProjectController,
  TemplateController,
  TemplateSocialController,
  AigcController,
  FileController
} from '@repo/sdk'
import type { CreateProjectInput, Project, ListProjectsResult } from '@repo/sdk'

// 获取控制器实例(完整类型推导)
const projectCtrl = root.get(ProjectController)
const templateCtrl = root.get(TemplateController)
const socialCtrl = root.get(TemplateSocialController)
const aigcCtrl = root.get(AigcController)
const fileCtrl = root.get(FileController)

// 调用方法(参数和返回值都有完整类型提示)
const projects: ListProjectsResult = await projectCtrl.list({ page: 1, limit: 10 })
const project: Project = await projectCtrl.create({ title: '新项目', content: {} })
const template = await templateCtrl.get('template-id')
await socialCtrl.like({ templateId: 'xxx' })

优势

  1. 完整类型安全:参数和返回值都有精确的类型推导
  2. IDE 支持:自动补全、跳转定义、重构支持
  3. 可测试性:可以轻松 mock Controller 进行单元测试
  4. 前后端一致:服务端和客户端使用相同的接口定义
  5. 灵活性:可以自定义代理实现(如添加缓存、日志等)

前置条件

需要先初始化客户端插件(注册 HTTP 代理):

import { createAuthClient } from 'better-auth/client'
import { createSkerClientPlugin } from '@repo/sdk'

// 初始化(会自动将 Controller 替换为 HTTP 代理)
const auth = createAuthClient({
  baseURL: 'http://localhost:3000',
  plugins: [createSkerClientPlugin()]
})

自定义代理(高级)

import { root } from '@repo/core'
import { ProjectController } from '@repo/sdk'
import type { ListProjectsInput, ListProjectsResult } from '@repo/sdk'

// 注册带缓存的自定义实现
root.set([{
  provide: ProjectController,
  useFactory: () => ({
    list: async (input: ListProjectsInput): Promise<ListProjectsResult> => {
      const cacheKey = `projects-${JSON.stringify(input)}`
      const cached = cache.get(cacheKey)
      if (cached) return cached

      const result = await originalProxy.list(input)
      cache.set(cacheKey, result)
      return result
    },
    // ... 其他方法
  })
}])

2. 复用 Schema 和类型

强烈推荐复用 SDK 导出的 Zod Schema 和 TypeScript 类型,确保前后端数据一致性。

导入类型(推荐)

// 始终从 @repo/sdk 导入类型,不要自己定义
import type {
  // 项目相关
  Project,
  CreateProjectInput,
  ListProjectsInput,
  ListProjectsResult,
  UpdateProjectInput,
  TransferProjectInput,

  // 模板相关
  TemplateDetail,
  CreateTemplateInput,
  RunTemplateInput,
  ListTemplatesInput,
  ListTemplatesResult,

  // AI 生成
  AigcModel,
  SubmitTaskBody,
  SubmitTaskResult,
  GetTaskStatusResult,

  // 社交
  LikeTemplateInput,
  CreateCommentInput,
  TemplateComment,
  GetCommentsResponse,

  // 消息
  Message,
  ListMessagesInput,
  UnreadCountResult,

  // 文件
  FileUploadResponse,
  VideoCompressRequest,
  ConvertToWebpRequest
} from '@repo/sdk'

复用 Zod Schema 验证

import {
  CreateProjectSchema,
  ListProjectsSchema,
  RunTemplateSchema,
  SubmitTaskBodySchema,
  CreateCommentSchema
} from '@repo/sdk'

// 验证用户输入(抛出错误)
function validateInput(data: unknown) {
  return CreateProjectSchema.parse(data)
}

// 安全验证(不抛错,返回结果对象)
function safeValidate(data: unknown) {
  const result = CreateProjectSchema.safeParse(data)
  if (!result.success) {
    console.error('验证失败:', result.error.flatten())
    return null
  }
  return result.data  // 类型安全的数据
}

// 部分验证(用于更新操作)
const PartialProjectSchema = CreateProjectSchema.partial()
function validatePartialUpdate(data: unknown) {
  return PartialProjectSchema.parse(data)
}

前端表单集成

import { CreateProjectSchema } from '@repo/sdk'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import { root } from '@repo/core'
import { ProjectController } from '@repo/sdk'

// 从 Schema 推导表单类型
type FormData = z.infer<typeof CreateProjectSchema>

function CreateProjectForm() {
  const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
    resolver: zodResolver(CreateProjectSchema)  // 复用 SDK Schema
  })

  const onSubmit = async (data: FormData) => {
    const projectCtrl = root.get(ProjectController)
    const project = await projectCtrl.create(data)  // 类型完全匹配
    console.log('创建成功:', project.id)
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('title')} />
      {errors.title && <span>{errors.title.message}</span>}
      {/* ... */}
    </form>
  )
}

服务端实现复用类型

import { Injectable } from '@repo/core'
import { ProjectController } from '@repo/sdk'
// 直接复用 SDK 导出的类型
import type {
  Project,
  CreateProjectInput,
  ListProjectsInput,
  ListProjectsResult,
  UpdateProjectInput
} from '@repo/sdk'

@Injectable()
export class ProjectControllerImpl implements ProjectController {
  constructor(
    private readonly db: DatabaseService,
    private readonly auth: AuthService
  ) {}

  // 参数和返回值类型直接复用 SDK 类型
  async create(data: CreateProjectInput): Promise<Project> {
    const userId = this.auth.getCurrentUserId()
    return this.db.project.create({
      data: { ...data, userId }
    })
  }

  async list(body: ListProjectsInput): Promise<ListProjectsResult> {
    // 实现...
  }

  async update(body: UpdateProjectInput): Promise<Project> {
    // 实现...
  }
}

3. Better Auth 插件风格

适用场景:已使用 Better Auth 进行身份认证的项目

安装和配置

import { createAuthClient } from 'better-auth/client'
import { createSkerClientPlugin } from '@repo/sdk'

// 创建带 Sker 插件的 Auth 客户端
const auth = createAuthClient({
  baseURL: 'http://localhost:3000',
  plugins: [createSkerClientPlugin()]
})

export { auth }

调用方式

// 项目管理
const projects = await auth.sker.project.list({ page: 1, limit: 10 })
const project = await auth.sker.project.create({ title: '新项目', content: {} })
const detail = await auth.sker.project.get('project-id')

// 模板管理
const templates = await auth.sker.template.list({ page: 1 })
const { generationId } = await auth.sker.template.run({ templateId: 'xxx', formData: {} })

// 社交功能
await auth.sker.templateSocial.like({ templateId: 'xxx' })
const comments = await auth.sker.templateSocial.getComments({ templateId: 'xxx' })

// AI 生成
const models = await auth.sker.aigc.getModels('image')
const { task_id } = await auth.sker.aigc.submitTask({ model_name: 'xxx', prompt: 'xxx' })

// 文件上传
const formData = new FormData()
formData.append('file', file)
const { data: fileUrl } = await auth.sker.file.uploadS3(formData)

插件工作原理

createSkerClientPlugin() 做了以下事情:

  1. 注册 HTTP 代理:将所有 Controller 替换为 HTTP 调用代理
  2. 提供 Better Auth 风格 API:通过 auth.sker.xxx 访问
  3. 自动处理认证:使用 Better Auth 的 session 进行认证
  4. 生成路径映射:自动从 Controller 元数据生成路由
// 插件内部实现简化版
export function createSkerClientPlugin(): BetterAuthClientPlugin {
  return {
    id: 'sker',
    getActions: ($fetch) => {
      // 注册代理到 DI 容器
      registerControllerProxies($fetch)
      // 返回 Better Auth 风格的 actions
      return buildBetterAuthActions(controllers, $fetch)
    },
    pathMethods: generatePathMethods(controllers)
  }
}

4. API 函数风格

推荐场景:不使用 Better Auth、需要独立的 API 客户端

配置客户端

import { client, generateApiFunctions } from '@repo/sdk'

// 配置基础 URL 和认证
client.setConfig({
  baseUrl: 'http://localhost:3000',
  auth: async () => {
    // 返回认证 token
    return localStorage.getItem('token') || ''
  }
})

// 生成所有 API 函数
const apis = generateApiFunctions()

调用方式

函数名格式:{httpMethod}{Path}(驼峰命名)

// POST /loomart/project/list -> postLoomartProjectList
const projects = await apis.postLoomartProjectList({ page: 1, limit: 10 })

// GET /loomart/template/get -> getLoomartTemplateGet
const template = await apis.getLoomartTemplateGet({ id: 'template-id' })

// POST /loomart/template/run -> postLoomartTemplateRun
const { generationId } = await apis.postLoomartTemplateRun({
  templateId: 'xxx',
  formData: {}
})

// GET /loomart/aigc/models -> getLoomartAigcModels
const models = await apis.getLoomartAigcModels({ category: 'image' })

// POST /loomart/file/upload-s3 -> postLoomartFileUploadS3
const formData = new FormData()
formData.append('file', file)
const result = await apis.postLoomartFileUploadS3(formData)

函数名映射规则

路由 HTTP 方法 函数名
/loomart/project/create POST postLoomartProjectCreate
/loomart/project/list POST postLoomartProjectList
/loomart/project/get GET getLoomartProjectGet
/loomart/template/run POST postLoomartTemplateRun
/loomart/aigc/task/submit POST postLoomartAigcTaskSubmit
/loomart/aigc/task/status GET getLoomartAigcTaskStatus
/loomart/file/upload-s3 POST postLoomartFileUploadS3

动态认证

client.setConfig({
  baseUrl: 'http://localhost:3000',
  auth: async (authContext) => {
    // 可以根据上下文动态获取 token
    if (authContext?.refreshToken) {
      // 刷新 token
      const newToken = await refreshAccessToken(authContext.refreshToken)
      return newToken
    }
    return localStorage.getItem('token') || ''
  }
})

5. 服务端实现

场景:在 Hono/Express 等后端框架中实现 Controller

注册控制器实现

import { root } from '@repo/core'
import { ProjectController } from '@repo/sdk'
import { ProjectControllerImpl } from './impl/project.controller.impl'

// 注册实现类
root.set([
  {
    provide: ProjectController,
    useClass: ProjectControllerImpl
  }
])

实现类示例

import { Injectable } from '@repo/core'
import { ProjectController } from '@repo/sdk'
import type {
  Project,
  CreateProjectInput,
  ListProjectsInput,
  ListProjectsResult
} from '@repo/sdk'

@Injectable()
export class ProjectControllerImpl implements ProjectController {
  constructor(
    private readonly db: DatabaseService,
    private readonly auth: AuthService
  ) {}

  async create(data: CreateProjectInput): Promise<Project> {
    const userId = this.auth.getCurrentUserId()

    const project = await this.db.project.create({
      data: {
        ...data,
        userId
      }
    })

    return project
  }

  async list(body: ListProjectsInput): Promise<ListProjectsResult> {
    const userId = this.auth.getCurrentUserId()

    const [projects, total] = await Promise.all([
      this.db.project.findMany({
        where: { userId, isDeleted: false },
        skip: ((body.page || 1) - 1) * (body.limit || 20),
        take: body.limit || 20,
        orderBy: { [body.sortBy || 'createdAt']: body.sortOrder || 'desc' }
      }),
      this.db.project.count({ where: { userId, isDeleted: false } })
    ])

    return {
      projects,
      total,
      page: body.page || 1,
      limit: body.limit || 20
    }
  }

  async get(id: string): Promise<Project> {
    const project = await this.db.project.findUnique({ where: { id } })
    if (!project) throw new Error('Project not found')
    return project
  }

  async update(body: UpdateProjectInput): Promise<Project> {
    return this.db.project.update({
      where: { id: body.id },
      data: body
    })
  }

  async delete(body: { id: string }): Promise<{ message: string }> {
    await this.db.project.update({
      where: { id: body.id },
      data: { isDeleted: true }
    })
    return { message: 'Project deleted' }
  }

  async transfer(body: TransferProjectInput): Promise<TransferProjectResult> {
    // 实现转移逻辑...
  }
}

与 Hono 集成

import { Hono } from 'hono'
import { root, CONTROLLES } from '@repo/core'

const app = new Hono()

// 自动注册所有控制器路由
const controllers = root.get(CONTROLLES, [])
for (const controller of controllers) {
  // 使用 @repo/core 的路由注册逻辑
  registerControllerRoutes(app, controller)
}

export default app

6. 错误处理

客户端错误处理

import { ProjectController } from '@repo/sdk'
import { root } from '@repo/core'

const projectCtrl = root.get(ProjectController)

try {
  const project = await projectCtrl.get('non-existent-id')
} catch (error) {
  if (error.message.includes('API error')) {
    // API 返回的错误
    console.error('API 错误:', error.message)
  } else {
    // 网络或其他错误
    console.error('请求失败:', error)
  }
}

统一错误处理

// 创建带错误处理的包装器
function withErrorHandling<T extends (...args: any[]) => Promise<any>>(fn: T): T {
  return (async (...args: Parameters<T>) => {
    try {
      return await fn(...args)
    } catch (error: any) {
      // 统一错误处理
      if (error.message.includes('401')) {
        // 未认证,跳转登录
        window.location.href = '/login'
        return
      }
      if (error.message.includes('403')) {
        // 无权限
        throw new Error('您没有权限执行此操作')
      }
      if (error.message.includes('404')) {
        throw new Error('资源不存在')
      }
      throw error
    }
  }) as T
}

// 使用
const safeList = withErrorHandling(projectCtrl.list.bind(projectCtrl))
const projects = await safeList({ page: 1 })

最佳实践总结

推荐做法

场景 推荐方式
调用 API root.get(Controller) - 完整类型安全
定义类型 @repo/sdk 导入,不要自己定义
验证数据 复用 SDK 导出的 Zod Schema
表单验证 使用 zodResolver(Schema)
服务端实现 implements Controller + 复用类型

避免做法

// ❌ 不要自己定义类型
interface MyProject {
  id: string;
  title: string;
}

// ✅ 从 SDK 导入类型
import type { Project } from '@repo/sdk'

// ❌ 不要自己写验证逻辑
if (!data.title || data.title.length < 1) {
  throw new Error('标题不能为空')
}

// ✅ 复用 SDK Schema
import { CreateProjectSchema } from '@repo/sdk'
CreateProjectSchema.parse(data)

// ❌ 不要使用 any 类型
const projectCtrl: any = root.get(ProjectController)

// ✅ 让 TypeScript 自动推导
const projectCtrl = root.get(ProjectController)  // 类型自动推导