Codegen实战:从原理到项目落地的完整指南
我的写法,亲测靠谱
在移动端项目里用 Codegen(比如 Apollo Codegen、GraphQL Codegen 这类),我一开始图快,直接按文档跑个命令生成一堆 TypeScript 类型和 hooks,结果上线前被类型错误和缓存问题搞到半夜。后来折腾了几个项目,慢慢摸出一套自己觉得最稳的用法。核心就一点:别让自动生成的代码成为你项目的“黑盒”。
我现在的做法是:生成的代码只作为“类型参考”和“基础请求封装”,真正的业务逻辑绝不直接依赖它。比如 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.ts、generated/users.ts,配合codegen.yml的generates字段精细控制。 - 忽略 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 差异?或者有没有自动化校验生成类型和运行时数据一致性的方案?我还在摸索中。

暂无评论