Codegen实战:从原理到项目落地的完整指南

宇文思捷 移动 阅读 1,174
赞 17 收藏
二维码
手机扫码查看
反馈

我的写法,亲测靠谱

在移动端项目里用 Codegen(比如 Apollo Codegen、GraphQL Codegen 这类),我一开始图快,直接按文档跑个命令生成一堆 TypeScript 类型和 hooks,结果上线前被类型错误和缓存问题搞到半夜。后来折腾了几个项目,慢慢摸出一套自己觉得最稳的用法。核心就一点:别让自动生成的代码成为你项目的“黑盒”

Codegen实战:从原理到项目落地的完整指南

我现在的做法是:生成的代码只作为“类型参考”和“基础请求封装”,真正的业务逻辑绝不直接依赖它。比如 GraphQL Codegen 生成的 useQuery,我几乎从来不用,而是自己封装一层:

// api/queries/product.ts
import { useQuery } from '@apollo/client';
import { GetProductDocument, GetProductQuery, GetProductQueryVariables } from '@/gql/generated';

export const useProduct = (id: string) => {
  const { data, loading, error } = useQuery<GetProductQuery, GetProductQueryVariables>(
    GetProductDocument,
    {
      variables: { id },
      // 关键:明确配置缓存策略
      fetchPolicy: 'cache-first',
      nextFetchPolicy: 'cache-only', // 避免重复网络请求
    }
  );

  return {
    product: data?.product,
    loading,
    error,
  };
};

这样做的好处很明显:第一,调用方只需要关心 product 对象,不用管 GraphQL 的嵌套结构;第二,缓存策略集中管理,不会因为某个页面忘了设 fetchPolicy 导致数据不一致;第三,万一后面要换数据源(比如从 GraphQL 切到 REST),只要改这一层就行,上层完全无感。

很多人图省事,直接在组件里写:

// ❌ 别这么干!
const { data } = useQuery(GetProductDocument, { variables: { id } });
const product = data?.getProduct?.product; // 嵌套太深,容易出错

这种写法在小项目可能没问题,但一旦接口字段变动,或者多人协作时,很容易漏掉某处的解构,导致 undefined 满天飞。而且缓存策略散落在各处,调试起来特别痛苦。

这几种错误写法,别再踩坑了

我见过也踩过不少 Codegen 的坑,总结几个高频雷区:

  • 生成路径混乱:默认配置经常把所有类型塞进一个 generated.ts 文件,几百个接口混在一起,VS Code 打开都卡。建议按模块拆分,比如 generated/products.tsgenerated/users.ts,配合 codegen.ymlgenerates 字段精细控制。
  • 忽略 nullable 处理:GraphQL 里字段标了 ! 才是非空,但很多后端偷懒全标成可空。Codegen 生成的类型会带 | null | undefined,如果前端直接 .map() 或访问属性,运行时就崩。我的习惯是在封装层做一次清洗,把确定有值的字段提出来。
  • 版本不同步:本地生成的类型基于旧 schema,但后端已经改了接口。这种情况本地跑得好好的,CI 直接报错。解决办法是:把 schema.graphql 文件纳入 Git,并在 pre-commit 钩子里强制重新生成。或者更狠一点,CI 流程里每次 build 前都拉最新 schema 再生成。

还有一个特别隐蔽的问题:Codegen 默认生成的 hooks 不处理 loading/error 状态复用。比如两个页面同时请求同一个 product,第一个还在 loading,第二个进来又触发一次网络请求。虽然 Apollo Client 有 dedup 机制,但如果 fetchPolicy 设成 network-only 就完蛋了。所以我在封装层加了状态共享逻辑(借助 Zustand 或 React Context),但这属于进阶优化,小项目可以先不管。

实际项目中的坑

最近一个电商项目,用 GraphQL Codegen + React Query(对,不是 Apollo,现在好多团队转 React Query 了)。这里有个细节:React Query 的 Codegen 插件(比如 @graphql-codegen/typescript-react-query)生成的 hooks 默认不带 queryKey 自动推导,得手动传。我一开始嫌麻烦,直接写死字符串:

// ❌ 千万别写死 queryKey!
const { data } = useGetProductQuery({ id }, { queryKey: ['product', id] });

结果某次重构改了 hook 名字,queryKey 没跟着改,缓存失效,用户反复看到 loading。后来改成用生成的 document 对象当 key:

// ✅ 安全做法
import { getProductQuery } from '@/gql/generated';

const { data } = useGetProductQuery({ id }, { 
  queryKey: [getProductQuery, id] 
});

这样就算 hook 重命名,只要 GraphQL document 不变,缓存依然有效。

另外,移动端网络不稳定,Codegen 生成的错误类型经常是 unknown,直接 toast 会显示 [object Object]。我现在会在全局 error boundary 里加一层格式化:

// utils/error.ts
export const formatGraphQLError = (error: unknown): string => {
  if (error instanceof Error) return error.message;
  if (typeof error === 'string') return error;
  if (typeof error === 'object' && error !== null) {
    // 尝试取 GraphQL errors
    const gqlError = (error as any).graphQLErrors?.[0]?.message;
    if (gqlError) return gqlError;
  }
  return '请求失败,请稍后重试';
};

这个函数在所有 API 调用的地方统一用,避免每个页面自己处理。

最后提一嘴性能:生成的类型文件别直接 import 到页面组件里。TypeScript 编译时会解析整个文件,如果 generated 文件太大,热更新会变慢。建议通过 barrel file(比如 api/index.ts)导出需要的部分,或者用 Webpack 的 alias 拆分。

结尾碎碎念

Codegen 是个好工具,能省掉大量手写类型和请求代码的时间,但前提是你要理解它生成的东西怎么工作。别把它当成魔法,生成完就扔那不管。花半小时读一遍生成的代码,搞清楚缓存、错误、类型边界在哪,能省下你未来好几天的 debug 时间。

以上是我踩坑后的总结,希望对你有帮助。有更好的方案欢迎评论区交流——比如你们怎么处理多端 schema 差异?或者有没有自动化校验生成类型和运行时数据一致性的方案?我还在摸索中。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论