yapi-to-typescript
yapi-to-typescript是一个代码生成工具,其可根据 YApi 或 Swagger 的接口定义生成 TypeScript 或 JavaScript 的接口类型及其请求函数代码。
安装
选择你常用的包管理器将 yapi-to-typescript 加入项目依赖即可:
npm
npm i yapi-to-typescript
yarn
yarn add yapi-to-typescript
pnpm
pnpm add yapi-to-typescript
配置
首先,使用以下命令初始化配置文件:
npx ytt init
项目根目录(或您指定的路径)会生成配置文件然后ytt.config.ts 或 ytt.config.js,打开 ytt.config.ts 或 ytt.config.js 文件进行配置。
你还可以自定义配置文件的路径:
npx ytt init -c config/ytt.ts
ytt.config.ts
注释中表明需要修改的一些配置
import { defineConfig } from 'yapi-to-typescript'
export default defineConfig([
{
serverUrl: 'http://serverUrl', //************ 服务器地址:需要修改为YApi 首页地址,如 https://yapi.name.com/
typesOnly: false, //************ 是否只生成接口请求内容和返回内容的 TypeSript 类型,是则请求文件和请求函数都不会生成。
target: 'typescript',
reactHooks: {
enabled: false,
},
prodEnvName: 'local', //************ 生产环境名称。用于获取生产环境域名。 获取方式:打开项目 -> 设置 -> 环境配置 -> 点开或新增生产环境 -> 复制生产环境名称。
outputFilePath: 'src/api/index.ts',// ************ 输出文件路径。可以是 相对路径 或 绝对路径。如 'src/api/index.ts'。
requestFunctionFilePath: 'src/api/request.ts', // 请求函数文件路径。如 'src/api/request.ts'
dataKey: 'data', //************ 作为具体业务,我们只关心 data 字段内的数据(code、msg 已经由请求函数统一处理),此时,可将 dataKey 设为 data
projects: [ //************ 项目列表
{
token: 'token', //************ 项目的唯一标识。支持多个项目。对于基于 Swagger 的项目,置空即可。 获取方式:打开项目 -> 设置 -> token 配置 -> 复制 token。
categories: [ //************ 分类列表
{
//************ 分类 ID,可以设置多个。设为 0 时表示全部分类。
//************ 如果需要获取全部分类,同时排除指定分类,可以这样:[0, -20, -21],分类 ID 前面的负号表示排除。
//************ 获取方式:打开项目 -> 点开分类 -> 复制浏览器地址栏 /api/cat_ 后面的数字。
id: '395865',
//************ 获取请求函数的名称。
getRequestFunctionName(interfaceInfo, changeCase) {
// 以接口全路径生成请求函数名
return changeCase.camelCase(interfaceInfo.path)
// 若生成的请求函数名存在语法关键词报错、或想通过某个关键词触发 IDE 自动引入提示,可考虑加前缀,如:
// return changeCase.camelCase(`api_${interfaceInfo.path}`)
// 若生成的请求函数名有重复报错,可考虑将接口请求方式纳入生成条件,如:
// return changeCase.camelCase(`${interfaceInfo.method}_${interfaceInfo.path}`)
},
},
],
},
],
},
])
使用
生成代码 使用以下命令生成代码:
npx ytt
如果要使用自定义的配置文件:
npx ytt -c config/ytt.ts
在项目中会生成两个文件 /src/api/index.ts、 /src/api/request.ts
/src/api/index.ts 请求内容和返回内容的 TypeSript 类型,请求函数
**
* 接口 [add user↗](http://---) 的 **请求类型**
*
* @分类 [User↗](http://---)
* @请求头 `POST /test/user/add`
* @更新时间 `2022-02-22 15:15:09`
*/
export interface postTestUserAdd {
name: string
}
/**
* 接口 [add user↗](http://---) 的 **返回类型**
*
* @分类 [User↗](http://---)
* @请求头 `POST /test/user/add`
* @更新时间 `2022-02-22 15:15:09`
*/
export interface PostTestUserAddResponse {
code: number
message: string
result: boolean
}
...
/**
* 接口 [add user↗](http://---) 的 **请求函数**
*
* @分类 [User↗](http://---)
* @请求头 `POST /test/user/add`
* @更新时间 `2022-02-22 15:15:09`
*/
export const postTestUserAdd = /*#__PURE__*/ (requestData: postTestUserAdd, ...args: UserRequestRestArgs) => {
return request<PostTestUserAddResponse>(prepare(postTestUserAddRequestConfig, requestData), ...args)
}
/src/api/request.ts 请求文件
import { RequestFunctionParams } from 'yapi-to-typescript'
export interface RequestOptions {
/**
* 使用的服务器。
*
* - `prod`: 生产服务器
* - `dev`: 测试服务器
* - `mock`: 模拟服务器
*
* @default prod
*/
server?: 'prod' | 'dev' | 'mock',
}
export default function request<TResponseData>(
payload: RequestFunctionParams,
options: RequestOptions = {
server: 'prod',
},
): Promise<TResponseData> {
return new Promise<TResponseData>((resolve, reject) => {
// 基本地址
const baseUrl = options.server === 'mock'
? payload.mockUrl
: options.server === 'dev'
? payload.devUrl
: payload.prodUrl
// 请求地址
const url = `${baseUrl}${payload.path}`
// 具体请求逻辑
})
}
调用接口请求函数
从 outputFilePath 导入你要调用的接口请求函数即可,接口请求函数的名称由配置 getRequestFunctionName 决定,如:
import { postTestUserAdd } from '../api'
const createUser = async () => {
const data = await postTestUserAdd({
page: 1,
})
console.log(data)
}
调用上传文件类接口
对于上传文件类接口,你需要将文件包装为一个 FileData 实例,如:
import { FileData } from 'yapi-to-typescript'
import { uploadFile } from '../api'
const changeAvatar = async (file: File) => {
const res = await uploadFile({
type: 'avatar',
file: new FileData(file),
})
console.log(res)
}
获取接口的请求数据、返回数据类型
如果你没动过 getRequestDataTypeName、getResponseDataTypeName 这两个配置,默认情况下,你可以这样获取接口的请求数据、返回数据类型:
import { getUserInfo, GetUserInfoRequest, GetUserInfoResponse } from '../api'
interface CustomUserInfo extends GetUserInfoResponse {
gender: 'male' | 'female' | 'unknown'
}
const customGetUserInfo = async (
payload: GetUserInfoRequest,
): Promise<CustomUserInfo> => {
const userInfo = await getUserInfo(payload)
return {
...userInfo,
gender:
userInfo.sexy === 1 ? 'male' : userInfo.sexy === 2 ? 'female' : 'unknown',
}
}
如果你只想获得请求数据、返回数据下某个字段的类型,可以这样做:
import { GetUserInfoResponse } from '../api'
type UserRole = GetUserInfoResponse['role']
如何编写一个统一请求函数
基于浏览器 fetch 的示例
下面是一个基于浏览器原生 fetch 的示例,通过 cross-fetch,你也可以让它运行在一些未实现 fetch 接口的老旧浏览器、Node.js、React Native 上。
import fetch from 'cross-fetch'
import { RequestBodyType, RequestFunctionParams } from 'yapi-to-typescript'
export interface RequestOptions {
/**
* 是否返回 Blob 结果,适用某些返回文件流的接口。
*/
returnBlob?: boolean
}
export enum RequestErrorType {
NetworkError = 'NetworkError',
StatusError = 'StatusError',
BusinessError = 'BusinessError',
}
export class RequestError extends Error {
constructor(
public type: RequestErrorType,
public message: any,
public httpStatusOrBusinessCode: number = 0,
) {
super(message instanceof Error ? message.message : String(message))
}
}
export default async function request<TResponseData>(
payload: RequestFunctionParams,
options?: RequestOptions,
): Promise<TResponseData> {
try {
// 基础 URL,可以从载荷中拉取或者写死
const baseUrl = payload.prodUrl
// 完整 URL
const url = `${baseUrl}${payload.path}`
// fetch 选项
const fetchOptions: RequestInit = {
method: payload.method,
headers: {
...(payload.hasFileData
? {}
: payload.requestBodyType === RequestBodyType.json
? { 'Content-Type': 'application/json; charset=UTF-8' }
: payload.requestBodyType === RequestBodyType.form
? {
'Content-Type':
'application/x-www-form-urlencoded; charset=UTF-8',
}
: {}),
},
body: payload.hasFileData
? payload.getFormData()
: payload.requestBodyType === RequestBodyType.json
? JSON.stringify(payload.data)
: payload.requestBodyType === RequestBodyType.form
? Object.keys(payload.data)
.filter(key => payload.data[key] != null)
.map(
key =>
`${encodeURIComponent(key)}=${encodeURIComponent(
payload.data[key],
)}`,
)
.join('&')
: undefined,
}
// 发起请求
const [fetchErr, fetchRes] = await fetch(url, fetchOptions).then<
[
// 如果遇到网络故障,fetch 将会 reject 一个 TypeError 对象
TypeError,
Response,
]
>(
res => [null, res] as any,
err => [err, null] as any,
)
// 网络错误
if (fetchErr) {
throw new RequestError(RequestErrorType.NetworkError, fetchErr)
}
// 状态错误
if (fetchRes.status < 200 || fetchRes.status >= 300) {
throw new RequestError(
RequestErrorType.StatusError,
`${fetchRes.status}: ${fetchRes.statusText}`,
fetchRes.status,
)
}
// 请求结果处理
const res = options?.returnBlob
? await fetchRes.blob()
: (fetchRes.headers.get('Content-Type') || '').indexOf(
'application/json',
) >= 0
? await fetchRes
.json()
// 解析 JSON 报错时给个空对象作为默认值
.catch(() => ({}))
: await fetchRes.text()
// 业务错误
// 假设 code 为 0 时表示请求成功,其他表示请求失败,同时 msg 表示错误信息
if (
res != null &&
typeof res === 'object' &&
res.code != null &&
res.code !== 0
) {
throw new RequestError(RequestErrorType.BusinessError, res.msg, res.code)
}
// 适配 dataKey,取出 data
const data: TResponseData =
res != null &&
typeof res === 'object' &&
payload.dataKey != null &&
res[payload.dataKey] != null
? res[payload.dataKey]
: res
return data
} catch (err: unknown) {
// 重试函数
const retry = () => request<TResponseData>(payload, options)
if (err instanceof RequestError) {
// 网络错误处理
if (err.type === RequestErrorType.NetworkError) {
// 此处可弹窗说明原因:err.message,最好也提供重试操作,下面以原生 confirm 为例,建议替换为项目中使用到的弹窗组件
const isRetry = confirm(`网络错误:${err.message},是否重试?`)
if (isRetry) {
return retry()
}
throw err
}
// 状态错误处理
else if (err.type === RequestErrorType.StatusError) {
// 用户未登录处理
if (err.httpStatusOrBusinessCode === 401) {
// 推荐在此处发起登录逻辑
}
}
// 业务错误处理
else if (err.type === RequestErrorType.BusinessError) {
// 推荐弹个轻提示说明错误原因:err.message
throw err
}
} else {
throw err
}
}
}