自動產出前端 API 型態與 TanStack Query 方法實例,加速前端開發

| 3 min read

前後端彼此之間溝通,最麻煩的地方大概就是 API 格式、有沒有寫文件,文件有沒有定期更新維護之類的,後端可以妥善運用 Swagger 產出文件,前端只要依照 Swagger 文件就可以看到所有參數與回傳型態。

既然後端可以透過 Swagger 自動產文件,前端想必也一定可以透過某些方式拿到 Swagger 的文件格式,然後自動產出所有型態啦~ wwwww

有很多相關套件可以抓到型態,但我這次想介紹使用的是 Orval ,可以支援 OpenAPI v3 或是 Swagger v2 版本的 yaml 或是 json 格式,可以自動產出常使用到的 axios 或是 TanStack Query (React Query),不需要再看著文件一個一個寫 API。

作者本身就是因為經常使用到 Swagger editor 跟 Swagger codegen ,但覺得不夠好用所以才自己開發這個套件,因為作者自己會用到表示這個套件應該會一直更新下去(?)

在開發文件上也提供了詳細的官方文檔與 Examples。

swagger example

首先身為前端的你,必須要拿到 swagger.json 或是 yaml 檔案,網址也可以,我們會使用這兩個任一檔案透過 Orval 產出需要的東西。

1. 安裝

$ npm i orval -D
  or
$ yarn add orval -D

2. 在 package.json 寫入腳本

"scripts": {
    // ... others scripts

    "orval": "orval"
  },

3. 在專案的根目錄新增一個 orval.config.ts 的檔案

拿出準備好的 swagger.json 我們要開始自動產生 API client 囉!

orval.config.ts 為 Orval 的設定檔,可以客製化 axios 與使用 TanStack Query,設定檔內容可參考官方文件 https://orval.dev

// orval.config.ts
import { defineConfig } from 'orval'

export default defineConfig({
  api: {
    output: {
      mode: 'split',
      target: 'src/apis/endpoints.ts',
      // schemas: 'src/apis/models',   // 開啟此選項 endpoints.schemas.ts 會拆成多個檔案。
      client: 'react-query',
      prettier: true,
      override: {
        // 設定自定義的 API client 檔案位置
        mutator: {
          path: 'src/apis/custom-client.ts',
          name: 'useCustomInstance',
        },
        // 設定 react-query 的參數
        query: {
          useQuery: true,
          useInfinite: true,
          options: {
            staleTime: 10000,
          },
        },
      },
    },
    // 設定 swagger.json 的位置 (必填)
    input: {
      target: 'http://你的網址/swagger.json', // 也可以直接使用檔案位置
    },
  },
})

4. 設定客製化的 axios 實例

// custom-client.ts
import Axios, { AxiosRequestConfig } from 'axios'
import getToken from '@/utils/getToken'

export const AXIOS_INSTANCE = Axios.create({ baseURL: `${import.meta.env.VITE_API_URL}` }) // vite 拿 env 的用法

export const useCustomInstance = <T>(): ((config: AxiosRequestConfig) => Promise<T>) => {
  return (config: AxiosRequestConfig) => {
    const source = Axios.CancelToken.source()
    const token = (Axios.AxiosHeaders = getToken())
    const promise = AXIOS_INSTANCE({
      ...config,
      cancelToken: source.token,
      headers: { ...config.headers, Authorization: `${token}` },
    }).then(({ data }) => {
      // 這裡可以自定義回傳格式
      return data.data
    })

    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
    // @ts-ignore
    promise.cancel = () => {
      source.cancel('Query was cancelled by React Query')
    }

    return promise
  }
}

export default useCustomInstance

// 定義你的 ErrorType 與 BodyType
export type ErrorType<ErrorData> = ErrorData

export type BodyType<BodyData> = BodyData & {
  headers?: any
  data?: { token?: string; results?: any; data?: any; userInfo?: any }
  code?: number
  message?: string
}

5. 產生檔案

最後在終端機輸入 yarn orval 就會自動產生 endpoints.tsendpoints.schemas.ts 兩個檔案,不要手動修改這兩個檔案,每次都透過 yarn orval 指令來更新這兩個檔案。

