GraphQL 实战篇:Apollo Client 配置与缓存

GraphQL 实战篇:Apollo Client 配置与缓存

上一篇:GraphQL 入门篇:基础查询语法

依旧和上一篇的笔记一样,主实操,没啥过多的细节讲解,代码具体在:

https://github.com/GoldenaArcher/graphql-by-example

异常处理

官方文档参考: Error Handling

graphql 处理默认异常是没什么问题的,比如说 id 没有提供的情况:

但是,再找不到数据的情况下,graphql 就会返回 null 而非报错:

诚然,使用 !Company 去强制执行非空也是一种解决方案:

type Query {job(id: ID!): Jobjobs: [Job]company(id: ID!): Company!
}

但是,这也会同样触发其他的问题,比如说下面这个情况,当所找的公司不存在,又如果使用非空操作就会跑出内部异常错误,同时 job 的 query 也会终止:

尽管这种调用方式比较少见,但是也是具有特定的业务场景的,因此返回值并不推荐使用强制非空操作,而是建议使用 custom error

custom errors - server

这里只需要更新 resolvers 即可:

import { GraphQLError } from "graphql";
import { getJobs, getJob, getJobsByCompany } from "./db/jobs.js";
import { getCompany } from "./db/companies.js";export const resolvers = {Query: {// new code here, can extract into functionjob: async (_root, { id }) => {const job = await getJob(id);if (!job) {throw new GraphQLError("Job not found: " + id, {extensions: {code: "NOT_FOUND",},});}return job;},jobs: async () => getJobs(),// new code herecompany: async (_root, { id }) => {const company = await getCompany(id);if (!company) {throw new GraphQLError("Company not found: " + id, {extensions: {code: "NOT_FOUND",},});}return company;},},
};

GraphQLError 我这里用了 class-based 的实现:

class GraphQLNotFound extends GraphQLError {constructor(message) {super(message, {extensions: {code: "NOT_FOUND",// this will change the entire graphql status code, be sure to discuss how to approach it firsthttp: {status: 404,},},});}
}

用方法跑出对应的 GraphQLError 也可以

customer errors - client

服务端这里,主要还是在 fetch 的时候进行对应的操作,和正常的 rest 操作也没什么区别:

import { useParams } from "react-router";
import { useEffect, useState } from "react";
import { getCompany } from "../lib/graphql/queries";
import JobList from "../components/JobList";function CompanyPage() {const { companyId } = useParams();const [state, setState] = useState({company: null,loading: true,hasErrors: false,});useEffect(() => {(async () => {try {const company = await getCompany(companyId);setState({ company, loading: false, hasErrors: false });} catch (e) {setState({company: null,loading: false,hasErrors: true,});}})();}, [companyId]);const { company, loading, hasErrors } = state;if (loading) {return <div>Loading...</div>;}if (hasErrors) {return <div>Something went wrong...</div>;}return (<div><h1 className="title">{company.name}</h1><div className="box">{company.description}</div><h2 className="title is-5">Jobs At {company.name}</h2><JobList jobs={company.jobs} /></div>);
}export default CompanyPage;

效果如下:

mutation - add

开始 CUD 操作,这三类都可以被归类到 Mutation 中

mutation - add, server

company 来说,只有 title 和 description 可以被更新,不过因为目前没有做 auth 部分的实现,所以还是会写在 signature 里——但是没用上

  • schema
    type Mutation {createJob(title: String!, description: String, companyId: ID!): Job
    }
    
  • resolver
      Mutation: {createJob: (_root, { title, description }) => {const companyId = "FjcJCHJALA4i"; // TODO - change it laterreturn createJob({ companyId, title, description });},},
    

sandbox 中验证更新:

mutation - add, client

  • queries
    query 这里的代码和 server 中差不多,不过返回数据只显示 id 去重新拉数据
    CreateJobInput 的实现看下一个 section
    export async function createJob({ title, description }) {const mutation = gql`mutation CreateJob($input: CreateJobInput!) {createJob(input: $input) {id}}`;const { createJob: job } = await client.request(mutation, {input: {title,description,},});return job;
    }
    
  • react
    import { useState } from "react";
    import { useNavigate } from "react-router-dom";
    import { createJob } from "../lib/graphql/queries";function CreateJobPage() {const navigate = useNavigate();const [title, setTitle] = useState("");const [description, setDescription] = useState("");const handleSubmit = async (event) => {event.preventDefault();const job = await createJob({ title, description });console.log(job);navigate(`/jobs/${job.id}`);};// 省略 JSX
    }export default CreateJobPage;
    

效果如下:

Input Type

在 graphql 里,input 和 output 是两种不同的类型。之前定义的是 output,下面会重新定义 input:

  • schema
    type Mutation {createJob(input: CreateJobInput!): Job
    }input CreateJobInput {title: String!description: String
    }
    

mutation - delete

这里只更新 server 端:

  • schema
    type Mutation {deleteJob(id: ID!): Job
    }
    
  • resolvers
      Mutation: {deleteJob: (_root, { id }) => {deleteJob(id);},},
    

mutation - update

这里只更新 server 端:

  • schema
    type Mutation {updateJob(input: UpdateJobInput!): Job
    }input UpdateJobInput {id: ID!title: Stringdescription: String
    }
    
  • resolvers
      Mutation: {updateJob: async (_root, { input: { id, title, description } }) => {await updateJob({ id, title, description });},},
    

认证(Authentication)

具体的认证流程就不说了,这里依旧用的是基础的 jwt 进行认证

登录时会将 jwt 保存到 local storage 中,这里就简单讲一下,怎么在 graphql 中传递和处理 jwt

目前这段处理服务端代码

  • server
    const getContext = ({ req }) => {return {auth: req.auth,};
    };app.use("/graphql", apolloMiddleware(aplloServer, { context: getContext }));
    
  • resolvers
        updateJob: async (_root, { input: { id, title, description } }, {auth}) => {if (!auth) {throw new GraphQLUnauthorized("Unauthorized");}await updateJob({ id, title, description });
    

效果展示:

用户 ↔ 公司 关联

前面是写死了公司 id,现在会用更符合现实逻辑的方式去实现:

  • server
    const getContext = async ({ req }) => {if (!req.auth) {return {};}const user = await getUser(req.auth.sub);return {user,};
    };
    
    sub 可以理解成用户 id:Asserts the identity of the user, called subject in OpenID (sub).
  • resolver
        createJob: (_root, { input: { title, description } }, context) => {if (!context.user) {throw new GraphQLUnauthorized("Unauthorized");}const companyId = context.user.companyId; // TODO - change it laterreturn createJob({ companyId, title, description });},
    

效果如下:

认证(Authorization)- 客户端

目前后端加了验证,前端如果不传 auth token 的话就无法实现增加的功能:

客户端的实现更多的和用的包有关,这部分会提一下 graqhql-request 的实现,后面替换成 apollo 的 client 也会完成对应的更换:

const client = new GraphQLClient("http://localhost:9000/graphql", {headers: () => {const accessToken = getAccessToken();if (accessToken) {return {Authorization: "Bearer " + accessToken,};}return {};},
});

效果如下:

认证(Authorization)- 删除

删除功能除了认证用户之外,还需要确认当前用户是否可以删除对应的记录,比如说现在的实现,A 公司的用户可以删除 B 公司的 post,从而开启一场朴实无华的商业战:

因此,在做认证的时候就需要多加一步:

deleteJob: async (_root, { id }, { user }) => {if (!user) {throw new GraphQLUnauthorized("Unauthorized");}const job = await deleteJob(id, user.companyId);if (!job) {throw new GraphQLNotFound("Job not found: " + id);}return job;
};

这里的 deleteJob 会通过 idcompanyId 去查找并返回对应的记录

设置 apollo-client

apollo-client 是官方提供的 client 和 server 一样,优势在于提供更可靠的 cache 机制,替换方式相对而言也比较简单——尤其是 graphql-request 和 apollo-client 的冲突不算特别多的情况下,完整的实现如下:

import {ApolloClient,ApolloLink,concat,createHttpLink,gql,InMemoryCache,
} from "@apollo/client";
import { getAccessToken } from "../auth";const httpLink = createHttpLink({uri: "http://localhost:9000/graphql",
});const authLink = new ApolloLink((operation, forward) => {const accessToken = getAccessToken();if (accessToken) {operation.setContext({headers: {authorization: `Bearer ${accessToken}`,},});}return forward(operation);
});const apolloClient = new ApolloClient({link: concat(authLink, httpLink),cache: new InMemoryCache(),
});export async function createJob({ title, description }) {const mutation = gql`mutation CreateJob($input: CreateJobInput!) {createJob(input: $input) {id}}`;const { data } = await apolloClient.mutate({mutation,variables: {input: {title,description,},},});return data.createJob;
}export async function getJobs() {const query = gql`query {jobs {iddatetitlecompany {idname}}}`;const { data } = await apolloClient.query({ query });return data.jobs;
}export async function getJob(id) {const query = gql`query ($id: ID!) {job(id: $id) {iddatetitledescriptioncompany {idname}}}`;const { data } = await apolloClient.query({query,variables: { id },});return data.job;
}export async function getCompany(id) {const query = gql`query Jobs($id: ID!) {company(id: $id) {idnamedescriptionjobs {iddatetitle}}}`;const { data } = await apolloClient.query({ query, variables: { id } });return data.company;
}

这里主要做了几个部分:

  • link
    类比的话,就是 apollo 的 middleware,官方的图解如下:

    目前常见的 Link 有:
    Link 类型用途
    HttpLink把请求发到 GraphQL 后端(最常用)
    ErrorLink捕获 GraphQL 错误 / 网络错误,打印或重定向
    AuthLink / ApolloLink自定义 headers,例如添加 token
    RetryLink请求失败时自动 retry(可设置 retry 策略)
    BatchHttpLink把多个 query 合并成一个 HTTP 请求
    这里只会用到 HttpLinkApolloLink
    需要注意 chaining 的顺序,HttpLink 必须是第一个调用的,按顺序必须在 concat 的最后一个
    对于非 HttpLink 来说,需要注意 forward(operation) 进行下一步操作
  • cache
    这个下一个 section 会讲的更多一些,这里开启了 InMemoryCache
  • 验证
    就是 ApolloLink 中实现的内容
    这里主要实现的内容就是绑定了 Authorization header,实现方式和 graphql-request 略有不同,本质上是一样的
  • query/mutation 的转换
    graphql-request 只有 request,apollo-client 则是像 server 一样分离了 query 和 mutation
    然后就是返回结果不一样
    这两点需要注意一下

cache

缓存只会在 client 做,server 是没有缓存机制的

这也是为啥课程换用 apollo-client 了吧……

Caching in Apollo Client

前面开启了默认的 InMemoryCache ,实现之后,当遇到数据已经获取的情况下,就不会重新获取数据。如下面这张图:

从主页面到子页面还会进行 fetch——因为缺乏必要的 description,但是当从子页面返回主页面时,数据在已经存在的情况下,就不会自动更新了

他们这个机制还是有点复杂的,Apollo Client 会以对象结构保存请求字段(不是 query 名称,而是字段结构 + 参数值)对应的数据。每次请求时,它根据:

  1. 查询字段结构是否一致(即你请求了哪些字段);
  2. 传入的变量(如 $id)是否一致;
  3. 缓存中是否已有这些字段对应的数据(使用 normalized cache,如 Job:123);

如果三者都满足,就会直接从缓存中读取,避免触发网络请求。

cache 机制

目前官方的 client 提供的机制有下面几个:

具体说明如下:

NameDescriptionDescription
cache-firstApollo Client first executes the query against the cache. If all requested data is present in the cache, that data is returned. Otherwise, Apollo Client executes the query against your GraphQL server and returns that data after caching it.Prioritizes minimizing the number of network requests sent by your application.This is the default fetch policy.缓存优先策略:优先从缓存中读取数据,若缓存不完整则发起网络请求,并将结果缓存。默认策略,适合数据变化不频繁的场景。
cache-onlyApollo Client executes the query only against the cache. It never queries your server in this case.A cache-only query throws an error if the cache does not contain data for all requested fields.仅使用缓存:完全依赖本地缓存,不发起任何网络请求。若缓存中无数据或不完整会抛出错误。适用于确保缓存已存在的场景。
cache-and-networkApollo Client executes the full query against both the cache and your GraphQL server. The query automatically updates if the result of the server-side query modifies cached fields.Provides a fast response while also helping to keep cached data consistent with server data.缓存和网络并用:先返回缓存数据以快速响应,再请求服务端数据并更新缓存。兼顾速度与一致性。
network-onlyApollo Client executes the full query against your GraphQL server, without first checking the cache. The query’s result is stored in the cache.Prioritizes consistency with server data, but can’t provide a near-instantaneous response when cached data is available.仅走网络,带缓存写入:跳过缓存,始终走网络请求,但结果会写入缓存。适合需要最新数据的情况
no-cacheSimilar to network-only, except the query’s result is not stored in the cache.不使用缓存:请求数据但不写入缓存。适用于一次性查询或敏感数据展示等场景
standbyUses the same logic as cache-first, except this query does not automatically update when underlying field values change. You can still manually update this query with refetch and updateQueries.待机模式:和 cache-first 类似,但不会自动响应数据变更,适合后台组件或非活跃页面手动更新数据的场景

GraphQL client 端自带的 cache 机制……我觉得应该是可以满足大多数的业务需求了

customized query

下面这个实现,在 createJob 里面写满了对应的数据——也就是 jobByIdQuery 中有的数据,就可以在创建数据后,不获取数据库最新的数据,而直接渲染:

const jobByIdQuery = gql`query ($id: ID!) {job(id: $id) {iddatetitlecompany {idname}}}
`;export async function createJob({ title, description }) {const mutation = gql`mutation CreateJob($input: CreateJobInput!) {createJob(input: $input) {id}}`;const { data } = await apolloClient.mutate({mutation,variables: {input: {title,description,},},update: (cache, { data }) => {cache.writeQuery({query: jobByIdQuery,variables: { id: data.createJob.id },data,});},});return data.createJob;
}export async function getJob(id) {const query = jobByIdQuery;const { data } = await apolloClient.query({query,variables: { id },});return data.job;
}

注意, writeQuery 是在 cache 之后手动重写进 cache 的方法

Fragment

上面的案例中已经可以看到, job 的定义已经被复用了好几次。graphql 本身也提供了一个 Fragment 的对象,可以用来改善代码复用的问题:

const jobDetailFragment = gql`fragment JobDetail on Job {iddatetitledescriptioncompany {idname}}
`;const jobByIdQuery = gql`query ($id: ID!) {job(id: $id) {...JobDetail}}${jobDetailFragment}
`;export async function createJob({ title, description }) {const mutation = gql`mutation CreateJob($input: CreateJobInput!) {createJob(input: $input) {...JobDetail}}${jobDetailFragment}`;const { data } = await apolloClient.mutate({mutation,variables: {input: {title,description,},},update: (cache, { data }) => {cache.writeQuery({query: jobByIdQuery,variables: { id: data.createJob.id },data,});},});return data.createJob;
}export async function getJobs() {const query = gql`query {jobs {...JobDetail}}${jobDetailFragment}`;const { data } = await apolloClient.query({ query });return data.jobs;
}export async function getJob(id) {const query = jobByIdQuery;const { data } = await apolloClient.query({query,variables: { id },});return data.job;
}

可以看到,本来需要重复声明的 Job 定义,这里可以用 JobDetail 去获取

这里还是建议类似的 Fragment 保存最低所需的数据,否则就失去 graphql 可以动态获取对应数据的优势了

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.pswp.cn/web/83124.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

web3-基于贝尔曼福特算法(Bellman-Ford )与 SMT 的 Web3 DeFi 套利策略研究

web3-基于贝尔曼福特算法&#xff08;Bellman-Ford &#xff09;与 SMT 的 Web3 DeFi 套利策略研究 如何找到Defi中的交易机会 把defi看做是一个完全开放的金融产品图表&#xff0c;可以看到所有的一切东西&#xff1b;我们要沿着这些金融图表找到一些最优的路径&#xff0c;就…

SQL Server 触发器调用存储过程实现发送 HTTP 请求

文章目录 需求分析解决第 1 步:前置条件,启用 OLE 自动化方式 1:使用 SQL 实现启用 OLE 自动化方式 2:Sql Server 2005启动OLE自动化方式 3:Sql Server 2008启动OLE自动化第 2 步:创建存储过程第 3 步:创建触发器扩展 - 如何调试?第 1 步:登录 SQL Server 2008第 2 步…

Go 语言中的内置运算符

1. 算术运算符 注意&#xff1a; &#xff08;自增&#xff09;和--&#xff08;自减&#xff09;在 Go 语言中是单独的语句&#xff0c;并不是运算符。 package mainimport "fmt"func main() {fmt.Println("103", 103) // 13fmt.Println("10-3…

SQL注入篇-sqlmap的配置和使用

在之前的皮卡丘靶场第五期SQL注入的内容中我们谈到了sqlmap&#xff0c;但是由于很多朋友看不了解命令行格式&#xff0c;所以是纯手动获取数据库信息的 接下来我们就用sqlmap来进行皮卡丘靶场的sql注入学习&#xff0c;链接&#xff1a;https://wwhc.lanzoue.com/ifJY32ybh6vc…

发立得信息发布系统房屋信息版(php+mysql)V1.0版

# 发立得信息发布系统房屋信息版(phpmysql) 一个轻量级的房屋信息发布平台&#xff0c;基于PHP和MySQL开发&#xff0c;支持用户发布房屋出售/出租信息&#xff0c;以及后台管理功能。 轻量级适合网站开发PHP方向入门者学习&#xff0c;首发版本&#xff0c;未经实际业务流程检…

学习 React【Plan - June - Week 1】

一、使用 JSX 书写标签语言 JSX 是一种 JavaScript 的语法扩展&#xff0c;React 使用它来描述用户界面。 什么是 JSX&#xff1f; JSX 是 JavaScript 的一种语法扩展。看起来像 HTML&#xff0c;但它实际上是在 JavaScript 代码中写 XML/HTML。浏览器并不能直接运行 JSX&…

小智AI+MCP

什么是小智AI和MCP 如果还不清楚的先看往期文章 手搓小智AI聊天机器人 MCP 深度解析&#xff1a;AI 的USB接口 如何使用小智MCP 1.刷支持mcp的小智固件 2.下载官方MCP的示例代码 Github&#xff1a;https://github.com/78/mcp-calculator 安这个步骤执行 其中MCP_ENDPOI…

基于python大数据的口红商品分析与推荐系统

博主介绍&#xff1a;高级开发&#xff0c;从事互联网行业六年&#xff0c;熟悉各种主流语言&#xff0c;精通java、python、php、爬虫、web开发&#xff0c;已经做了多年的设计程序开发&#xff0c;开发过上千套设计程序&#xff0c;没有什么华丽的语言&#xff0c;只有实实在…

ArcPy扩展模块的使用(3)

管理工程项目 arcpy.mp模块允许用户管理布局、地图、报表、文件夹连接、视图等工程项目。例如&#xff0c;可以更新、修复或替换图层数据源&#xff0c;修改图层的符号系统&#xff0c;甚至自动在线执行共享要托管在组织中的工程项。 以下代码展示了如何更新图层的数据源&…

打开GitHub网站因为网络原因导致加载失败问题解决方案

Date: 2025.06.09 20:34:22 author: lijianzhan 在Windows系统中&#xff0c;打开GitHub网站因为网络原因导致加载失败问题解决方案 打开Windows系统下方搜索框&#xff0c;搜索Microsoft Store&#xff0c;并且双击打开 在应用里面搜索Watt Toolkit&#xff0c;并下载安装 …

AI代码助手需求说明书架构

AI代码助手需求说明书架构 #mermaid-svg-6dtAzH7HjD5rehlu {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-6dtAzH7HjD5rehlu .error-icon{fill:#552222;}#mermaid-svg-6dtAzH7HjD5rehlu .error-text{fill:#552222;s…

.NET开发主流框架全方位对比分析

文章目录 1. ASP.NET Core核心特性代码示例&#xff1a;基本控制器优势劣势 2. .NET MAUI核心特性代码示例&#xff1a;基本页面优势劣势 3. Blazor两种托管模型核心特性代码示例&#xff1a;计数器组件优势劣势 4. WPF (Windows Presentation Foundation)核心特性代码示例&…

【系统架构设计师-2025上半年真题】案例分析-参考答案及部分详解(回忆版)

更多内容请见: 备考系统架构设计师-专栏介绍和目录 文章目录 试题一(25分)【问题1】(12分)【问题2】(13分)试题二(25分)【问题1】(10分)【问题2】(6分)【问题3】(9分)试题三(25分)【问题1】(13分)【问题2】(8分)【问题3】(4分)试题四(25分)【问题1】(6分)【问题2】(12…

【中间件】Web服务、消息队列、缓存与微服务治理:Nginx、Kafka、Redis、Nacos 详解

Nginx 是什么&#xff1a;高性能的HTTP和反向代理Web服务器。怎么用&#xff1a;通过配置文件定义代理规则、负载均衡、静态资源服务等。为什么用&#xff1a;提升Web服务性能、高并发处理、负载均衡和反向代理。优缺点&#xff1a;轻量高效&#xff0c;但动态处理能力较弱&am…

运动控制--小车的启动和停止算法

一、现实问题 小车在启动时由于受到惯性&#xff0c;后轮和前轮速度不一致&#xff0c;会引起车身不稳。 如小车上面装的是水&#xff0c;会出现倾洒&#xff0c;体验差。 二、数学研究 启动时 停止时 急动度&#xff08;jerk) 三、BLDC控制与S型曲线的融合逻…

WebFuture:Ubuntu 系统上在线安装.NET Core 8 的步骤

方法一&#xff1a;使用官方二进制包安装 下载.NET Core 8 SDK 二进制包&#xff1a;访问 .NET Core 8 SDK 官方下载页面&#xff0c;根据你的系统架构选择对应的 Linux x64 版本等下载链接&#xff0c;将其下载到本地4. 创建安装目录&#xff1a;在终端中执行以下命令创建用于…

可视化预警系统:如何实现生产风险的实时监控?

在生产环境中&#xff0c;风险无处不在&#xff0c;而传统的监控方式往往只能事后补救&#xff0c;难以做到提前预警。但如今&#xff0c;可视化预警系统正在改变这一切&#xff01;它能够实时收集和分析生产数据&#xff0c;通过直观的图表和警报&#xff0c;让管理者第一时间…

深度解析 Linux 内核参数 net.ipv4.tcp_rmem:优化网络性能的关键

文章目录 引言一、认识 net.ipv4.tcp_rmem1. 最小值&#xff08;min&#xff09;2. 默认值&#xff08;default&#xff09;3. 最大值&#xff08;max&#xff09; 二、net.ipv4.tcp_rmem 的工作原理三、net.ipv4.tcp_rmem 的实际应用场景1. 高并发 Web 服务器2. 文件传输服务3…

Windmill:开源开发者基础设施的革命者

前言 在企业内部,开发者经常需要构建各种内部工具来支持业务运营、数据分析和系统管理。这些工具通常需要前端界面、后端逻辑和工作流编排,开发过程繁琐且耗时。今天要介绍的Windmill项目,正是为解决这一痛点而生,它让构建内部工具变得简单高效,堪称开发者的得力助手。 …

国产化Excel处理组件Spire.XLS教程:用 Java 获取所有 Excel 工作表名称(图文详解)

在 Excel 中&#xff0c;工作表名称通常能够反映其用途或所含内容&#xff0c;提取这些名称有助于理清整个工作簿的结构。对于新用户或协作者来说&#xff0c;仅凭这些名称就能快速掌握各表中的数据类型。本文将演示如何使用 Java 获取 Excel 文件中的所有工作表名称&#xff0…