Skip to content

GraphQL - 缓存

GraphQL - 使用 Apollo Client 实现缓存

Section titled “GraphQL - 使用 Apollo Client 实现缓存”

缓存是 GraphQL 客户端库(如 Apollo Client)最强大的功能之一。通过在本地存储先前获取的数据,客户端可以避免冗余的网络请求,从而实现更快的 UI 更新、减轻服务器负载,并提供更好的用户体验,尤其是在慢速或不可靠的网络环境下。

Apollo Client 的缓存主要由 InMemoryCache 处理。

InMemoryCache 在存储数据之前对其进行规范化(normalization)。规范化意味着:

  • **唯一标识:**每个具有 __typename 和唯一标识符(默认是 id 或 _id,或通过 typePolicies 中的 keyFields 指定的自定义键)的对象只在扁平查找表中存储一次。
  • **按 ID 存储:**缓存通过其唯一的缓存 ID(通常是 ${__typename}:${id})索引对象进行存储。
  • **用引用代替对象:**当一个对象引用另一个对象时(例如,一个 Student 对象有一个 college 字段),缓存存储对相关对象的引用(即缓存 ID),而不是嵌套完整的对象数据。
  • **自动更新:**当缓存中的对象被更新时(例如,通过 mutation 结果),任何引用该对象的查询结果都会自动看到更新后的数据,而无需重新获取。

这种规范化使得 Apollo Client 能够高效地管理可能重叠或深度嵌套的查询结果,并保持 UI 的一致性。

Apollo Client 关于缓存和网络请求的行为由 fetch 策略控制。你可以在初始化客户端时设置默认策略,或者在 useQuery 中使用 fetchPolicy 选项为每个查询指定策略。

常见的 fetch 策略:

  • **cache-first (默认):**首先检查缓存。如果所有请求的数据都存在,则从缓存返回。否则,发起网络请求。网络请求后更新缓存。
  • **cache-and-network:**首先检查缓存(如果可用,立即返回数据)。然后,总是发起网络请求。如果网络数据不同,则更新缓存并可能再次更新 UI。适用于快速初始加载 + 后台更新。
  • **network-only:**总是发起网络请求,初始读取时忽略缓存。请求后更新缓存。
  • **cache-only:**只检查缓存。从不发起网络请求。如果数据不在缓存中,则抛出错误。
  • **no-cache:**类似于 network-only,但在网络请求完成后甚至不更新缓存。

选择正确的 fetch 策略取决于特定查询所需的数据新鲜度。

我们将创建一个简单的 React 应用,包含两个路由:‘Home’ 和 ‘Students’。‘Students’ 路由将使用 Apollo Client 获取数据。我们会在查询中包含服务器时间戳,以便观察何时实际发起了网络请求以及何时数据从缓存中提供。

确保你有一个 GraphQL 服务器正在运行(类似于之前的示例),并包含以下 schema 和 resolvers:

#graphql
type Query {
students: [Student]
# 获取当前服务器时间的字段
serverTime: String
}
type Student {
id: ID!
firstName: String!
lastName: String
}
// resolvers.js
// 假设 db.students.list() 在之前的示例中存在
import { db } from './db.js';
const resolvers = {
Query: {
students: () => {
console.log("Resolving: Query.students");
return db.students.list();
},
serverTime: () => {
const now = new Date();
console.log(`Resolving: Query.serverTime at ${now.toLocaleTimeString()}`);
return now.toISOString(); // 返回 ISO 字符串以保持一致性
},
},
};
export default resolvers;

启动你的 GraphQL 服务器(例如,npm run dev)。

如果你还没有 React 应用,请创建一个。

npm install @apollo/client graphql react-router-dom

我们添加 react-router-dom 用于路由之间的导航。

步骤 2:配置 Apollo Client 与 Router

Section titled “步骤 2:配置 Apollo Client 与 Router”

在你的主入口文件(例如,src/main.jsx)中设置客户端和路由器。

src/main.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';
import './index.css';
// 配置 Apollo Client
const client = new ApolloClient({
uri: 'http://localhost:9000/graphql', // 你的服务器 URL
cache: new InMemoryCache(),
});
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<ApolloProvider client={client}>
<BrowserRouter> { /* 使用 BrowserRouter 或 HashRouter */ }
<App />
</BrowserRouter>
</ApolloProvider>
</React.StrictMode>
);

步骤 3:创建组件和路由 (App.jsx)

Section titled “步骤 3:创建组件和路由 (App.jsx)”

设置基本的路由和导航。