endpoints.ts

自動產生 API client 與 react-query 版本,名稱也會幫你取好,註解也會一起寫進來,真心方便(=´∀ `)人(´∀ `=)。

// endpoints.ts

/**
 * Generated by orval v6.17.0 🍺
 * Do not edit manually.
 * Swagger Petstore
 * OpenAPI spec version: 1.0.0
 */
import { useQuery, useMutation } from 'react-query'
import type {
  UseQueryOptions,
  UseMutationOptions,
  QueryFunction,
  MutationFunction,
  UseQueryResult,
  QueryKey,
} from 'react-query'
import type { Pets, Error, ListPetsParams, Pet, CreatePetsBody } from '../model'
import { useCustomClient } from '../mutator/custom-client'
import type { ErrorType, BodyType } from '../mutator/custom-client'

type AwaitedInput<T> = PromiseLike<T> | T

type Awaited<O> = O extends AwaitedInput<infer T> ? T : never

/**
 * @summary List all pets
 */
export const useListPetsHook = () => {
  const listPets = useCustomClient<Pets>()

  return (params?: ListPetsParams, version = 1, signal?: AbortSignal) => {
    return listPets({
      url: `/v${version}/pets`,
      method: 'get',
      params,
      signal,
    })
  }
}

export const getListPetsQueryKey = (params?: ListPetsParams, version = 1) => {
  return [`/v${version}/pets`, ...(params ? [params] : [])] as const
}

export const useListPetsQueryOptions = <
  TData = Awaited<ReturnType<ReturnType<typeof useListPetsHook>>>,
  TError = ErrorType<Error>
>(
  params?: ListPetsParams,
  version = 1,
  options?: {
    query?: UseQueryOptions<Awaited<ReturnType<ReturnType<typeof useListPetsHook>>>, TError, TData>
  }
): UseQueryOptions<Awaited<ReturnType<ReturnType<typeof useListPetsHook>>>, TError, TData> & { queryKey: QueryKey } => {
  const { query: queryOptions } = options ?? {}

  const queryKey = queryOptions?.queryKey ?? getListPetsQueryKey(params, version)

  const listPets = useListPetsHook()

  const queryFn: QueryFunction<Awaited<ReturnType<ReturnType<typeof useListPetsHook>>>> = ({ signal }) =>
    listPets(params, version, signal)

  return { queryKey, queryFn, enabled: !!version, ...queryOptions }
}

export type ListPetsQueryResult = NonNullable<Awaited<ReturnType<ReturnType<typeof useListPetsHook>>>>
export type ListPetsQueryError = ErrorType<Error>

// ...以下省略

endpoints.schemas.ts

這個檔案會自動產生 API 的 TypeScript,直接使用該檔案內 export 的內容即可。 也可以加入 msw 或是 faker 使用唷,因為我這邊沒用到所以方法請參考官方說明文件

使用方式

因為 Orval 只安裝在開發環境上,所以他只是輔助產出檔案,使用方式就跟以前寫 axios 或是 TanStack Query 都一樣。

// App.tsx
import React, { useEffect } from 'react'
import { useListPets } from './apis/endpoints.ts'
import { useAuthDispatch } from './auth.context'
import './App.css'

function App() {
  const dispatch = useAuthDispatch()
  const { data: pets, refetch } = useListPets() // 直接使用

  useEffect(() => {
    dispatch('token')
    setTimeout(() => {
      refetch()
    }, 2000)
  }, [refetch, dispatch])

  return (
    <div className="App">
      <header className="App-header">
        {pets?.map((pet: any) => (
          <p key={pet.id}>{pet.name}</p>
        ))}
      </header>
    </div>
  )
}

以後只要 API 文件有更新,只要再跑一遍 yarn orval 或是直接寫入你的執行的 dev 腳本內,每次開發時就會自己抓最新 API 文件產出格式了,是不是真的好方便~~(撒花

喜歡這篇文章就讓我們一起加入前端懶人行列吧~~(\ *≧ω≦)