Codegen技术实战解析与前端自动化代码生成优化经验
「我的写法,亲测靠谱」
Codegen 这玩意儿,我最早是被 GraphQL 客户端(比如 Apollo)带进来的,后来在微前端、BFF 层、甚至纯前端状态管理里都用上了。它不是银弹,但用对了真能省下大量样板代码——前提是别把它当黑盒瞎跑。
我目前在三个中型项目里落地 Codegen,最稳的方案是:用 @graphql-codegen/cli + 自定义插件 + 本地脚本封装,不依赖 CI 自动触发,全部手动执行、人工 review 输出结果。为什么?因为一旦生成器出错,你连报错栈都找不到源头,全靠猜。
下面是我的标准工作流(已封装成 npm run gen):
npx graphql-codegen --config codegen.yml --watch=false && prettier --write 'src/generated/**/*.{ts,tsx}'
注意两个关键点:禁用 watch 模式(热重载容易漏文件、卡死、覆盖未提交改动),生成后立刻格式化(否则团队里有人用 Prettier、有人不用,diff 会疯掉)。
codegen.yml 的核心配置我也贴出来(删减了无关插件):
overwrite: true
schema:
- 'https://jztheme.com/api/graphql'
- './src/graphql/schema.graphql'
documents: './src/graphql/**/*.graphql'
generates:
src/generated/graphql.ts:
plugins:
- typescript
- typescript-operations
- typescript-react-query
config:
skipTypename: true
dedupeOperationSuffix: true
exposeQueryKeys: true
fetcher: ./src/lib/fetcher#fetcher
reactQueryVersion: 5
这里重点说 exposeQueryKeys 和 fetcher。前者让每个 query 自动生成 key 数组,方便手动 invalidate;后者指向我自己的 fetch 封装,里面统一处理 auth、超时、错误分类——千万别用默认 fetch! 我之前踩过坑:默认 fetch 不带 credentials,登录态丢了,接口全 401,查了俩小时才发现是 Codegen 自己配的 fetcher 覆盖了全局配置。
还有个细节:我强制把 schema 分成两份——线上地址用于 CI,本地 .graphql 文件用于开发联调。为什么?因为 dev 环境经常切 mock server 或本地 GraphQL 服务,如果只配一个线上地址,一断网就跑不动 gen,整个开发流程卡住。本地 schema 文件我用 graphql-inspector 做 diff 校验,有变更才提醒更新。
「这几种错误写法,别再踩坑了」
下面这些,都是我在 Codegen 上翻过的车,按严重程度排:
- 错误1:直接 commit 生成文件到 Git,且没加 .gitattributes 或 ignore 注释
后果:两个人改同一个 gql 文件,生成内容冲突,merge 后 query key 错乱,useQuery 报 undefined。我们试过自动 resolve,结果发现某些字段类型推导错位,runtime 崩溃。现在做法:所有src/generated/**加.gitignore,但文档里写清楚「必须本地运行 gen 后才能启动 dev server」。 - 错误2:在 documents 里写
**/*.graphql,却不约束命名规范
某次新加了个user-profile-update.graphql,结果生成的 hook 叫useUserProfileUpdateMutation,和已有useUpdateUserProfileMutation冲突,TS 直接报重定义。后来我们强制约定:query 必须以Get开头,mutation 必须以Update/Create开头,Codegen 配置加dedupeOperationSuffix: true才稳。 - 错误3:把 Codegen 当 API 文档用,不写 @deprecated、不加 description
后端改了个字段名,没标 deprecated,Codegen 照常生成新类型,老组件还在用旧字段,TS 不报错(因为类型宽泛),上线后 UI 空白。现在要求所有 gql 文件顶部加注释:# @deprecated use newField instead,并配插件自动校验字段弃用状态。 - 错误4:在生成文件里手写逻辑,比如 patch 一个 type 或加个 helper 函数
第一次改完,第二次 gen 直接覆盖。我说服团队用了「生成 + 手动扩展」模式:生成graphql.ts,另建graphql.ext.ts,里面用declare module补充类型,或导出包装函数。这样既不污染生成区,又保留灵活性。
「实际项目中的坑」
第一个真实场景:我们有个大表单页,包含 12 个子模块,每个模块有自己的 GraphQL 查询。Codegen 默认为每个 operation 单独生成 hook,导致页面 import 了 12 个 hook,bundle size 涨了 86KB。解决办法是用 typescript-react-query 插件的 groupOperations: true,把同域查询合并成一个 hook 文件,再配合 React.lazy 动态加载子模块——Codegen 本身不解决性能问题,但它得给你留出优化入口。
第二个坑:TypeScript 版本升级后,types.d.ts 里生成的 Omit 类型报错。折腾半天发现是 Codegen 用了老版本的 TS 模板。最终方案不是降级 TS,而是加了一行 typescript: { disableTypescript: false } 并手动指定 typescript: 5.0(和项目一致)。记住:Codegen 的 TS 版本必须和项目 tsconfig.json 里的 compilerOptions.target 对齐,否则类型推导失真。
第三个细节:我们曾把 Codegen 放进 husky pre-commit,结果 commit 一次等 12 秒,开发者直接绕过 lint。现在改成了 pre-push + 缓存检查:先比对 src/graphql/ 的 git hash 和上次生成的 hash,不一致才跑 gen。提速 90%,也避免了误触发。
「结尾唠叨两句」
以上是我总结的最佳实践,没有「完美方案」,只有「当前项目里最省心的方案」。Codegen 是工具,不是框架,它不会替你思考数据流设计、错误边界、loading 状态粒度——这些还得靠人。
如果你也在用 Codegen,欢迎评论区交流:你们怎么处理多环境 schema?有没有试过自定义插件做字段权限过滤?或者……你们还踩过哪些离谱的坑?
这个技巧的拓展用法还有很多,比如用 Codegen 生成 Zustand store 结构、生成 i18n key 类型、甚至生成 Jest mock 数据模板,后续我会继续分享这类博客。
