gql标签使用字符串插值生成动态typeDefs出现报错问题求解
我已经在graphql-tag仓库提交了该问题,现在在此发布希望能得到可行的解决方案。
我需要基于传入React组件的props动态定义本地类型的typeDefs。
我知道这不是gql的标准定义方式,目前我正在开发Apollo Provider的React封装组件,目标是简化本地数据mock流程,减少样板代码。
我计划采用声明式方案:用户只需定义字段数组(包含字段名、graphQL类型、可选实现,未指定实现时默认采用对应graphQL类型的合理faker实现)即可将本地字段直接挂载到Query类型上;同时支持定义类型数组(包含类型名、字段、可选嵌套类型),无需编写样板代码即可声明任意本地schema。
首先基准实现完全正常,可以通过Apollo CLI执行代码生成,也能在应用中查询新增的本地测试字段(带@client指令),代码如下:
const localTypeDefs = gql` extend type Query { test: String } `; const client = new ApolloClient({ cache: new InMemoryCache(), uri: "https://localhost:4000/graphql", typeDefs: localTypeDefs, ...apolloClientOptions, });
如果我将gql定义改为字符串插值的形式:
const name = "name"; const graphQLType = "String"; const localTypeDefs = gql` extend type Query { ${name}: ${graphQLType} } `;
运行codegen时会报错:Syntax Error: Expected Name, found ":"。
如果我改成如下的gql定义:
const testString = "test: String"; const localTypeDefs = gql` extend type Query { ${testString} } `;
运行codegen时会报错:Syntax Error: Expected Name, found "}".。整体来看,插值字符串之后的内容都会导致编译器在后续第一个字符处抛出该错误;如果我将}也放入testString变量中,会报错Syntax Error: Expected Name, found <EOF>.。
之后我尝试改用函数调用语法定义localTypeDefs,代码如下:
const testString = "test: String"; const localTypeDefs = gql(`extend type Query { ${testString} } `);
此时typeDefs生成没有报错,但执行代码生成时查询该字段仍然报错GraphQLError: Cannot query field "test" on type "Query"。该查询和基准实现中正常运行的查询完全一致,只要切回基准实现代码生成就不会报错。
更奇怪的是,如果我保持函数调用语法,但是内容和基准实现完全一致,代码如下:
const localTypeDefs = gql(` extend type Query { test: String } `);
仍然会报错GraphQLError: Cannot query field "test" on type "Query",切回模板字符串写法就恢复正常。
根本原因说明
graphql-tag的两种调用方式逻辑完全不同:
- 模板字符串形式
gql...``会在编译阶段(包含codegen静态扫描阶段)直接解析静态内容生成AST,插值变量在静态扫描阶段仅为空占位符,不会被解析器识别,所以会出现语法错误。 - 函数调用形式
gql(str)只会在运行时动态拼接生成AST,Apollo CLI的codegen仅做静态代码扫描,不会执行运行时代码逻辑,所以动态拼接的内容完全不会被codegen识别。
可行方案
方案1:调整codegen数据源
修改codegen配置,不再直接扫描源代码提取typeDefs,改为从运行时导出的完整schema读取:
- 新增独立的schema导出脚本,在脚本中初始化你的Apollo封装组件,获取最终拼接完成的完整typeDefs字符串,导出为本地.graphql文件
- 将codegen配置的schema源改为上述导出的.graphql文件
方案2:直接传入字符串typeDefs
ApolloClient的typeDefs参数原生支持直接传入GraphQL Schema字符串,不需要用gql标签处理:
const name = "name"; const graphQLType = "String"; // 直接动态拼接完整schema字符串即可 const localTypeDefs = ` extend type Query { ${name}: ${graphQLType} } `; const client = new ApolloClient({ cache: new InMemoryCache(), uri: "https://localhost:4000/graphql", typeDefs: localTypeDefs, ...apolloClientOptions, });
该方案运行时功能完全正常,如果你不需要codegen识别动态生成的字段,可直接使用;如果需要生成类型,搭配方案1的导出schema流程即可。
方案3:使用mock工具动态扩展字段
如果你的核心需求是本地mock,可直接使用mock工具的动态扩展能力,不需要在Apollo Client的typeDefs中定义,完全避开静态解析限制。
内容的提问来源于stack exchange,提问作者Niklas Moss

