你有没有遇到过这样的场景:后端改了一个接口字段,前端完全不知道,直到上线后用户反馈才发现数据异常?或者你在写
axios.post('/user/list', params)时,根本不知道params该传什么字段,只能去翻 Swagger 文档对照着写?这篇文章介绍一套工具链,让这两个问题彻底消失。
问题的根源
传统前端 API 调用长这样:
ts
// 你不知道 body 该是什么结构
const res = await axios.post('/persona/config/save', body)
// 你不知道 res.data 里有什么字段
const data = res.data.data
类型是 any,IDE 没有提示,出错只能靠运行时发现。
根源在于:后端有完整的 API 描述(Swagger/OpenAPI 文档),但前端没有消费这份描述。
解决方案就是把 OpenAPI 文档转成 TypeScript 类型,再用类型安全的 HTTP 客户端调用它。整个工具链涉及四个工具:
zx → swagger2openapi → openapi-typescript → openapi-fetch
工具一:zx ------ 让 Node.js 脚本告别 child_process
它是什么
zx 是 Google 出品的 Node.js 脚本工具。核心思想是:写脚本的人更熟悉 JavaScript,但 shell 命令执行又很麻烦。zx 让你在 .js 文件里直接执行 shell 命令。
对比传统写法
js
// 传统 child_process,冗长且难处理异步
const { exec } = require('child_process')
exec('ls -la', (err, stdout) => {
if (err) throw err
console.log(stdout)
})
// zx,简洁直观
import { $ } from 'zx'
const result = await $`ls -la`
console.log(result.stdout)
核心特性
模板字符串执行命令
js
import { $ } from 'zx'
// 执行命令,自动处理转义
await $`echo "hello world"`
// 变量插值(自动安全转义,防注入)
const filename = 'my file.txt'
await $`cat ${filename}` // 不会因为空格出问题
管道支持
js
// shell 管道
await $`swagger2openapi http://api.example.com/v2/api-docs | openapi-typescript -o schema.ts`
错误处理
js
try {
await $`cat non-existent-file`
} catch (err) {
console.error(`Exit code: ${err.exitCode}`)
console.error(`Stderr: ${err.stderr}`)
}
安装
bash
pnpm add -D zx
zx 使用 ES Module 语法(import / await),运行前需要确保 JS 文件被识别为 ESM。有三种方式:
方式一(推荐):package.json 加 "type": "module"
json
// package.json
{ "type": "module" }
加了之后,项目内所有 .js 文件默认按 ESM 处理,直接用 node 运行:
bash
node generate_apis.js
方式二:文件后缀改为 .mjs
不改 package.json,把脚本文件命名为 generate_apis.mjs,node 会自动识别为 ESM:
bash
node generate_apis.mjs
方式三:用 zx CLI 直接运行
zx 自带命令行工具,无需关心 ESM 配置,直接运行:
bash
npx zx generate_apis.js
实际项目中通常把命令写进 package.json 的 scripts,用包管理器调用:
bash
pnpm gen:api # 等价于 node generate_apis.js 或 zx generate_apis.js
工具二:swagger2openapi ------ 格式转换桥梁
为什么需要它
OpenAPI 规范有两个主要版本:
- Swagger 2.0 :老版本,
/v2/api-docs端点,大量 Spring Boot 项目在用 - OpenAPI 3.0 :新版本,
/v3/api-docs端点,功能更强
openapi-typescript 只支持 OpenAPI 3.0。如果你的后端接口文档是 Swagger 2.0,就需要 swagger2openapi 先做一次转换。
用法
bash
# 直接转换并输出
swagger2openapi http://your-api.com/v2/api-docs
# 配合管道,转换后直接输入给 openapi-typescript
swagger2openapi http://your-api.com/v2/api-docs | openapi-typescript -o schema.ts
如何判断用哪个
| 后端文档地址 | 版本 | 是否需要 swagger2openapi |
|---|---|---|
/v2/api-docs |
Swagger 2.0 | 需要 |
/v3/api-docs |
OpenAPI 3.0 | 不需要 |
安装
bash
pnpm add -D swagger2openapi
工具三:openapi-typescript ------ 把 API 文档变成 TypeScript 类型
它做了什么
openapi-typescript 读取 OpenAPI 规范文档,生成对应的 TypeScript 类型定义文件。这个文件完整描述了每一个接口的路径、请求参数、请求体、响应结构。
输出示例
假设后端有一个接口:
yaml
POST /persona/config/save
requestBody:
application/json:
personaName: string (required)
personaAge: number
responses:
200:
application/json:
code: number
data: { id: number }
openapi-typescript 会生成:
ts
// server-schema.ts(自动生成,不要手动修改)
export interface paths {
"/persona/config/save": {
post: {
requestBody: {
content: {
"application/json": {
personaName: string
personaAge?: number
}
}
}
responses: {
200: {
content: {
"application/json": {
code: number
data: { id: number }
}
}
}
}
}
}
}
用法
bash
# OpenAPI 3.0 接口,直接生成
openapi-typescript http://your-api.com/v3/api-docs -o src/api/server-schema.ts
# Swagger 2.0 接口,先转换再生成
swagger2openapi http://your-api.com/v2/api-docs | openapi-typescript -o src/api/server-schema.ts
# 本地文件
openapi-typescript ./openapi.json -o src/api/server-schema.ts
安装
bash
pnpm add -D openapi-typescript
最佳实践
把命令写进 package.json,与团队共享:
json
{
"scripts": {
"gen:api": "node generate_apis.js"
}
}
generate_apis.js(用 zx 编写,支持多服务):
js
import { $ } from 'zx'
// Swagger 2.0 服务
await $`swagger2openapi http://service-a.com/v2/api-docs | openapi-typescript -o src/api/serviceA/server-schema.ts`
// OpenAPI 3.0 服务
await $`openapi-typescript http://service-b.com/v3/api-docs -o src/api/serviceB/server-schema.ts`
后端接口有变更时,运行一条命令即可同步:
bash
pnpm gen:api
工具四:openapi-fetch ------ 类型安全的 HTTP 客户端
它解决了什么
有了 server-schema.ts,还需要一个能"消费"这份类型的 HTTP 客户端。axios 做不到,因为它不知道每个 URL 对应什么类型。
openapi-fetch 就是为此而生的。它接受 paths 类型作为泛型参数,使得:
- 输入 URL 时有自动补全,路径写错会报错
body/params有完整类型约束,字段写错编译失败- 响应的
data有精确类型推断,不再是any
核心概念
1. 创建 client
ts
// src/api/client.ts
import createClient from 'openapi-fetch'
import type { paths } from './server-schema.ts'
export const apiClient = createClient<paths>({
baseUrl: 'https://your-api.com'
})
apiClient.use(middleware);
createClient<paths> 这一步是关键,它把所有接口类型绑定到这个 client 实例上。
2. Middleware(中间件)
等价于 axios 的拦截器,处理鉴权和统一错误处理:
ts
// src/api/middleware.ts
import type { Middleware } from 'openapi-fetch'
export const authMiddleware: Middleware = {
// 请求前:注入 token
async onRequest({ request }) {
const token = await getToken()
request.headers.set('Authorization', `Bearer ${token}`)
return request
},
// 响应后:统一处理错误
async onResponse({ response }) {
if (response.status === 401) {
// 跳转登录
return
}
if (response.status >= 400) {
const body = await response.clone().json()
throw new Error(body?.message || '请求失败')
}
return response
}
}
// 挂载中间件
apiClient.use(authMiddleware)
3. 发起请求
ts
import { apiClient } from '@/api/client'
// GET 请求(query 参数)
const { data, error } = await apiClient.GET('/persona/enum/list', {
params: {
query: { type: 'voice' } // 类型安全,写错字段名会报错
}
})
// POST 请求(body)
const { data, error } = await apiClient.POST('/persona/config/save', {
body: {
personaName: '助手A', // 少传 required 字段会报错
personaAge: 25
}
})
// 处理响应
if (error) {
console.error('请求失败', error)
} else {
console.log(data.id) // data 有精确类型,IDE 有提示
}
与 axios 的核心差异
| 对比项 | axios | openapi-fetch |
|---|---|---|
| URL 类型检查 | 无,字符串 | 有,仅允许 schema 中存在的路径 |
| 请求参数类型 | any |
与 OpenAPI schema 完全对应 |
| 响应类型 | 手动断言 as Xxx |
自动推断 |
| 鉴权/错误处理 | 拦截器 | Middleware |
| 底层实现 | XMLHttpRequest | Fetch API |
完整接入流程
第一步:安装依赖
bash
# 运行时依赖
pnpm add openapi-fetch
# 开发依赖(代码生成工具)
pnpm add -D openapi-typescript swagger2openapi zx
第二步:编写生成脚本
js
// generate_apis.js
import { $ } from 'zx'
console.log('Generating API schema...')
await $`openapi-typescript http://your-api.com/v3/api-docs -o src/api/server-schema.ts`
console.log('Done.')
json
// package.json
{
"scripts": {
"gen:api": "node generate_apis.js"
}
}
第三步:首次生成类型文件
bash
pnpm gen:api
# 生成 src/api/server-schema.ts
第四步:创建 middleware
ts
// src/api/middleware.ts
import type { Middleware } from 'openapi-fetch'
import { getToken, logout } from '@/auth'
import { showError } from '@/utils'
export const middleware: Middleware = {
async onRequest({ request }) {
const token = await getToken()
if (token) request.headers.set('Authorization', `Bearer ${token}`)
return request
},
async onResponse({ response }) {
if (response.status === 401) { await logout(); return }
if (response.status > 401 && response.status <= 500) {
const body = await response.clone().json().catch(() => ({}))
showError(body?.message || '请求失败')
throw new Error(body?.message)
}
// 处理业务 code 错误(后端约定 code=0 成功)
if (response.headers.get('Content-Type')?.includes('application/json')) {
const body = await response.clone().json()
if (body?.code !== undefined && body.code !== 0 && body.code !== 200) {
showError(body?.message || '业务错误')
throw new Error(body?.message)
}
}
return response
}
}
第五步:创建 client
ts
// src/api/client.ts
import createClient from 'openapi-fetch'
import type { paths } from './server-schema.ts'
import { middleware } from './middleware.ts'
export const apiClient = createClient<paths>({
baseUrl: import.meta.env.VITE_API_BASE_URL
})
apiClient.use(middleware)
第六步:在业务代码中使用
ts
// src/views/persona/index.vue
import { apiClient } from '@/api/client'
async function loadPersonaList() {
const { data, error } = await apiClient.GET('/persona/config/list', {
params: { query: { page: 1, size: 20 } }
})
if (error) return
personaList.value = data.data.records // 完整类型推断
}
async function savePersona(form: PersonaForm) {
const { data } = await apiClient.POST('/persona/config/save', {
body: form // form 字段类型由 schema 约束
})
return data
}
团队协作中的价值
接入这套工具链后,团队的工作流变成:
后端更新接口
↓
前端运行 pnpm gen:api
↓
TypeScript 编译报错,精确指出哪些调用处受到影响
↓
按报错修复,确保不遗漏
相比之前:
- 不再需要对着 Swagger 文档手写类型,生成脚本一键同步
- 接口变更有编译期保障,不会等到运行时才发现
- IDE 自动补全接口路径和参数,减少查文档的时间
- 新同学上手更快,不需要熟悉所有接口,IDE 会告诉你能传什么
常见问题
Q:server-schema.ts 可以手动修改吗?
不建议。文件头有 Do not make direct changes to the file. 的注释,每次重新生成会覆盖。如果后端文档有问题,应该推动后端修复,或者在生成后用脚本做后处理。
Q:多个后端服务怎么处理?
每个服务单独生成一个 server-schema.ts,创建各自的 createClient<paths>,然后分别导出:
ts
export const serviceAClient = createClient<ServiceAPaths>({ baseUrl: '...' })
export const serviceBClient = createClient<ServiceBPaths>({ baseUrl: '...' })
Q:本地开发需要代理,baseUrl 怎么配?
配合 Vite 的 proxy 使用时,baseUrl 设为空字符串 '' 即可,请求路径保持原样由 proxy 转发:
ts
export const apiClient = createClient<paths>({ baseUrl: '' })
Q:响应数据是包在 { code, data, msg } 外层的,怎么处理?
在 middleware 的 onResponse 里处理。如果想让 data 直接拿到内层数据,可以修改 response:
ts
async onResponse({ response }) {
const body = await response.clone().json()
// 返回一个新的 Response,body 替换为内层 data
return new Response(JSON.stringify(body.data), response)
}
但这样会丢失外层的 code、msg 类型信息,按项目需求取舍。
总结
这套工具链的本质是:把后端 API 的约定从运行时移到编译时。
zx:让代码生成脚本写起来像 shell,维护成本接近零swagger2openapi:消除 Swagger 2.0 和 OpenAPI 3.0 的格式鸿沟openapi-typescript:把 API 文档"翻译"成 TypeScript 类型,一次生成,长期受益openapi-fetch:让每一次 HTTP 调用都有类型保障,把接口错误拦截在编译阶段
四个工具各司其职,组合起来的效果远大于单独使用任何一个。