Apollo Client中GraphQL本地数据自动归一化实现方法咨询
问题背景
为优化性能(避免嵌套项变更触发全组件重绘),需要将GraphQL本地缓存归一化(扁平化嵌套结构)。原本以为Apollo会自动处理缓存归一化,但实际测试发现并非如此:
执行以下代码后:
const IS_LOGGED_IN = gql` query IsUserLoggedIn { isLoggedIn @client items @client { name @client quantity @client } } `; client.writeQuery({ query: IS_LOGGED_IN, data: { isLoggedIn: false, items: [ {name: "Oranges", quantity: "5"}, {name: "Pears", quantity: "10"}, ] }, });
开发者工具显示items数据未被归一化(仍保持嵌套结构),与从外部数据库查询的已归一化Location项形成明显差异。
手动为数据指定id和__typename可实现归一化,但操作繁琐:
const IS_LOGGED_IN = gql` query IsUserLoggedIn { isLoggedIn @client cake @client { name @client ingredients @client { name @client, quantity @client } } } `; client.writeQuery({ query: IS_LOGGED_IN, data: { isLoggedIn: false, cake: [ {id: "1", __typename:"Cake", name: "Cookies", ingredients: [ {id: "4", __typename:"Ingredients", name: "Sugar", quantity: "100g"}, {id: "5", __typename:"Ingredients", name: "Chocolate", quantity: "250g"} ]}, {id: "2", __typename:"Cake", name: "Choux", ingredients: [ {id: "6", __typename:"Ingredients", name: "Eggs", quantity: "2"}, {id: "8", __typename:"Ingredients", name: "Flour", quantity: "250g"} ]}, ] }, });
尝试通过typePolicies生成随机ID,但担心破坏cache.identify功能:
const client = new ApolloClient({ cache: new InMemoryCache({ typePolicies: { Ingredients: { // If the ID can be deduced from the fields //keyFields: ['name'], // If the ID must be different for each elements // I hope that using randomness here is not an issue… // in particular I am not sure if it does not break // functions to identify the ID of an element… keyFields: () => "Ingredients:" + uuidv4(), }, }, }), });
同时无法避免手动输入__typename,因此想知道:处理GraphQL本地数据时,能否自动从查询推断__typename并生成ID,减少手动操作与错误?
解决方案
1. 自动注入__typename
Apollo Client的addTypename默认开启,但仅对发送给服务端的查询生效。针对本地数据,可以通过以下方式自动注入:
方法一:工具函数批量添加
封装通用函数,为嵌套数据自动添加__typename:
const addTypenames = (data, typename, nestedMap = {}) => { const processed = { ...data, __typename: typename }; // 处理指定的嵌套字段 Object.entries(nestedMap).forEach(([key, childTypename]) => { if (processed[key]) { processed[key] = Array.isArray(processed[key]) ? processed[key].map(item => addTypenames(item, childTypename, nestedMap[key] || {})) : addTypenames(processed[key], childTypename, nestedMap[key] || {}); } }); return processed; }; // 使用示例 const cakeData = [ {name: "Cookies", ingredients: [ {name: "Sugar", quantity: "100g"}, {name: "Chocolate", quantity: "250g"} ]}, {name: "Choux", ingredients: [ {name: "Eggs", quantity: "2"}, {name: "Flour", quantity: "250g"} ]}, ].map(item => addTypenames(item, 'Cake', { ingredients: 'Ingredients' })); client.writeQuery({ query: IS_LOGGED_IN, data: { isLoggedIn: false, cake: cakeData } });
方法二:自定义链接拦截写入操作
通过Apollo Link拦截writeQuery/writeFragment操作,自动为数据添加__typename:
import { ApolloLink } from '@apollo/client'; const autoTypenameLink = new ApolloLink((operation, forward) => { const context = operation.getContext(); // 仅处理本地写入操作 if (context.operationName === 'IsUserLoggedIn') { const data = context.data || operation.variables?.data; if (data?.cake) { data.cake = data.cake.map(cake => ({ ...cake, __typename: 'Cake', ingredients: cake.ingredients.map(ing => ({ ...ing, __typename: 'Ingredients' })) })); } operation.setContext({ data }); } return forward(operation); }); // 注册到ApolloClient链接链 const client = new ApolloClient({ link: ApolloLink.from([autoTypenameLink, /* 其他链接 */]), cache: new InMemoryCache(...) });
2. 自动生成ID且不破坏缓存识别
绝对不要用随机函数作为keyFields的返回值——keyFields的作用是提取实体的唯一标识,每次生成新ID会导致缓存无法识别同一实体,完全失去归一化意义。正确的自动生成方式:
方法一:本地Schema解析器生成ID
定义本地GraphQL Schema,在解析器中自动为无ID的实体生成UUID:
import { makeExecutableSchema } from '@graphql-tools/schema'; import { v4 as uuidv4 } from 'uuid'; const typeDefs = ` type Cake { id: ID! name: String! ingredients: [Ingredients!]! } type Ingredients { id: ID! name: String! quantity: String! } type Query { isLoggedIn: Boolean! cake: [Cake!]! } `; const resolvers = { Cake: { id: (parent) => parent.id || uuidv4() }, Ingredients: { id: (parent) => parent.id || uuidv4() }, // 其他解析器... }; const localSchema = makeExecutableSchema({ typeDefs, resolvers });
后续通过writeQuery写入数据时,无需手动指定ID,解析器会自动补充。
方法二:写入前批量生成ID
封装工具函数,在写入缓存前为数据批量生成ID:
import { v4 as uuidv4 } from 'uuid'; const generateIds = (data, typename, nestedMap = {}) => { const processed = { ...data, id: data.id || `${typename}_${uuidv4()}`, __typename: typename }; // 处理嵌套字段 Object.entries(nestedMap).forEach(([key, childTypename]) => { if (processed[key]) { processed[key] = Array.isArray(processed[key]) ? processed[key].map(item => generateIds(item, childTypename, nestedMap[key] || {})) : generateIds(processed[key], childTypename, nestedMap[key] || {}); } }); return processed; }; // 使用示例 const cakeData = [ {name: "Cookies", ingredients: [ {name: "Sugar", quantity: "100g"}, {name: "Chocolate", quantity: "250g"} ]}, {name: "Choux", ingredients: [ {name: "Eggs", quantity: "2"}, {name: "Flour", quantity: "250g"} ]}, ].map(item => generateIds(item, 'Cake', { ingredients: 'Ingredients' })); client.writeQuery({ query: IS_LOGGED_IN, data: { isLoggedIn: false, cake: cakeData } });
3. 配置缓存策略确保归一化
确保InMemoryCache的typePolicies正确指定每个类型的主键字段,让Apollo能正确识别实体:
const client = new ApolloClient({ cache: new InMemoryCache({ typePolicies: { Cake: { keyFields: ['id'], // 明确指定主键为id }, Ingredients: { keyFields: ['id'], }, }, }), });
总结
- 禁用随机生成
keyFields的方式,避免破坏缓存识别逻辑; - 通过工具函数或本地Schema自动生成ID与注入
__typename,减少手动重复操作; - 正确配置
typePolicies的主键字段,确保Apollo能完成缓存归一化。
内容的提问来源于stack exchange,提问作者tobiasBora

