orval初步使用

简介

orval 是一款类似于 Alibaba Pont 的一款作品,用于对接后端 openApi(swagger)文档,生成前端接口封装+ts类型的插件!

只不过后者已经被弃更了,貌似大厂的KPI项目而已!

orval中文文档

可惜的是官方并没有出中文文档,好在他们的文档开源而且是markdown,我直接fork了一份,利用 AI GPT来了个全文翻译(比谷歌浏览器自带的翻译好)!

并部署到了自己的服务器上,直接访问即可:https://orval.dingshaohua.com
image

构建和部署心得(可不看)

另外因为是 nextjs,考虑便于被搜索,官方部署的方式是服务端部署,也就是 npm build + npm start,但这会需要启动一个服务,我打包成静态资源即可!
官方仓库并没有做这些,所以需要我改动一些东西

修改next.config.js如下,再次运行 build 就是客户端打包了,项目会多出 out 目录为产出

module.exports = {
  // 添加以下代码
  output: 'export',
  trailingSlash: true,
  images: {
    unoptimized: true
  },
  // 注释以下代码
  // rewrites() {
  //   return [
  //     {
  //       source: '/docs{/}?',
  //       destination: '/docs/overview',
  //     },
  //   ];
  // },
  // ... 其它不变
}

解构数据

比如我有个登录接口,返回数据如下:

{
  code:1,
  msg:'ok',
  data:{
    user:{
      name:'张三'
    }
  }
}

不解构

不解构数据的情况下,你得这么用

const res = await api.login();
console.log(res.data.data.user)

解构

结构需要自定义 api实例(客户端),也就是要给orval配置
orval.config.ts

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

export default defineConfig({
  petstore: {
    input: 'http://localhost:3003/api/doc', // ① 规范文件
    output: {
      mode: 'tags', // ② 按 tag 分子文件,好维护
      target: './src/lib/api/endpoints', // ③ 生成 *.ts 目录
      schemas: './src/lib/api/model', // ④ 生成的类型定义目录
      mock: false, // ⑥ 同时生成 MSW mock
      clean: true, // 👈 每次生成前清目录
      override: {
        // 自定义 axios 实例,让 Orval 知道我们的拦截器已经解构了 response.data
        mutator: {
          path: './src/lib/api/api.base.ts',
          name: 'customAxiosInstance',
        }
      },
    },
  },
});

解构一层

自定义的api客户端要这么定义
api.base.ts

export const customAxiosInstance = <T>(
  config: AxiosRequestConfig,
  options?: AxiosRequestConfig,
): Promise<T> => {
  const promise = axios({
    ...config,
    ...options,
  }).then(({ data }) => data);
  return promise;
};

仅会把axios自动包裹的结构给移除

const res = await api.login();
console.log(res.data.user)

解构两层

自定义的api客户端要这么定义
api.base.ts

axios.interceptors.response.use(
  (response) => {
    const res = response.data;
    if (res.code === 1) {
      toast.error(res.msg);
      return Promise.reject(res.msg);
    } else {
      return res.data;
    }
  },
  (error) => {
   return Promise.reject(error);
  },
);


// 类型工具:提取 API 响应中 data 字段的类型
type ExtractDataType<T> = T extends { data: infer D } ? D : T;

export const customAxiosInstance = <T>(
  config: AxiosRequestConfig,
  options?: AxiosRequestConfig,
): Promise<ExtractDataType<T>> => {
  // 直接返回 axios 调用的结果
  // 响应拦截器已经处理了数据解构,返回的就是最终的业务数据
  return axios({
    ...config,
    ...options,
  }) as Promise<ExtractDataType<T>>;
};

这样,不仅要把axios自动包裹的结构给移除,还要把后端统一包裹的结构(jsonResult)给移除

const res = await api.login();
console.log(res.user)

image

方案总结

  • 响应拦截器已经解构了一层数据(返回 res.data
  • customAxiosInstance 的泛型 T 仍然指向完整的响应结构 JsonResultTypeTokenStringMeUser
  • 导致 TypeScript 认为返回的还是完整结构,需要访问 .data 属性

解决方案

  1. 添加类型工具 ExtractDataType<T>:自动提取 API 响应中 data 字段的类型
  2. 修改 customAxiosInstance 返回类型:从 Promise<T> 改为 Promise<ExtractDataType<T>>
  3. 类型断言:确保返回的类型与实际解构后的数据一致
posted @ 2025-08-17 16:33  丁少华  阅读(51)  评论(0)    收藏  举报