yapi-to-typescript 根据YApi自动生成TypeScript类型文件

1,718 阅读2分钟

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
    }
  }
}