数据转换

package version >0.4.0

shadcn any version

author: cmtlyt

update time: 2026/04/08 15:42:00

支持请求和响应的数据转换,提供灵活的数据处理能力。

特性

  • 请求转换: 使用 tdto 在请求前转换数据
  • 响应转换: 使用 tvo 在响应后转换数据
  • 请求拦截: 使用 onRequest 拦截请求
  • 响应拦截: 使用 onResponse 拦截响应
  • 类型安全: 完整的 TypeScript 类型支持
  • 链式处理: 支持多个转换函数的链式调用

基础用法

请求前转换数据 (tdto)

import { createApi, defineApi } from '@cmtlyt/lingshu-toolkit/shared'

const getUserApi = defineApi({
  url: '/user',
  method: 'GET',
  // 请求前转换数据
  tdto(data: { id: string }) {
    return { id: Number(data.id) }
  },
})

const getUser = createApi(getUserApi, {
  baseUrl: 'https://api.example.com',
})

const result = await getUser({ id: '1' })
// 传递的 '1' 会被转换为数字 1

响应后转换数据 (tvo)

import { createApi, defineApi } from '@cmtlyt/lingshu-toolkit/shared'

const getUserApi = defineApi({
  url: '/user',
  method: 'GET',
  // 响应后转换数据
  tvo(data: any) {
    return { ...data, age: 18 } as { id: string; name: string; age: number }
  },
})

const getUser = createApi(getUserApi, {
  baseUrl: 'https://api.example.com',
})

const result = await getUser({ id: '1' })
// 响应数据会自动添加 age 字段
console.log(result) // { id: '1', name: 'John Doe', age: 18 }

高级用法

完整的数据转换流程

import { createApi, defineApi } from '@cmtlyt/lingshu-toolkit/shared'

const getUserApi = defineApi({
  url: '/user',
  method: 'GET',
  // 请求前转换数据
  tdto(data: { id: string }) {
    return { id: Number(data.id) }
  },
  // 请求拦截
  onRequest(req, config) {
    return req.json() as unknown as { id: number }
  },
  // 响应后转换数据
  tvo(data: any) {
    return { ...data, age: 18 } as { id: string; name: string; age: number }
  },
})

const getUser = createApi(getUserApi, {
  baseUrl: 'https://api.example.com',
  requestMode: 'mock',
})

const result = await getUser({ id: '1' })
console.log(result) // { id: '1', name: 'John Doe', age: 18 }

请求拦截 (onRequest)

network 模式下 onRequest 接收待发送的 Request,可以就地修改它,也可以返回一个新的 Request 来替换原请求:

import { createApi, defineApi } from '@cmtlyt/lingshu-toolkit/shared'

const getUserApi = defineApi({
  url: '/user',
  method: 'GET',
  onRequest(req, config) {
    // 就地修改 headers, 无需返回值
    req.headers.set('Authorization', 'Bearer token')
  },
})

const getUser = createApi(getUserApi, {
  baseUrl: 'https://api.example.com',
})

const result = await getUser({ id: '1' })

返回 Response 可以直接短路请求,不再发起真实网络请求(响应仍会走 onResponse/parser/tvo):

const getUserApi = defineApi({
  url: '/user',
  method: 'GET',
  onRequest(req, config) {
    const cached = sessionStorage.getItem('user')
    // 命中缓存则短路, 不发起网络请求
    if (cached) {
      return new Response(cached)
    }
  },
})

带 body 的请求若在 hook 中读取过 body,需要基于 url 重建 Request,不能直接用原 Request 作为构造参数(其 body 流已被消费):

const createUserApi = defineApi({
  url: '/user',
  method: 'POST',
  async onRequest(req, config) {
    const body = await req.json()
    return new Request(req.url, {
      method: req.method,
      headers: req.headers,
      body: JSON.stringify({ ...body, source: 'web' }),
    })
  },
})

响应拦截 (onResponse)

import { createApi, defineApi } from '@cmtlyt/lingshu-toolkit/shared'