src/App.jsx
import React from 'react';
import { Routes, Route, Link } from 'react-router-dom';
import StudentsPage from './StudentsPage'; // 接下来我们将创建它
import HomePage from './HomePage'; // 接下来我们将创建它
import './App.css';
function App() {
return (
<div>
<nav>
<ul>
<li><Link to="/">Home</Link></li>
<li><Link to="/students">Students</Link></li>
</ul>
</nav>
<hr />
<main>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/students" element={<StudentsPage />} />
</Routes>
</main>
</div>
);
}
export default App;
// src/HomePage.jsx
import React from 'react';
function HomePage() {
const clientTime = new Date().toLocaleTimeString();
return (
<div>
<h2>Home Page</h2>
<p>Welcome! This page doesn't fetch GraphQL data.</p>
<p>Current Client Time: {clientTime}</p>
</div>
);
}
export default HomePage;

步骤 5:创建 StudentsPage.jsx (获取数据)

Section titled “步骤 5:创建 StudentsPage.jsx (获取数据)”

这个组件将使用 useQuery 获取学生和服务器时间。

// src/StudentsPage.jsx
import React from 'react';
import { useQuery, gql } from '@apollo/client';
const GET_STUDENTS_AND_TIME = gql`
query GetStudentsAndTime {
serverTime
students {
id
firstName
}
}
`;
function StudentsPage() {
console.log('Rendering StudentsPage...');
// 使用默认的 fetchPolicy: 'cache-first'
const { loading, error, data, refetch } = useQuery(GET_STUDENTS_AND_TIME, {
notifyOnNetworkStatusChange: true, // 即使重新获取时也显示加载状态
});
const clientTime = new Date().toLocaleTimeString();
console.log('Query State:', { loading, error, data });
return (
<div>
<h2>Students List</h2>
<p>Current Client Time: {clientTime}</p>
<button onClick={() => refetch()} disabled={loading}>
Refetch Data
</button>
{loading && <p>Loading data from server...</p>}
{error && <p>Error fetching data: {error.message}</p>}
{data && (
<div>
<h3>Data fetched at Server Time: {new Date(data.serverTime).toLocaleTimeString()}</h3>
<h4>Students:</h4>
<ul>
{data.students.map(s => (
<li key={s.id}>{s.firstName}</li>
))}
</ul>
</div>
)}
</div>
);
}
export default StudentsPage;
  1. 启动你的 React 开发服务器(例如,npm run dev)。

  2. 在浏览器中打开应用,并打开开发者控制台。

  3. **导航到 ‘Students’:**第一次访问时,你会短暂看到“加载中…”信息。检查你的服务器终端和浏览器控制台的日志。服务器应该记录它解析了 Query.students 和 Query.serverTime。浏览器会记录组件的渲染和最终数据。注意显示的“服务器时间”。

  4. **导航到 ‘Home’:**回到主页。

  5. 导航回 ‘Students’:这一次,数据应该立即出现,不会有“加载中…”信息(除非数据被缓存垃圾回收了,但这对于这个简单案例不太可能)。至关重要的是,显示的“服务器时间”应该与你第一次访问时相同。检查服务器日志——你应该不会看到解析该查询的新日志。这表明数据是从 Apollo Client 缓存中提供的。

  6. **点击 ‘Refetch Data’:**你会再次短暂看到“加载中…”信息。服务器日志会显示该查询再次被解析。“服务器时间”在页面上会更新为新的时间。

修改 StudentsPage.jsx 中的 useQuery,尝试不同的 fetch 策略:

// 尝试:fetchPolicy: 'network-only'
const { loading, error, data } = useQuery(GET_STUDENTS_AND_TIME, {
fetchPolicy: 'network-only'
});
// 尝试:fetchPolicy: 'cache-and-network'
// const { loading, error, data } = useQuery(GET_STUDENTS_AND_TIME, {
// fetchPolicy: 'cache-and-network'
// });

观察在 ‘Home’ 和 ‘Students’ 之间来回导航时行为的变化。network-only 将始终显示“加载中…”并获取最新数据。cache-and-network 将立即从缓存加载数据,并触发后台重新获取,可能会在初始渲染后不久更新服务器时间。

  • **typePolicies:**允许对不同类型如何缓存进行细粒度控制,包括定义自定义键字段(如果 id 不是唯一标识符)以及字段级别的读/合并函数。
  • 缓存清除/垃圾回收:InMemoryCache 会自动移除断开连接的对象。你可以配置限制或手动清除数据。
  • **直接访问缓存:**使用 client.readQuery、client.writeQuery、client.readFragment、client.writeFragment 和 client.cache.modify 进行命令式缓存操作(常用于 mutation 的 update 函数)。

掌握 Apollo Client 的缓存是构建高性能 GraphQL 应用的关键。有关高级配置,请参阅官方文档。