Skip to content

高级用法

多租户 header、token 刷新队列、多服务 / 多上下文客户端、无类型 fallback、上传编排等。基础见 概述基础用法

动态 Header(app-id / token)

typescript
import { defineMiddleware } from '@vafast/api-client'

/** 多租户:每个请求带上 app-id */
const appIdMiddleware = defineMiddleware(async (ctx, next) => {
  ctx.headers.set('app-id', import.meta.env.VITE_APP_ID)
  return next()
}, { name: 'app-id' })

/** 可选登录:有 token 才带 Authorization */
const tokenMiddleware = defineMiddleware(async (ctx, next) => {
  const token = localStorage.getItem('token')
  if (token) {
    ctx.headers.set('Authorization', `Bearer ${token}`)
  }
  return next()
}, { name: 'token' })

Token 过期:单飞刷新 + 排队重试

多个请求同时命中过期码时,只刷新一次,其余排队拿新 token 后重试:

typescript
const TOKEN_EXPIRED = 40101 // 与认证服务约定的业务码

let refreshing = false
let queue: Array<{
  resolve: (token: string) => void
  reject: (err: unknown) => void
}> = []

async function refreshAccessToken(): Promise<string> {
  const res = await fetch('/auth/api/auth/refresh', {
    method: 'POST',
    headers: { 'content-type': 'application/json', 'app-id': APP_ID },
    body: JSON.stringify({ refreshToken: localStorage.getItem('refreshToken') }),
  })
  const body = await res.json()
  if (!body.jwtToken) throw new Error(body.message ?? 'refresh failed')
  localStorage.setItem('token', body.jwtToken)
  return body.jwtToken
}

const tokenRefreshMiddleware = defineMiddleware(async (ctx, next) => {
  const response = await next()
  if (response.error?.code !== TOKEN_EXPIRED) return response

  if (refreshing) {
    const token = await new Promise<string>((resolve, reject) => {
      queue.push({ resolve, reject })
    })
    ctx.headers.set('Authorization', `Bearer ${token}`)
    return next()
  }

  refreshing = true
  try {
    const token = await refreshAccessToken()
    queue.forEach((p) => p.resolve(token))
    queue = []
    ctx.headers.set('Authorization', `Bearer ${token}`)
    return next()
  } catch (e) {
    queue.forEach((p) => p.reject(e))
    queue = []
    // 清会话、跳登录…
    return response
  } finally {
    refreshing = false
  }
}, { name: 'token-refresh' })

中间件叠放顺序

建议:业务 header / 鉴权 → 刷新 → 重试 / 日志

typescript
import { createClient, retryMiddleware, loggerMiddleware } from '@vafast/api-client'

const client = createClient({ baseURL: '/blog/api', timeout: 30_000 })
  .use(appIdMiddleware)
  .use(tokenMiddleware)
  .use(tokenRefreshMiddleware)
  .use(retryMiddleware({ count: 2, delay: 500 }))
  .use(loggerMiddleware({ prefix: '[blog]' }))

多服务

typescript
import { createClient } from '@vafast/api-client'
import { createApiClient as createAuthClient } from './types/auth.generated'
import { createApiClient as createBlogClient } from './types/blog.generated'

const AUTH = { baseURL: '/auth/api', timeout: 30_000 }
const BLOG = { baseURL: '/blog/api', timeout: 30_000 }

export const auth = createAuthClient(
  createClient(AUTH).use(tokenMiddleware).use(tokenRefreshMiddleware),
)

export const blog = createBlogClient(
  createClient(BLOG).use(appIdMiddleware).use(tokenMiddleware).use(tokenRefreshMiddleware),
)

const { data, error } = await blog.posts.find.post({ current: 1, pageSize: 10 })

同一服务、不同租户上下文

同一份 generated 类型,挂不同中间件拆成多个导出:

typescript
const tenantAppId = defineMiddleware(async (ctx, next) => {
  ctx.headers.set('app-id', currentTenantId())
  return next()
})

const systemAppId = defineMiddleware(async (ctx, next) => {
  ctx.headers.set('app-id', SYSTEM_APP_ID)
  return next()
})

export const blog = createBlogClient(createClient(BLOG).use(tenantAppId).use(tokenMiddleware))
export const blogSystem = createBlogClient(createClient(BLOG).use(systemAppId).use(tokenMiddleware))

/** 临时打某个租户的 API */
export function createBlogForTenant(appId: string) {
  const mw = defineMiddleware(async (ctx, next) => {
    ctx.headers.set('app-id', appId)
    return next()
  })
  return createBlogClient(createClient(BLOG).use(mw).use(tokenMiddleware))
}

调用风格:REST vs Body RPC

风格示例对应服务端
REST 路径参数api.users({ id: '1' }).get()GET /users/:id
Body RPCapi.users.find.post({ id: '1' })POST /users/find
typescript
const one = await api.users({ id: '1' }).get()

const list = await api.users.find.post({ current: 1, pageSize: 20 })
const detail = await api.users.findOne.post({ id: '1' })

无生成类型:client.request

渐进接入或临时路径可用底层 request(仍走中间件、仍是 { data, error }):

typescript
const client = createClient('/queue/api').use(tokenMiddleware)

const { data, error } = await client.request<{ ok: boolean }>(
  'POST',
  '/jobs/run',
  { name: 'cleanup' },
)

if (error) {
  showError(error.message)
  return
}
console.log(data.ok)

并发共享同一 client

typescript
const [users, posts] = await Promise.all([
  blog.users.find.post({ current: 1, pageSize: 10 }),
  blog.posts.find.post({ current: 1, pageSize: 10 }),
])

if (users.error || posts.error) {
  showError(users.error?.message ?? posts.error?.message ?? '加载失败')
  return
}

上传编排(凭证 → 直传 → 登记)

大文件常不把文件打进 API body,而是多步 { data, error } 编排,并用 AbortSignal 取消:

typescript
async function uploadFile(file: File, signal: AbortSignal) {
  const cred = await blog.upload.credentials.post({ filename: file.name }, { signal })
  if (cred.error) return cred

  await fetch(cred.data.uploadUrl, {
    method: 'PUT',
    body: file,
    signal,
    headers: cred.data.headers,
  })

  return blog.files.create.post(
    { key: cred.data.key, size: file.size },
    { signal },
  )
}

const controller = new AbortController()
const { data, error } = await uploadFile(file, controller.signal)
if (error) showError(error.message)

相关