const getUserApi = defineApi({
  url: '/user',
  method: 'GET',
  onResponse(res, config) {
    // 可以修改响应数据
    return res.json().then(data => ({
      ...data,
      timestamp: Date.now(),
    }))
  },
})

const getUser = createApi(getUserApi, {
  baseUrl: 'https://api.example.com',
})

const result = await getUser({ id: '1' })

Mock 数据开发

import { createApi, defineApi } from '@cmtlyt/lingshu-toolkit/shared'

const mockApi = defineApi({
  url: '/user',
  onRequest() {
    return { id: '1', name: 'Mock User' }
  },
})

const api = createApi(mockApi, {
  baseUrl: 'https://api.example.com',
  requestMode: 'mock',
})

// 使用 Mock 数据
const result = await api({ id: '1' })
console.log(result) // { id: '1', name: 'Mock User' }

在 API Map 中使用

import { createApiWithMap, defineApiMap } from '@cmtlyt/lingshu-toolkit/shared'

const apiMap = defineApiMap({
  user: {
    getInfo: {
      url: '/user',
      tdto(data: { id: string }) {
        return { id: Number(data.id) }
      },
      tvo(data: any) {
        return { ...data, createdAt: new Date(data.createdAt) }
      },
    },
  },
})

const api = createApiWithMap(apiMap, {
  baseUrl: 'https://api.example.com',
})

const result = await api.user.getInfo({ id: '1' })

Hook 执行顺序

数据转换 Hook 的执行顺序:

  1. tdto - 请求前转换数据
  2. onRequest - 请求拦截
  3. onResponse/parser - 响应拦截/解析
  4. tvo - 响应后转换数据
import { createApi, defineApi } from '@cmtlyt/lingshu-toolkit/shared'

const getUserApi = defineApi({
  url: '/user',
  method: 'GET',
  tdto(data: { id: string }) {
    console.log('1. tdto - 请求前转换数据')
    return { id: Number(data.id) }
  },
  onRequest(req, config) {
    console.log('2. onRequest - 请求拦截')
    return req
  },
  onResponse(res, config) {
    console.log('3. onResponse - 响应拦截')
    return res
  },
  tvo(data: any) {
    console.log('4. tvo - 响应后转换数据')
    return { ...data, processed: true }
  },
})

const getUser = createApi(getUserApi, {
  baseUrl: 'https://api.example.com',
})

const result = await getUser({ id: '1' })
// 执行顺序: 1 -> 2 -> 3 -> 4

注意事项

⚠️ 数据转换

  • tdto 在请求前执行,用于转换请求数据
  • tvo 在响应后执行,用于转换响应数据
  • onRequest 在请求发送前执行,可以修改请求配置
  • onResponse 在响应解析后执行,可以修改响应数据
  • onResponse 会覆盖 parser 的解析方式

⚠️ onRequest 的返回值语义随请求模式变化

请求模式返回值处理
network(默认)返回 Response 短路请求;返回 Request 替换待发送的请求;返回 undefined/null 发送原请求;其余返回值被忽略并输出一次告警
mock返回值直接作为响应体使用
requestModeMap 自定义模式不会执行 onRequest,由自定义实现自行决定

⚠️ 行为变更

  • network 模式此前不会执行 onRequest
  • 若此前在共享配置中定义 onRequest 仅用于 mock 模式,升级后它在 network 模式下也会被执行
  • 这类 hook 返回裸数据对象时会被忽略并输出告警,可在 hook 内通过 config.requestMode 区分处理

⚠️ Mock 模式

  • mock 模式会使用 onRequest 返回的数据作为响应
  • network 模式会发送真实的网络请求
  • 可以通过 requestModeMap 自定义请求模式
  • 自定义请求模式会绕过默认的 hook 链

🔧 类型安全

  • 所有转换函数都支持完整的 TypeScript 类型
  • 建议为转换函数的参数和返回值指定明确的类型
  • 使用类型断言时要小心,确保类型安全

🔧 错误处理

  • 转换函数中抛出的错误会被捕获并传递到调用方
  • 建议在转换函数中添加适当的错误处理逻辑
  • 可以使用 try-catch 来处理转换过程中的错误