GraphQL - 缓存
GraphQL - 使用 Apollo Client 实现缓存
Section titled “GraphQL - 使用 Apollo Client 实现缓存”缓存是 GraphQL 客户端库(如 Apollo Client)最强大的功能之一。通过在本地存储先前获取的数据,客户端可以避免冗余的网络请求,从而实现更快的 UI 更新、减轻服务器负载,并提供更好的用户体验,尤其是在慢速或不可靠的网络环境下。
Apollo Client 的缓存主要由 InMemoryCache 处理。
InMemoryCache 工作原理:规范化
Section titled “InMemoryCache 工作原理:规范化”InMemoryCache 在存储数据之前对其进行规范化(normalization)。规范化意味着:
- **唯一标识:**每个具有
__typename和唯一标识符(默认是id或_id,或通过typePolicies中的keyFields指定的自定义键)的对象只在扁平查找表中存储一次。 - **按 ID 存储:**缓存通过其唯一的缓存 ID(通常是
${__typename}:${id})索引对象进行存储。 - **用引用代替对象:**当一个对象引用另一个对象时(例如,一个
Student对象有一个college字段),缓存存储对相关对象的引用(即缓存 ID),而不是嵌套完整的对象数据。 - **自动更新:**当缓存中的对象被更新时(例如,通过 mutation 结果),任何引用该对象的查询结果都会自动看到更新后的数据,而无需重新获取。
这种规范化使得 Apollo Client 能够高效地管理可能重叠或深度嵌套的查询结果,并保持 UI 的一致性。
Fetch 策略
Section titled “Fetch 策略”Apollo Client 关于缓存和网络请求的行为由 fetch 策略控制。你可以在初始化客户端时设置默认策略,或者在 useQuery 中使用 fetchPolicy 选项为每个查询指定策略。
常见的 fetch 策略:
- **
cache-first(默认):**首先检查缓存。如果所有请求的数据都存在,则从缓存返回。否则,发起网络请求。网络请求后更新缓存。 - **
cache-and-network:**首先检查缓存(如果可用,立即返回数据)。然后,总是发起网络请求。如果网络数据不同,则更新缓存并可能再次更新 UI。适用于快速初始加载 + 后台更新。 - **
network-only:**总是发起网络请求,初始读取时忽略缓存。请求后更新缓存。 - **
cache-only:**只检查缓存。从不发起网络请求。如果数据不在缓存中,则抛出错误。 - **
no-cache:**类似于network-only,但在网络请求完成后甚至不更新缓存。
选择正确的 fetch 策略取决于特定查询所需的数据新鲜度。
示例:演示缓存行为
Section titled “示例:演示缓存行为”我们将创建一个简单的 React 应用,包含两个路由:‘Home’ 和 ‘Students’。‘Students’ 路由将使用 Apollo Client 获取数据。我们会在查询中包含服务器时间戳,以便观察何时实际发起了网络请求以及何时数据从缓存中提供。
确保你有一个 GraphQL 服务器正在运行(类似于之前的示例),并包含以下 schema 和 resolvers:
Schema (schema.graphql 或 server.js 中)
Section titled “Schema (schema.graphql 或 server.js 中)”#graphqltype Query { students: [Student] # 获取当前服务器时间的字段 serverTime: String}
type Student { id: ID! firstName: String! lastName: String}Resolvers (resolvers.js)
Section titled “Resolvers (resolvers.js)”// 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 客户端
Section titled “设置 React 客户端”如果你还没有 React 应用,请创建一个。
步骤 1:安装依赖
Section titled “步骤 1:安装依赖”npm install @apollo/client graphql react-router-dom我们添加 react-router-dom 用于路由之间的导航。
步骤 2:配置 Apollo Client 与 Router
Section titled “步骤 2:配置 Apollo Client 与 Router”在你的主入口文件(例如,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 Clientconst 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)”设置基本的路由和导航。
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;步骤 4:创建 HomePage.jsx
Section titled “步骤 4:创建 HomePage.jsx”// src/HomePage.jsximport 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.jsximport 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;步骤 6:运行并观察缓存
Section titled “步骤 6:运行并观察缓存”-
启动你的 React 开发服务器(例如,
npm run dev)。 -
在浏览器中打开应用,并打开开发者控制台。
-
**导航到 ‘Students’:**第一次访问时,你会短暂看到“加载中…”信息。检查你的服务器终端和浏览器控制台的日志。服务器应该记录它解析了
Query.students和Query.serverTime。浏览器会记录组件的渲染和最终数据。注意显示的“服务器时间”。 -
**导航到 ‘Home’:**回到主页。
-
导航回 ‘Students’:这一次,数据应该立即出现,不会有“加载中…”信息(除非数据被缓存垃圾回收了,但这对于这个简单案例不太可能)。至关重要的是,显示的“服务器时间”应该与你第一次访问时相同。检查服务器日志——你应该不会看到解析该查询的新日志。这表明数据是从 Apollo Client 缓存中提供的。
-
**点击 ‘Refetch Data’:**你会再次短暂看到“加载中…”信息。服务器日志会显示该查询再次被解析。“服务器时间”在页面上会更新为新的时间。
尝试不同的 Fetch 策略
Section titled “尝试不同的 Fetch 策略”修改 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 将立即从缓存加载数据,并触发后台重新获取,可能会在初始渲染后不久更新服务器时间。
缓存配置与控制
Section titled “缓存配置与控制”- **
typePolicies:**允许对不同类型如何缓存进行细粒度控制,包括定义自定义键字段(如果id不是唯一标识符)以及字段级别的读/合并函数。 - 缓存清除/垃圾回收:
InMemoryCache会自动移除断开连接的对象。你可以配置限制或手动清除数据。 - **直接访问缓存:**使用
client.readQuery、client.writeQuery、client.readFragment、client.writeFragment和client.cache.modify进行命令式缓存操作(常用于 mutation 的update函数)。
掌握 Apollo Client 的缓存是构建高性能 GraphQL 应用的关键。有关高级配置,请参阅官方文档。