Apollo Client 中的本地字段
使用单个 GraphQL 查询检索本地和远程数据
您的Apollo Client查询可以包含只限本地使用的字段,这些字段没有定义在您的GraphQL服务器's schema:
query ProductDetails($productId: ID!) {product(id: $productId) {namepriceisInCart @client # This is a local-only field}}
这些字段的值通过你想要的任何逻辑在本地上进行计算,例如从localStorage
。
如所示,一个查询可以包含仅本地字段 和 字段,这些字段是从你的GraphQL服务器获得的。
定义
假设我们正在构建一个电子商务应用程序。大多数产品详细信息存储在我们的后端服务器上,但我们要定义一个Product.isInCart
布尔字段,它是客户端本地的。首先,我们为isInCart
创建一个字段策略。
字段策略指定了如何从你的Apollo客户端缓存中读取和写入单个GraphQL字段的自定义逻辑。你可以为本地只读字段和远程检索字段定义字段策略。
字段策略主要在自定义缓存字段的行为中进行文档说明。本文特别说明了如何与本地只读字段一起使用它们。
你将在一个映射中定义你应用程序的字段策略,并将其提供给Apollo客户端的InMemoryCache
构造函数。每个字段策略都是特定类型策略的子策略(就像相应的字段是特定类型的子字段一样)。
以下是一个示例InMemoryCache
构造函数,它定义了Product.isInCart
的字段策略:
const cache = new InMemoryCache({typePolicies: { // Type policy mapProduct: {fields: { // Field policy map for the Product typeisInCart: { // Field policy for the isInCart fieldread(_, { variables }) { // The read function for the isInCart fieldreturn localStorage.getItem('CART').includes(variables.productId);}}}}}});
查询
const GET_PRODUCT_DETAILS = gql`query ProductDetails($productId: ID!) {product(id: $productId) {namepriceisInCart @client}}`;
注意:如果你将@client
指令应用于具有子字段的字段,该指令将自动应用于所有子字段。
存储和突变
您可以使用Apollo Client查询和修改本地状态,无论您如何存储该状态。Apollo Client为表示本地状态提供了一些可选但非常有用的机制:
考虑仅本地突变,这些突变在其他本地字段中表达得类似。在使用Apollo Client的旧版本时,开发人员会使用@client
定义仅本地的突变。然后,他们可以使用局部解析器来处理client.mutate
/useMutation
调用。但是,现在不再是这种情况。对于Apollo Client版本 > = 3的用户,我们建议使用writeQuery
、writeFragment
或响应式变量来管理本地状态。
使用响应式变量存储和更新本地状态
Apollo Client的响应式响应式变量非常适合表示本地状态:
- 您可以从应用中的任何地方读取和修改响应式变量,而无需使用GraphQL操作。
- 与Apollo Client缓存不同,响应式变量不强制执行数据规范化,这意味着您可以以任何格式存储数据。
- 如果一个字段's的值依赖于一个响应式变量's的值,并且那个变量的值改变,那么包含该字段的每个活跃查询都会自动刷新。
示例
回到我们的电子商务应用程序,假设我们想获取一个用户的购物车中的项目ID列表,并且这个列表是本地存储的。执行该查询的方法如下:
export const GET_CART_ITEMS = gql`query GetCartItems {cartItems @client}`;
让我们使用makeVar
函数来初始化一个存储我们本地购物项目列表的响应式变量:
import { makeVar } from '@apollo/client';export const cartItemsVar = makeVar([]);
这初始化了一个包含空数组的响应式变量(你可以传递任何初始值到makeVar
)。请注意,makeVar
的返回值不是变量本身,而是一个函数。我们通过调用cartItemsVar()
来获取变量的当前值,并通过调用cartItemsVar(newValue)
来设置一个新的新值。
接下来,让我们定义字段策略,用于cartItems
。和往常一样,我们将它传递给InMemoryCache
的构造函数:
export const cache = new InMemoryCache({typePolicies: {Query: {fields: {cartItems: {read() {return cartItemsVar();}}}}}});
以下read函数在查询cartItems
时返回我们的响应式变量的值。
现在,让我们创建一个按钮组件,使用户能够将产品添加到购物车
import { cartItemsVar } from './cache';// ... other importsexport function AddToCartButton({ productId }) {return (<div class="add-to-cart-button"><Button onClick={() => cartItemsVar([...cartItemsVar(), productId])}>Add to Cart</Button></div>);}
单击时,此按钮更新cartItemsVar
的值,以便将按钮关联的productId
附加到。当这发生时,Apollo Client将通知每个包含cartItems字段
这是一个Cart
组件,它使用GET_CART_ITEMS
查询
export const GET_CART_ITEMS = gql`query GetCartItems {cartItems @client}`;export function Cart() {const { data, loading, error } = useQuery(GET_CART_ITEMS);if (loading) return <Loading />;if (error) return <p>ERROR: {error.message}</p>;return (<div class="cart"><Header>My Cart</Header>{data && data.cartItems.length === 0 ? (<p>No items in your cart</p>) : (<Fragment>{data && data.cartItems.map(productId => (<CartItem key={productId} />))}</Fragment>)}</div>);}
组件会在cartItemsVar
发生变化时自动刷新:
import { useReactiveVar } from '@apollo/client';export function Cart() {const cartItems = useReactiveVar(cartItemsVar);return (<div class="cart"><Header>My Cart</Header>{cartItems.length === 0 ? (<p>No items in your cart</p>) : (<Fragment>{cartItems.map(productId => (<CartItem key={productId} />))}</Fragment>)}</div>);}
与前面的useQuery
示例一样,每当cartItemsVar
变量
重要:如果您在上面的示例中使用cartItemsVar
而不是useReactiveVar(cartItemsVar)
,未来的变量更新将不会导致Cart
组件重新渲染
在缓存中存储和修改本地状态
直接在Apollo Client缓存中存储本地状态提供了一些优势,但通常需要的代码比使用响应式变量多:
- 您无需定义字段策略的本地字段,因为这些字段仅存在于缓存中。如果您查询一个未定义函数的字段,则默认情况下,Apollo Client会尝试直接从缓存中获取该字段的值。
- 当您修改缓存的字段与以下操作之一相关时
writeQuery
或writeFragment
,则包含该字段的每个活动查询都会自动刷新。
示例
例如,我们的应用程序定义以下查询:
const IS_LOGGED_IN = gql`query IsUserLoggedIn {isLoggedIn @client}`;
此isLoggedIn
字段是本地字段。我们可以使用的writeQuery
方法来将一个值直接写入此字段到Apollo Client缓存中,如下所示:
cache.writeQuery({query: IS_LOGGED_IN,data: {isLoggedIn: !!localStorage.getItem("token"),},});
这会根据localStorage
是否存在一个token
来写入一个布尔值,指示一个活动的会话。
现在,我们的应用程序的组件可以根据isLoggedIn
字段的值进行了渲染,无需为它定义read
函数:
function App() {const { data } = useQuery(IS_LOGGED_IN);return data.isLoggedIn ? <Pages /> : <Login />;}
这里有一个包含上述代码块的完整示例
请注意,即使在您将本地数据作为字段存储在Apollo Client缓存中,您仍然可以(并可能应该!)定义read
函数来处理这些字段。一个read
函数可以执行有用的自定义逻辑,例如在字段不在缓存中时返回默认值。
跨会话持久本地状态
默认情况下,响应式变量和InMemoryCache
在会话之间不会持久化其状态(例如,如果用户刷新浏览器)。要持久化此状态,您需要添加相应的逻辑。
apollo3-cache-persist
库帮助您在会话之间持久化和重新活化Apollo Client缓存。详情请参阅持久化缓存。
目前还没有用于持久化响应式变量的内置API,但您可以在它们被修改时将变量值写入localStorage
(或其他存储),在应用加载时用存储的值(如果有的话)初始化这些变量。
修改
修改本地仅字段值的方法取决于您如何存储该字段:
如果您正在使用响应式变量,您只需要设置响应式变量的新值。存储位置。Apollo Client会自动检测该更改并触发包含受影响字段的每个活动操作刷新。
如果您直接使用缓存(请见直接使用缓存部分),请调用
writeQuery
,writeFragment
,或者cache.modify
(此处有完整文档))来修改缓存字段。这些方法类似于响应式变量,都会触发每个受影响的活动操作刷新。如果您使用其他存储方法,例如
localStorage
,请使用您应用的方法设置字段的新的值。然后,您可以通过调用cache.evict
强制刷新每个受影响的操作。在您的调用中,提供包含该字段的实体的id
以及只存在于本地的字段的名称。
将本地专用字段作为 GraphQL 变量使用
如果您的 GraphQL 查询使用变量,则该查询的本地专用字段可以提供这些变量的 值。
为了这样做,您可以在字段上应用 @export(as: "variableName")
指令,如下所示:
const GET_CURRENT_AUTHOR_POST_COUNT = gql`query CurrentAuthorPostCount($authorId: Int!) {currentAuthorId @client @export(as: "authorId")postCount(authorId: $authorId)}`;
在上面的查询中,本地专用字段 currentAuthorId
的结果用作传递给 postCount
的 $authorId
变量的值。
即使 postCount
也是一个本地专用字段(即,如果它也标记为 @client
),您也可以这样做。
@export
指令的使用注意事项
要使用
@export
指令,一个字段 还必须使用 @client 指令。换句话说,只有本地专用字段可以用于变量值。必须放在任何使用该变量的字段之前的字段才可以将变量值
@export
。如果操作中的多个字段都使用
@export
指令将它们的值分配给同一个变量,则列出的最后一个字段具有优先权。在开发模式下发生这种情况时,Apollo Client 会记录一条警告信息。初看之下,
@export
指令似乎违反了 GraphQL 规范的要求 ,即操作执行的顺序不应影响其结果:…非顶层 变异 字段的解析必须始终是无副作用的和幂等的,执行的顺序不应影响结果,因此服务器有自由按其认为最优化顺序执行字段条目。
然而,所有
@export
输出的 变量 值都应在将操作发送到远程服务器之前填充。只有本地字段可以使用@export
指令,这些字段在传输之前会被从 操作 中移除。