本地解析器
使用GraphQL类似解析器管理本地数据
📄 注意:我们推荐使用 字段策略而不是本地 解析器,如 局部字段。
本地 解析器支持将被从未来的主要版本中的 Apollo客户端的核心中移除。相同或类似的功能将通过 ApolloLink
获得,有关详细信息,请参阅 此问题。
我们已经学习如何使用Apollo客户端从我们的 GraphQL服务器管理远程数据,但对于本地数据我们应该做什么呢?我们希望从多个组件访问布尔标志和设备API结果,但又不想维护一个单独的Redux或MobX存储。理想情况下,我们希望Apollo缓存成为客户端应用中所有数据的单一真相来源。
Apollo 客户端(版本 >= 2.5)内置了本地状态处理功能,允许您将本地数据存储在与远程数据相同的 Apollo 缓存中。要访问本地数据,只需使用 查询 和 GraphQL 来查询它。您甚至可以在同一个查询中请求本地和服务器数据!
在本节中,您将学习 Apollo 客户端如何帮助简化您应用程序中的本地状态管理。我们将涵盖客户端 resolvers 如何帮助我们执行本地查询和 mutations。您还将学习如何使用 @client
directive 查询和更新缓存。
请注意,此文档的目的是帮助您熟悉 Apollo 客户端的本地状态管理功能,并作为参考指南。如果您正在寻找一个按步骤教程,概述如何使用 Apollo 客户端处理本地状态(并利用其他 Apollo 组件构建全栈应用程序),请参阅 全栈快速入门课程。
更新本地状态
进行局部状态 mutations 有两种主要方式。第一种方式是通过调用 cache.writeQuery
直接写入缓存。直接写入非常适合一次性的 mutations,这些突变不依赖于当前缓存中的数据,例如写入单个值。第二种方式是通过使用带有 GraphQL mutation 的 useMutation hook 来调用本地客户端 resolver。如果您要进行的突变依赖于缓存中的现有值,例如将项目添加到列表中或切换布尔值,我们建议使用 resolver。
直接写入
对缓存进行直接写入不需要 GraphQL mutation 或 resolver 函数。它们通过直接访问从 useApolloClient
hook 返回的 client
属性来实现,该属性在 useQuery
hook 返回的结果中提供,或在 ApolloConsumer
component 的渲染 prop 函数中。我们建议使用此策略进行简单的写入,例如写入字符串,或一次性的写入。请注意,直接写入不是作为 GraphQL mutations 在底层实现的,因此您不应将其包含在模式中。它们也不验证写入缓存的数据是否符合有效的 GraphQL 数据格式。如果这些功能对您很重要,请选择使用本地 resolver。
import React from "react";import { useApolloClient } from "@apollo/client";import Link from "./Link";function FilterLink({ filter, children }) {const client = useApolloClient();return (<LinkonClick={() => client.writeQuery({query: gql`query GetVisibilityFilter { visibilityFilter }`,data: { visibilityFilter: filter },})}>{children}</Link>);}
当我们使用单个值调用 ApolloConsumer
渲染属性函数时,传入了 Apollo Client 实例。可以将 ApolloConsumer
组件视为与 React 的 useContext API 中的 Consumer
组件类似。从客户端实例,你可以直接调用 client.writeQuery
并传入你想写入缓存的数据。
如果我们想立即订阅刚刚写入缓存中的数据呢?让我们在相关链接上创建一个 active
属性,如果它与缓存中当前的 visibilityFilter
相同,则标记链接的过滤器为活动状态。为了立即订阅客户端的突变,我们可以使用 useQuery
。 useQuery
钩子也使其客户端实例在其结果对象中可用。
import React from "react";import { gql, useQuery } from "@apollo/client";import Link from "./Link";const GET_VISIBILITY_FILTER = gql`query GetVisibilityFilter {visibilityFilter @client}`;function FilterLink({ filter, children }) {const { data, client } = useQuery(GET_VISIBILITY_FILTER);return (<LinkonClick={() => client.writeQuery({query: GET_VISIBILITY_FILTER,data: { visibilityFilter: filter },})}active={data.visibilityFilter === filter}>{children}</Link>)}
你会在我们的查询中注意到,在我们的 visibilityFilter 字段旁边有一个 @client
指令。这告诉 Apollo Client 从本地(从缓存或使用本地 resolver)获取字段数据,而不是发送到我们的 GraphQL 服务器。一旦你调用了 client.writeQuery
,渲染属性函数上的查询结果将自动更新。所有缓存写入和读取都是同步的,所以你不必担心加载状态。
本地解析器
如果您想将本地的状态更新作为 GraphQL mutation 来实现,那么您需要在本地 resolver 映射中指定一个函数。resolver 映射是一个包含每个 GraphQL 对象类型 的 resolver 函数的对象。为了直观了解这一切是如何结合在一起的,可以将 GraphQL 查询 或 mutation 看作是一个针对每个 字段 的函数调用树。这些函数调用解析数据或另一个函数调用。所以当 GraphQL 查询通过 Apollo Client 运行时,它会寻找一种本质上是针对查询中的每个字段运行函数的方法。当它发现字段上的 @client
指令 时,它会转向其内部 resolver 映射,寻找可以为该字段运行的函数。
为了使本地的 resolvers 更加灵活,resolver 函数的签名与用 Apollo Server 构建的,在服务器上使用的 resolver 函数的签名完全相同。让我们回顾一下 resolver 函数的四个参数:
fieldName: (obj, args, context, info) => result;
obj
: 包含从父字段 resolver 返回的结果的对象,或者对于一个顶级 查询 或 mutation 的情况,是ROOT_QUERY
对象。args
: 包含传递给该字段的全部 参数 的对象。例如,如果您调用了一个 mutation,名为 updateNetworkStatus(isConnected: true),则args
对象将是:{ isConnected: true }
.context
: 包含在你的 React 组件和 Apollo Client 网络 stack 之间共享的上下文信息。除了可能存在的任何自定义上下文属性之外,本地的 resolvers 总是接收以下内容:context.client
: Apollo Client 实例。context.cache
: Apollo Cache 实例,可以用它与context.cache.readQuery
,.writeQuery
,.readFragment
,.writeFragment
,.modify
和.evict
方法来操作缓存。您可以在 管理缓存 中了解更多关于这些方法的信息。context.getCacheKey
: 使用__typename
和id
获取缓存中的一个键。
info
: 查询执行状态的信息。您可能永远用不到这个。
让我们看看一个示例 resolver,我们将使用它来切换待办事项的完成状态:
import { ApolloClient, InMemoryCache } from '@apollo/client';const client = new ApolloClient({cache: new InMemoryCache(),resolvers: {Mutation: {toggleTodo: (_root, variables, { cache }) => {cache.modify({id: cache.identify({__typename: 'TodoItem',id: variables.id,}),fields: {completed: value => !value,},});return null;},},},});
在Apollo Client的旧版本中,切换completed
状态需要从缓存中读取TodoItem
片段,通过否定completed
布尔值来修改结果,然后将片段写回缓存。Apollo Client 3.0引入了cache.modify
方法,这是一种更简单、更快的更新给定entity
对象特定fields
的方式。要确定实体的ID,我们将对象的__typename
和主键字段传递给cache.identify
方法。
一旦我们切换completed
字段,因为我们不打算在UI中使用mututation的返回结果,所以我们会返回null
,因为所有GraphQL类型默认均为可空。
下面我们来学习如何从组件中触发toggleTodo
mututation:
import React from "react"import { gql, useMutation } from "@apollo/client";const TOGGLE_TODO = gql`mutation ToggleTodo($id: Int!) {toggleTodo(id: $id) @client}`;function Todo({ id, completed, text }) {const [toggleTodo] = useMutation(TOGGLE_TODO, { variables: { id } });return (<lionClick={toggleTodo}style={{textDecoration: completed ? "line-through" : "none",}}>{text}</li>);}
首先,我们创建一个GraphQL mututation,它将我们想要切换的todo id作为唯一参数。我们通过在字段上标记 @client
directive来标识这是一个本地mututation。这将告诉Apollo Client调用我们的本地toggleTodo
mututation resolver来解析字段。然后,我们创建一个带useMutation
的组件,就像我们为远程mututation做的那样。最后,将您的GraphQL mututation传递给组件,并在您渲染prop函数的UI中触发它。
查询本地状态
查询本地数据与查询您的GraphQL服务器非常相似。唯一的区别是您需要在本地字段上添加 @client
directive以指示它们应从Apollo Client缓存或本地 resolver函数中解析。以下是一个例子:
import React from "react";import { gql, useQuery } from "@apollo/client";import Todo from "./Todo";const GET_TODOS = gql`query GetTodos {todos @client {idcompletedtext}visibilityFilter @client}`;function TodoList() {const { data: { todos, visibilityFilter } } = useQuery(GET_TODOS);return (<ul>{getVisibleTodos(todos, visibilityFilter).map(todo => (<Todo key={todo.id} {...todo} />))}</ul>);}
在这里,我们创建了一个GraphQL查询并添加了 @client
directives到todos
和visibilityFilter
字段。然后,我们将查询传递给useQuery
hook。这里 @client
directives允许useQuery
组件知道todos
和visibilityFilter
应从Apollo Client缓存中提取或使用预定义的本地 resolver解析。后续章节将帮助更详细地解释这两种选项如何工作。
⚠️ 由于上述查询在组件挂载时立即运行,如果没有在缓存中发现待办事项或在本地定义任何帮助计算 todos
的解析器,我们该怎么办?我们需要在运行查询之前将初始状态写入缓存,以防止它出错。有关更多信息,请参阅以下初始化缓存部分。
初始化缓存
通常,您需要将初始状态写入缓存,以便在触发变异之前查询数据的任何组件都不会出错。cache.writeQuery
用于为缓存准备初始值。
import { ApolloClient, InMemoryCache } from '@apollo/client';const cache = new InMemoryCache();const client = new ApolloClient({cache,resolvers: { /* ... */ },});cache.writeQuery({query: gql`query GetTodosNetworkStatusAndFilter {todosvisibilityFilternetworkStatus {isConnected}}`,data: {todos: [],visibilityFilter: 'SHOW_ALL',networkStatus: {__typename: 'NetworkStatus',isConnected: false,},},});
有时您可能需要重置您的应用中的存储(例如,当用户登出时)。如果您在任何地方调用client.resetStore
,您可能希望再次初始化缓存。您可以使用client.onResetStore
方法来注册一个回调,它将再次调用cache.writeQuery
。
import { ApolloClient, InMemoryCache } from '@apollo/client';const cache = new InMemoryCache();const client = new ApolloClient({cache,resolvers: { /* ... */ },});function writeInitialData() {cache.writeQuery({query: gql`query GetTodosNetworkStatusAndFilter {todosvisibilityFilternetworkStatus {isConnected}}`,data: {todos: [],visibilityFilter: 'SHOW_ALL',networkStatus: {__typename: 'NetworkStatus',isConnected: false,},},});}writeInitialData();client.onResetStore(writeInitialData);
本地数据查询流程
当执行包含@client
指示符的查询时,Apollo Client 会按照某些连续步骤尝试找到@client
字段的结果。让我们使用以下isInCart
字段为本地数据查找流程:
const GET_LAUNCH_DETAILS = gql`query LaunchDetails($launchId: ID!) {launch(id: $launchId) {isInCart @clientsiterocket {type}}}`;
此查询包括远程和本地字段。isInCart
是唯一带有@client
指示符的字段,因此这是我们关注的焦点。当 Apollo Client 运行此查询并试图找到isInCart
字段的结果时,它会执行以下步骤:
- 是否有设置与字段名称
isInCart
相关的解析器函数(通过ApolloClient
构造函数的resolvers
参数或Apollo Client 的setResolvers
/addResolvers
方法)?如果是,运行并返回解析器函数的结果。 - 如果找不到匹配的解析器函数,请检查Apollo Client
缓存,看看是否可以直接找到isInCart
值。如果找到,则返回该值。
让我们更仔细地看一下这两个步骤。
- 使用本地解析器解决
@client
数据(如上所述的第一步)的说明,请参阅使用解析器处理 client 字段。 - 正在加载
@client
数据来自缓存(如上所述的第2步),解释详见使用缓存处理@client字段。
使用 resolver 处理@client字段
本地resolver与远程resolver非常相似。与将 GraphQL 查询发送到远程 GraphQL 端点、然后运行 resolver 函数来填充并返回结果集不同,Apollo Client将对标记有@client指令的任何字段运行本地定义的 resolver 函数。Apollo Client。
import { ApolloClient, InMemoryCache, HttpLink, gql } from '@apollo/client';const GET_CART_ITEMS = gql`query GetCartItems {cartItems @client}`;const cache = new InMemoryCache();cache.writeQuery({query: GET_CART_ITEMS,data: {cartItems: [],},});const client = new ApolloClient({cache,link: new HttpLink({uri: 'https://127.0.0.1:4000/graphql',}),resolvers: {Launch: {isInCart: (launch, _args, { cache }) => {const { cartItems } = cache.readQuery({ query: GET_CART_ITEMS });return cartItems.includes(launch.id);},},},});const GET_LAUNCH_DETAILS = gql`query LaunchDetails($launchId: ID!) {launch(id: $launchId) {isInCart @clientsiterocket {type}}}`;// ... run the query using client.query, a <Query /> component, etc.
在此处,当执行GET_LAUNCH_DETAILS
查询时,Apollo Client会寻找与isInCart
字段关联的本地resolver。由于我们在
ApolloClient
构造函数中定义了isInCart
字段的本地resolver,所以它找到了可以使用的resolver。这个 resolver 函数被运行,然后结果被计算并与查询结果的其余部分(如果找不到本地 resolver,Apollo Client将检查缓存以寻找匹配的字段——请参阅本地数据查询流程以获得更多详细信息)合并。
通过ApolloClient
's构造函数的
resolvers参数或其setResolvers/ addResolvers方法设置 resolver,将 resolver 添加到 Apollo Client 内部 resolver 映射中(有关 resolver 映射的更多详细信息,请参阅本地 resolver部分)。在上面的例子中,我们向 resolver 映射中添加了一个
isInCart
resolver,用于launch
GraphQL对象类型。
resolvers: {Launch: {isInCart: (launch, _args, { cache }) => {const { cartItems } = cache.readQuery({ query: GET_CART_ITEMS });return cartItems.includes(launch.id);},},},
launch包含服务器返回的其余查询数据,这意味着在这种情况下我们可以使用launch
来获取当前的launch id
。在这个 resolver 中我们没有使用任何参数,因此可以省略第二个 resolver 参数。从context
(第三个参数),我们使用cache
引用直接操作缓存。所以在这个 resolver 中,我们直接调用缓存来获取所有购物车项目,检查是否有任何加载的购物车项目与上级launch.id匹配,并据此返回true
或false。
返回的布尔值随后被合并到原始查询的结果中。
就像服务器上的解析器一样,本地解析器非常灵活。它们可以用来执行任何您希望在返回指定字段的结果之前进行的本地计算。您可以手动查询(或将数据写入)缓存的不同方式,调用其他辅助工具或库来准备/验证/清理数据,跟踪统计信息,调用其他数据存储库来准备结果,等等。
将 @client
集成到远程查询中
虽然 Apollo Client 的本地状态处理功能可用于仅使用本地状态,但大多数基于 Apollo 的应用程序都是构建为与远程数据源一起使用的。为了解决这个问题,Apollo Client 支持混合使用基于@client
的本地解析器与远程查询,以及在同一个请求中将基于@client
的字段作为远程查询的参数。
可以使用@client
指令在任意选择集或字段上,以标识该字段的应通过本地解析器帮助在本地上载的结果。
import { ApolloClient, InMemoryCache, HttpLink, gql } from '@apollo/client';const MEMBER_DETAILS = gql`query Member {member {nameroleisLoggedIn @client}}`;const client = new ApolloClient({link: new HttpLink({ uri: 'https://127.0.0.1:4000/graphql' }),cache: new InMemoryCache(),resolvers: {Member: {isLoggedIn() {return someInternalLoginVerificationFunction();}}},});// ... run the query using client.query, the <Query /> component, etc.
当上述MEMBER_DETAILS
查询由 Apollo Client(假设我们在与基于网络的GraphQLAPI)调用时,@client
字段首先被从文档中删除,剩余的查询被通过网络发送到 GraphQL API。远程解析器处理查询并将其结果发送回 Apollo Client 后,然后运行@client
部分的原查询的本地解析器,将合并网络结果的合并与网络结果,并将最终结果数据作为对原始操作的响应返回。因此,在上面的示例中,isLoggedIn
在发送到网络 API 处理的其余部分之前被删除,然后当结果返回时,通过从解析器映射中调用isLoggedIn()
函数来计算isLoggedIn
。本地和网络结果合并在一起,并将最终响应提供给应用程序。
可以使用@client
指令与整个选择集一起:
import { ApolloClient, InMemoryCache, HttpLink, gql } from '@apollo/client';const MEMBER_DETAILS = gql`query Member {member {namerolesession @client {isLoggedInconnectionCounterrors}}}`;const client = new ApolloClient({link: new HttpLink({ uri: 'https://127.0.0.1:4000/graphql' }),cache: new InMemoryCache(),resolvers: {Member: {session() {return {__typename: 'Session',isLoggedIn: someInternalLoginVerificationFunction(),connectionCount: calculateOpenConnections(),errors: sessionError(),};}}},});
Apollo Client 支持查询、突变和订阅的本地@client
结果与远程结果的合并。
异步本地解析器
Apollo客户端支持异步本地解析器函数。这些函数可以是async
函数或返回Promise
对象的普通函数。异步解析器在需要从异步API返回数据时非常有用。
⚠️ 如果您想从解析器中调用REST端点,我们推荐检查apollo-link-rest
,这是在Apollo Client中使用REST端点的更全面解决方案。
对于React Native和大多数浏览器API,您应该在组件的生命周期方法中设置监听器,并以回调形式传递您的mutation触发函数,而不是使用异步解析器。然而,异步async
解析器函数通常是消费异步设备API的最方便方式:
import { ApolloClient, InMemoryCache } from '@apollo/client';import { CameraRoll } from 'react-native';const client = new ApolloClient({cache: new InMemoryCache(),resolvers: {Query: {async cameraRoll(_, { assetType }) {try {const media = await CameraRoll.getPhotos({first: 20,assetType,});return {...media,id: assetType,__typename: 'CameraRoll',};} catch (e) {console.error(e);return null;}},},},});
CameraRoll.getPhotos()
返回一个Promise
对象,该对象包含一个edges
属性,该属性是一个相机节点对象的数组,以及一个page_info
属性,该属性包含分页信息。这是一个非常适合使用GraphQL的场景,因为我们可以将返回值过滤到只有我们的组件需要的数据。
import { gql } from "@apollo/client";const GET_PHOTOS = gql`query GetPhotos($assetType: String!) {cameraRoll(assetType: $assetType) @client {idedges {node {image {uri}location {latitudelongitude}}}}}`;
用缓存处理@client
字段
正如处理解析器中的@client字段中概述的,@client
字段可以用本地解析器函数解析。但是,值得注意的是,在使用@client指令时,并不总是需要本地解析器。带有@client标记的字段可以直接从缓存中提取匹配的值来本地解析。例如:
import React from "react";import ReactDOM from "react-dom";import {ApolloClient,InMemoryCache,HttpLink,ApolloProvider,useQuery,gql} from "@apollo/client";import Pages from "./pages";import Login from "./pages/login";const cache = new InMemoryCache();const client = new ApolloClient({cache,link: new HttpLink({ uri: "https://127.0.0.1:4000/graphql" }),resolvers: {},});const IS_LOGGED_IN = gql`query IsUserLoggedIn {isLoggedIn @client}`;cache.writeQuery({query: IS_LOGGED_IN,data: {isLoggedIn: !!localStorage.getItem("token"),},});function App() {const { data } = useQuery(IS_LOGGED_IN);return data.isLoggedIn ? <Pages /> : <Login />;}ReactDOM.render(<ApolloProvider client={client}><App /></ApolloProvider>,document.getElementById("root"),);
在上面的示例中,我们首先使用cache.writeQuery
来存储isLoggedIn
字段的值。之后,通过Apollo Client的useQuery
钩子运行IS_LOGGED_IN
查询,其中包含@client
指令。当Apollo Client执行IS_LOGGED_IN
查询时,它首先寻找可以用来处理@client
字段的本地解析器。当找不到时,它将尝试从缓存中提取指定的字段。所以在这个例子中,通过useQuery
钩子返回的data
值具有一个isLoggedIn
属性可供使用,它包含从缓存直接拉取的isLoggedIn
结果(!!localStorage.getItem('token')
)。
⚠️ 如果您想使用Apollo Client的@client
支持来查询缓存而不使用本地解析器,您必须将一个空对象传递到ApolloClient
构造函数的resolvers
选项中。没有这个选项,Apollo Client将不会启用其集成@client
支持,这意味着您的基于@client
的查询将被传递到Apollo Client链中。您可以在这里了解更多详细信息。
直接从缓存中提取@client
字段值并不像本地解析器函数那样灵活,因为本地解析器可以在返回结果前执行额外的计算。然而,根据应用程序的需求,直接从缓存中加载@client
字段可能是一个更简单的选项。Apollo Client并不限制同时使用这两种方法,所以可以根据需要混合和匹配。如果有需要,您可以同时在同一个查询中从缓存中提取一些@client
值,并用本地解析器解决其他值。
处理缓存策略
在Apollo Client执行查询之前,它首先会检查已配置的fetchPolicy
,以确定使用哪种策略。这样做是为了知道优先从缓存或网络中尝试解决查询的位置。当运行查询时,Apollo Client将基于@client
的本地解析器视同远程解析器一样对待,即它会遵循定义的fetchPolicy
,以确定首先尝试从哪里拉取数据。当与本地解析器一起工作时,理解fetch策略如何影响解析函数的运行非常重要,因为默认情况下本地解析函数不会在每次请求时运行。这是因为运行本地解析器的结果会与其他查询结果一起缓存,并在下一次请求时从缓存中检索。让我们看一个例子:
import React, { Fragment } from "react";import { useQuery, gql } from "@apollo/client";import { Loading, Header, LaunchDetail } from "../components";import { ActionButton } from "../containers";export const GET_LAUNCH_DETAILS = gql`query LaunchDetails($launchId: ID!) {launch(id: $launchId) {isInCart @clientsiterocket {type}}}`;export default function Launch({ launchId }) {const { loading, error, data } = useQuery(GET_LAUNCH_DETAILS,{ variables: { launchId } });if (loading) return <Loading />;if (error) return <p>ERROR: {error.message}</p>;return (<Fragment><Header image={data.launch.mission.missionPatch}>{data.launch.mission.name}</Header><LaunchDetail {...data.launch} /><ActionButton {...data.launch} /></Fragment>);}
在上面的例子中,我们使用Apollo Client的useQuery
钩子来运行GET_LAUNCH_DETAILS
查询。基于@client
的isInCart
字段已配置为从以下解析器拉取数据:
import { GET_CART_ITEMS } from './pages/cart';export const resolvers = {Launch: {isInCart: (launch, _, { cache }) => {const { cartItems } = cache.readQuery({ query: GET_CART_ITEMS });return cartItems.includes(launch.id);},},};
假设我们从空缓存开始。由于我们没有在useQuery
调用中指定fetchPolicy
属性,我们正在使用Apollo Client的默认cache-first
fetchPolicy。这意味着当运行
GET_LAUNCH_DETAILS
查询时,它会首先检查缓存以查看是否能找到结果。需要注意的是,当检查缓存时,整个查询都针对缓存执行,但任何基于@client
的本地解析器都会被跳过(不运行)。因此,使用以下内容查询缓存(相当于没有指定@client
指令):
launch(id: $launchId) {isInCartsiterocket {type}}
在这种情况下,无法从缓存中提取结果(因为我们的缓存为空),所以在幕后 Apollo 客户端 会继续沿查询执行路径向下移动。在下一步中,它会将原始查询基本分为两部分——包含 @client
字段的部分和将通过网络触发的部分。这两部分都被执行——从网络获取结果,并运行本地解析器来计算结果。然后合并本地解析器和网络的结果,并将最终结果写入缓存并返回。因此,在第一次运行后,我们现在在缓存中为原始 查询 有了一个结果,该结果包括 @client
部分和查询的网络部分的 数据。
当第二次运行 GET_LAUNCH_DETAILS
查询时,由于我们使用 Apollo 客户端' 的默认 fetchPolicy
为 cache-first
,首先会检查缓存以查找结果。这一次找到了该查询的完整结果,因此它将通过我们的 useQuery
调用返回。由于我们正在寻找的结果可以从缓存中提取,所以我们 @client
本地解析器不会被触发。
在很多情况下,将本地解析器视为远程解析器,使它们遵循相同的 fetchPolicy
,是非常有意义的。一旦获得所需的数据,无论这些数据是远程获取的还是使用本地解析器计算出来的,您都可以将其缓存起来,以避免在随后的请求中重新计算/重新获取。但假设您正在使用本地解析器来运行需要在每个请求上触发的计算?这可以通过几种不同的方式来处理。您可以切换查询以使用一个 fetchPolicy
,该策略强制在每次请求上运行整个 查询,例如 no-cache
或 network-only
。这将确保在每次请求上触发本地解析器,但也会确保您的基于网络的查询组件在每次请求上都会被触发。根据您的使用情况,这可能没问题,但如果您希望查询的网络部分利用缓存,而只想让 @client
部分在每次请求上运行,我们将详细说明这个更灵活的选项,在 @client(always: true)
部分。
使用 @client(always: true)
强制解析器
Apollo 客户端利用其缓存来帮助减少不断请求相同数据时所需的网络开销。默认情况下,@client
基于的字段与远程字段一样使用缓存。在运行本地解析器后,其结果将与任何远程结果一起缓存。这样,每当新的查询被触发并且其结果可以在缓存中找到时,都会使用这些结果,并且任何相关的本地解析器都不会再次被触发(除非数据被从缓存中删除或查询被更新为使用 no-cache
或 network-only
fetchPolicy
)。
虽然利用缓存来处理本地和远程结果在许多情况下非常有用,但并不是总是最佳选择。我们可能想使用一个本地的 resolver来计算一个需要在每次请求时刷新的动态值,同时继续使用缓存来处理查询的网络部分。为了支持这种情况,Apollo Client's @client
指令接受一个always 参数,当设置为true时,将确保关联本地 resolver 在每次请求时运行。以下是一个示例:
import { ApolloClient, InMemoryCache, gql } from '@apollo/client';const client = new ApolloClient({cache: new InMemoryCache(),resolvers: {Query: {isLoggedIn() {return !!localStorage.getItem('token');},},},});const IS_LOGGED_IN = gql`query IsUserLoggedIn {isLoggedIn @client(always: true)}`;// ... run the query using client.query, a <Query /> component, etc.
上面的isLoggedIn
resolver正在检查是否在localStorage
中存在一个认证令牌。在本例中,我们希望确保每次执行IS_LOGGED_IN
查询时,isLoggedIn
本地 resolver也触发,以便我们有最新的登录信息。为此,我们在查询的isLoggedIn
字段中使用了@client(always: true)
指令。如果我们没有包括always: true
,则本地 resolver将基于查询的fetchPolicy
触发,这意味着我们可能会收到对isLoggedIn
的缓存值。使用@client(always: true)
确保我们总是获取运行相关本地 resolver的直接结果。
⚠️ 请仔细考虑使用 @client(always: true)
的影响。虽然强迫本地 resolver在每次请求上运行可能是有用的,但如果该 resolver计算成本高昂或具有副作用,这可能会对您的应用程序产生负面影响。我们建议在尽可能使用缓存的同时,利用本地 resolver,以帮助提高应用程序性能。@client(always: true)在您的工具箱中很有用,但让本地 resolver遵循查询的 fetchPolicy应该是首选。
当@client(always: true)
确保总是执行本地 resolver 时,需要注意的是,如果一个 query 使用了依赖于缓存的fetchPolicy
(如cache-first
、cache-and-network
、cache-only
),则 query 仍然会首先尝试从缓存中解决,然后再执行本地 resolver。这是因为@client(always: true)
的使用可能与同一 query 中的普通@client
使用混用,这意味着我们希望部分 query 遵循定义的fetchPolicy
。这样做的好处是,可以从缓存中首先加载数据的任何内容都可以提供给您的@client(always: true)
resolver函数,因为它的第一个参数是。所以即使在您已使用@client(always: true)
来标识您希望始终运行特定的 resolver,在该 resolver 中,您也可以检查从 query 加载的缓存值,并根据需要决定是否运行 resolver。
使用 @client
字段作为变量
Apollo Client 提供了一种方法,可以将一个@client
字段结果用作同一操作中的 selection set 或字段的变量。 这样,我们不必首先运行一个基于@client
的查询,获取本地结果,然后使用加载的本地结果作为一个 variable 运行第二个查询,一切都可以在一个请求中处理。这是通过将@client
指令与@export(as: "variableName")
指令相结合来实现的。
import { ApolloClient, InMemoryCache, HttpLink, gql } from '@apollo/client';const query = gql`query CurrentAuthorPostCount($authorId: Int!) {currentAuthorId @client @export(as: "authorId")postCount(authorId: $authorId)}`;const cache = new InMemoryCache();const client = new ApolloClient({link: new HttpLink({ uri: 'https://127.0.0.1:4000/graphql' }),cache,resolvers: {},});cache.writeQuery({query: gql`query GetCurrentAuthorId { currentAuthorId }`,data: {currentAuthorId: 12345,},});// ... run the query using client.query, the <Query /> component, etc.
在上述示例中,currentAuthorId
首先从缓存中加载,然后通过后续的postCount
字段作为变量authorId
(通过@export(as: "authorId")
指令指定)传递进去。@export
指令还可以用于选择集中的特定字段,如下所示:
import { ApolloClient, InMemoryCache, HttpLink, gql } from '@apollo/client';const query = gql`query CurrentAuthorPostCount($authorId: Int!) {currentAuthor @client {nameauthorId @export(as: "authorId")}postCount(authorId: $authorId)}`;const cache = new InMemoryCache();const client = new ApolloClient({link: new HttpLink({ uri: 'https://127.0.0.1:4000/graphql' }),cache,resolvers: {},});cache.writeQuery({query: gql`query GetCurrentAuthor {currentAuthor {nameauthorId}}`,data: {currentAuthor: {__typename: 'Author',name: 'John Smith',authorId: 12345,},},});// ... run the query using client.query, the <Query /> component, etc.
在这里,authorId
变量是通过从缓存中加载的字段currentAuthor
设置的。@export
variable 的使用不仅限于远程查询;它还可以用于定义其他@client
字段或 selection sets 的变量:
import { ApolloClient, InMemoryCache, HttpLink, gql } from '@apollo/client';const query = gql`query CurrentAuthorPostCount($authorId: Int!) {currentAuthorId @client @export(as: "authorId")postCount(authorId: $authorId) @client}`;const cache = new InMemoryCache();const client = new ApolloClient({cache,resolvers: {Query: {postCount(_, { authorId }) {return authorId === 12345 ? 100 : 0;},},},});cache.writeQuery({query: gql`{ currentAuthorId }`,data: {currentAuthorId: 12345,},});// ... run the query using client.query, the <Query /> component, etc.
因此,这里是currentAuthorId
从缓存中加载,然后传递到postCount
本地 resolver 中作为authorId
。
关于 @export
使用的几点重要说明
当前,Apollo 客户端仅支持使用
@export
指令存储 变量 用于本地数据。@export
必须与@client
结合使用。@client @export
的使用可能看起来与 GraphQL 规范相冲突,因为操作执行的顺序可能会影响结果。这可以从 正常和串行执行 部分 GraphQL 规范中得知:...除了顶级 mutation 字段外的 fields 的解析必须是幂等的、无副作用的,执行顺序不得影响结果,因此服务器可以自由地按其认为最优的顺序执行字段条目。
Apollo 客户端当前仅支持在使用
@client
指令时与@export
指令一起使用。它通过首先运行带有@client @export
指令的操作,提取指定的@export
变量,然后尝试从本地缓存或本地 resolvers 中解析这些变量的值,来准备@export
变量。一旦建立了一个变量名到本地值的映射,该映射就用于填充在运行基于服务器的 GraphQL 查询时传递的变量。基于服务器的 GraphQL 查询的执行顺序不受@export
的影响;变量在服务器查询运行之前就准备好了,因此遵守了规范。如果在一个操作中定义了多个具有相同名称的
@export
变量,那么将使用最后一个@export
变量的值作为后续的变量值。当这种情况发生时,Apollo 客户端将会记录一个警告消息(仅限开发模式)。
缓存管理
当您使用 Apollo 客户端处理本地状态时,您的 Apollo 缓存成为您所有本地和远程数据的单一真相来源。
cache.writeQuery
更新缓存最简单的方法是使用 cache.writeQuery
。以下是如何在您的 resolving 映射中使用它以执行简单更新的示例:
import { ApolloClient, InMemoryCache } from '@apollo/client';const client = new ApolloClient({cache: new InMemoryCache(),resolvers: {Mutation: {updateVisibilityFilter: (_, { visibilityFilter }, { cache }) => {cache.writeQuery({query: gql`query GetVisibilityFilter { visibilityFilter }`,data: {__typename: 'Filter',visibilityFilter,},});},},},};
缓存writeFragment
方法允许您传递一个可选的id
属性,以将一个片段写入缓存中已存在的对象。如果您想向缓存中已存在的对象添加一些客户端字段,这将非常有用。
import { ApolloClient, InMemoryCache } from '@apollo/client';const client = new ApolloClient({cache: new InMemoryCache(),resolvers: {Mutation: {updateUserEmail: (_, { id, email }, { cache }) => {cache.writeFragment({id: cache.identify({ __typename: "User", id }),fragment: gql`fragment UserEmail on User { email }`,data: { email },});},},},};
cache.writeQuery
和cache.writeFragment
方法应该覆盖您的大部分需求;然而,有些情况下,您要写入缓存的数据依赖于已存在的数据。在这种情况下,您可以组合使用cache.read{Query,Fragment}
后跟cache.write{Query,Fragment}
,或者使用cache.modify({ id, fields })
来更新通过id
标识的实体对象中特定fields
。
writeQuery and readQuery
有时,您写入缓存的数据依赖于缓存中已有的数据;例如,您向列表中添加一个条目或设置基于现有属性值的属性。在这种情况下,您应该使用cache.modify
来更新特定现有的fields
。让我们看一个示例,其中我们向列表中添加一个待办事项:
import { ApolloClient, InMemoryCache, gql } from '@apollo/client';let nextTodoId = 0;const cache = new InMemoryCache();cache.writeQuery({query: gql`query GetTodos { todos { ... } }`,data: { todos: [] },});const client = new ApolloClient({resolvers: {Mutation: {addTodo: (_, { text }, { cache }) => {const query = gql`query GetTodos {todos @client {idtextcompleted}}`;const previous = cache.readQuery({ query });const newTodo = { id: nextTodoId++, text, completed: false, __typename: 'TodoItem' };const data = {todos: [...previous.todos, newTodo],};cache.writeQuery({ query, data });return newTodo;},},},});
为了将我们的待办事项添加到列表,我们需要缓存中现有的待办事项,这也是为什么我们需要调用cache.readQuery
来检索它们的原因。cache.readQuery
如果数据不在缓存中,将抛出一个错误,因此我们需要提供一个初始状态。这就是为什么我们在创建InMemoryCache
后,使用空待办事项数组调用cache.writeQuery
的原因。
writeFragment and readFragment
cache.readFragment
类似于cache.readQuery
,except you pass in afragment
。这提供了更大的灵活性,因为只要您有它的缓存键,就可以从缓存中的任何条目读取。相比之下,cache.readQuery
只能从缓存的根读取。
让我们回到之前的待办事项列表示例,看看cache.readFragment
如何帮助我们切换待办事项的状态为完成。
import { ApolloClient, InMemoryCache } from '@apollo/client';const client = new ApolloClient({resolvers: {Mutation: {toggleTodo: (_, variables, { cache }) => {const id = `TodoItem:${variables.id}`;const fragment = gql`fragment CompleteTodo on TodoItem {completed}`;const todo = cache.readFragment({ fragment, id });const data = { ...todo, completed: !todo.completed };cache.writeFragment({ fragment, id, data });return null;},},},});
为了切换我们的待办事项,我们需要从缓存中获取待办事项及其状态,这就是为什么我们调用cache.readFragment
并传入一个fragment
来检索它的原因。id
我们传递给cache.readFragment
的是它的缓存键。如果您使用的是InMemoryCache
且没有覆盖dataIdFromObject
配置属性,则您的缓存键应该是__typename:id
。
高级
代码拆分
根据您本地解析器的复杂性和大小,您不一定需要在创建初始 ApolloClient
实例时立即定义它们。如果您只需在应用程序的特定部分使用本地解析器,可以利用 Apollo Client's addResolvers
和 setResolvers
函数在任何时候调整您的 resolver 映射。这在使用基于路由的代码分割等技术时非常有用,例如使用 react-loadable
。
假设我们正在构建一个消息应用,并有一个 /stats
路由,用于返回存储在本地消息的总数。如果我们使用 react-loadable
加载我们的 Stats
组件,像这样:
import Loadable from 'react-loadable';import Loading from './components/Loading';export const Stats = Loadable({loader: () => import('./components/stats/Stats'),loading: Loading,});
并在我们的 Stats
组件被调用时定义我们的本地 resolvers(使用 addResolvers
):
import React from "react";import { ApolloConsumer, useApolloClient, useQuery, gql } from "@apollo/client";const GET_MESSAGE_COUNT = gql`query GetMessageCount {messageCount @client {total}}`;const resolvers = {Query: {messageCount: (_, args, { cache }) => {// ... calculate and return the number of messages in// the cache ...return {total: 123,__typename: "MessageCount",};},},};export function MessageCount() {const client = useApolloClient();client.addResolvers(resolvers);const { loading, data: { messageCount } } = useQuery(GET_MESSAGE_COUNT);if (loading) return "Loading ...";return (<p>Total number of messages: {messageCount.total}</p>);};
我们的本地 resolver 代码将仅在用户访问 /stats
时才会包含在用户下载的包中。它不会包含在初始应用程序包中,这有助于减少初始包的大小,从而最终有助于下载和应用程序启动时间。
API
Apollo Client 本地状态处理是内置的,因此您不需要安装任何额外的软件。可以在 ApolloClient
实例化时(通过 ApolloClient
构造函数)或使用 ApolloClient
的本地状态 API)配置本地状态管理。可以通过 ApolloCache
API 来管理缓存中的数据。
ApolloClient
构造函数
import { ApolloClient, InMemoryCache } from '@apollo/client';const client = new ApolloClient({cache: new InMemoryCache(),resolvers: { ... },typeDefs: { ... },});
选项 | 类型 | 描述 |
---|---|---|
resolvers? | 解析器|解析器[] | 一个解析器函数的映射,您的 GraphQL 查询和突变调用这些函数来读取和写入缓存。 |
typeDefs? | 字符串|字符串[]|DocumentNode|DocumentNode[];<字符串> | 一个字符串,表示您的客户端模式,该模式使用 Schema Definition Language 编写。此模式不用于验证,而是由 Apollo Client Devtools 用于反射。 |
这些选项都不是必需的。如果您不指定任何东西,您仍然可以使用 @client
指令来查询 Apollo Client 缓存。
方法
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';const client = new ApolloClient({cache: new InMemoryCache(),link: new HttpLink({ uri: 'https://127.0.0.1:4000/graphql' }),});client.setResolvers({ ... });
方法 | 描述 |
---|---|
addResolvers(resolvers: Resolvers|Resolvers[]) | 一个图中包含了解析函数,当调用 GraphQL 查询和突变以便读写缓存时,会调用这些解析函数。通过 addResolvers 添加的解析函数会被添加到内部解析函数图中,这意味着任何现有的解析函数(未重写的)都会被保留。 |
setResolvers(resolvers: Resolvers|Resolvers[]) | 一个图中包含了解析函数,当调用 GraphQL 查询和突变以便读写缓存时,会调用这些解析函数。通过 setResolvers 添加的解析函数会覆盖所有现有的解析函数(在添加新解析函数之前,现有的解析函数图会被清除)。 |
getResolvers | 获取当前定义的解析函数图。 |
setLocalStateFragmentMatcher(fragmentMatcher: FragmentMatcher) | 设置一个自定义的 FragmentMatcher 用来解析本地查询。 |
TypeScript 接口/类型
interface Resolvers {[key: string]: {[field: string]: (rootValue?: any,args?: any,context?: any,info?: any,) => any;};}type FragmentMatcher = (rootValue: any,typeCondition: string,context: any,) => boolean;
ApolloCache
方法
import { InMemoryCache } from '@apollo/client';const cache = new InMemoryCache();cache.writeQuery({query: gql`query MyQuery {isLoggedIn,cartItems}`,data: {isLoggedIn: !!localStorage.getItem('token'),cartItems: [],},});
方法 | 描述 |
---|---|
writeQuery({ query, variables, data }) | 使用指定的查询向缓存根写入数据,以确保要写入缓存的数据形状与查询所需的数据形状相同。对于用初始数据准备缓存非常有益。 |
readQuery({ query, variables }) | 读取指定查询的缓存数据。 |
writeFragment({ id, fragment, fragmentName, variables, data }) | 与 writeQuery 类似(写入缓存数据),但使用指定的片段来验证要写入缓存的数据形状与片段所需的数据形状相同。 |
readFragment({ id, fragment, fragmentName, variables }) | 读取指定片段的缓存数据。 |
弃用通知
使用客户端 resolvers 来管理本地状态的构想首次被引入到 Apollo Client 生态系统中,这是通过 apollo-link-state
项目实现的。Apollo Client 团队一直在寻找改进本地状态管理的方法,因此我们决定将本地 resolver 和 @client
支持直接集成到 Apollo Client 核心中,自 2.5 版本开始。虽然使用本地 resolvers 管理状态效果良好,但 apollo-link-state
提供的功能以及从 Apollo Client 直接提供的功能,由于与 Apollo Client 缓存的距离,原本设计中存在一些限制。 Apollo Link's 缓存没有直接访问权限,这意味着 apollo-link-state
必须实施一种无法像我们所希望的那样无缝地注入或钩入缓存的方法。Apollo Client 在 2.5 版本中集成的本地 resolver 支持本质上是对 Link 方法的镜像,但作了一些调整以更紧密地连接到缓存。这意味着 Apollo Client 的本地 resolver 方法在更紧密地与缓存协作时仍有一定局限性,并且最终为开发者提供了更好的体验。
为了解决本地 resolver API 的局限性,我们在 Apollo Client 3.0 中设计了新的本地状态管理方法,这种方法作为缓存的直接扩展。 字段策略和响应变量不仅从 API 使用和功能角度提供了更好的开发者体验,还提高了性能,并提供了更低级的本地状态管理基础。考虑到 Apollo Client 缓存来重新思考本地状态处理,有助于减少由于本地 resolver 与缓存内部结构距离过远而引起的大量本地状态错误。
关于 Apollo Client 3 的新本地状态管理功能,使用字段策略管理状态部分进行了更详细的说明。我们强烈建议审查并考虑使用这些新的 API 作为本地 resolvers 的替代方案。尽管 Apollo Client 3 仍然支持本地 resolver,但将在未来的重大版本中将它移出核心模块。