GraphQL 入门篇:基础查询语法

article/2025/7/21 4:35:09

GraphQL 入门篇:基础查询语法

最近准备面试的东西,所以就开始查漏补缺,就发现缺的东西还是蛮多的吧……(挠头

果然还是得定时找找工作之类的,和市场上的行情校对一下,这样才能够知道最近市场上需求的人才/知识是什么。之前的话有点太沉溺于 React 的垂直发展,最近找纯前端不太顺利,全栈纵向发展又有点不太够……

能补一点是一点吧 😮‍💨

代码在这里:

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

用的就是课程名

初始项目

这个项目走一下最基础的 graphql 的实现和结构,顺便介绍一点 playground 之类的,给下半篇,也就是正式做 graphql 的项目热身了

服务端代码

  • package.json
    {"name": "graphql","version": "1.0.0","main": "index.js","type": "module","license": "MIT","dependencies": {"@apollo/server": "^4.12.1","graphql": "^16.11.0"}
    }
    
  • server
    import { ApolloServer } from "@apollo/server";
    import { startStandaloneServer } from "@apollo/server/standalone";
    import { log } from "console";const typeDefs = `#graphqltype Query {greeting: String}
    `;const resolvers = {Query: {greeting: () => "Hello world!",},
    };const server = new ApolloServer({ typeDefs, resolvers });
    const info = await startStandaloneServer(server, { listen: { port: 9000 } });console.log(`🚀  Server ready at: ${info.url}`);
    

实现效果如下:

server 里面没有使用其他的服务——如 express 或是内置的 http 开启服务器,而是直接使用了 ApolloServer —— Apollo 自带的服务器,因此会在 9000 这个端口开启一个 Apollo 的 sandbox

默认情况下,所有的 graphql 请求都是 POST 请求,返回类型是 JSON 格式

服务端

服务端也遵从极简模式,只要能够从 server 拉数据并成功渲染即可

这里选择的是原生 js+ fetch 进行调用,并且通过 DOM 操作渲染到 HTML 文档中

  • js
    async function fetchGreeting() {const res = await fetch("http://localhost:9000/", {method: "POST",headers: {"Content-Type": "application/json",},body: JSON.stringify({query: "query {greeting }",}),});const { data } = await res.json();return data.greeting;
    }fetchGreeting().then((greeting) => {document.getElementById("greeting").innerHTML = greeting;
    });
    
  • html
    <!DOCTYPE html>
    <html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><script src="app.js"></script><title>GraphQL Client</title></head><body><h1>GraphQL Client</h1><p>The server says:<strong id="greeting"> Loading... </strong></p></body>
    </html>
    

最后渲染结果:

Code-First vs Schema-First

之前看 swagger 的时候就碰到过这种问题……

这种问题本质上来说,没有哪一个比较好,只是根据业务场景/使用情况不同而裁决。比如说当前的 graphql 只在当前内部项目使用,没有暴露的需求,那么 code-first 会更有效率;与之相反的是如果当前的 graphql 一定会被暴露,并当成服务共享,那么先写 schema,生成对应的 contract,再根据具体的情况去完成迭代——deprecate 或者 expire,就是更可取的方式

graphql 默认是 schema-first 的实现,想要避免写 type,用 code-first 的实现方法,可以考虑使用以下几个 dependencies:

  • **TypeGraphQL -** 周下载量 20w+,看起来有在维护,但不是特别的 active,应该还算挺稳定的
    这个使用方法是最不一样的,是对 TypeScript 的支持,使用的是注解的方法
  • Nexus - 已经进入不算太 active 的维护状态,上次更新是两年前的事情,不过下载量还是比较大的(周下载 10w+),应该相对而言比较稳定
  • **Pothos GraphQL -** 下载量不算太大,7-8w 左右,相对比较稳定,还在 actively 更新

用 nexus 做下例子,code-first 的实现方法大体如下:

import { queryType, stringArg, makeSchema } from "nexus";
import { GraphQLServer } from "graphql-yoga";const Query = queryType({definition(t) {t.string("hello", {args: { name: stringArg() },resolve: (parent, { name }) => `Hello ${name || "World"}!`,});},
});const schema = makeSchema({types: [Query],outputs: {schema: __dirname + "/generated/schema.graphql",typegen: __dirname + "/generated/typings.ts",},
});const server = new GraphQLServer({schema,
});server.start(() => `Server is running on http://localhost:4000`);

可以看到,没有 schema 的强调,对于第三方——非开发团队来说,想要复用当前 graphql 是一个比较困难的事情

graphql & 框架

这部分的实现就会使用 express+middleware+官方的 apollo server 管理 graphql,前端则是使用 React+graphql-request——一个轻量的 graphql client 去进行实现

server 端实现

这里也先开始一个比较基础的案例,后面再一点点拓展

  • server

    import { ApolloServer } from "@apollo/server";
    import { expressMiddleware as apolloMiddleware } from "@apollo/server/express4";
    import cors from "cors";
    import express from "express";
    import { readFile } from "node:fs/promises";
    import { authMiddleware, handleLogin } from "./auth.js";
    import { resolvers } from "./resolvers.js";const PORT = 9000;const app = express();
    app.use(cors(), express.json(), authMiddleware);app.post("/login", handleLogin);const typeDefs = await readFile("./schema.graphql", "utf-8");const aplloServer = new ApolloServer({ typeDefs, resolvers });
    await aplloServer.start();
    app.use("/graphql", apolloMiddleware(aplloServer));app.listen({ port: PORT }, () => {console.log(`Server running on port ${PORT}`);console.log(`GraphQL endpoint: http://localhost:${PORT}/graphql`);
    });
    
  • type defs

    type Query {job: Job
    }type Job {title: Stringdescription: String
    }
    
  • resolvers

    export const resolvers = {Query: {job: () => {return {id: "test-id",title: "The Title",description: "The description",};},},
    };
    

这里每个部分都拆成了独立的文件,方便长期管理

client 端

就是 react 的项目,暂时没有放新的东西,等到后面真的牵扯到 UI 再写实际变动的部分再更新

简单配置完后,graphql 的 sandbox 会一样启动:

Scalar

就是 grapnql 的原始类型(primitive type),这个说法大概比较 fancy

默认情况下,graphql 支持下面 5 种格式:

  • Int
  • Float
  • String
  • Boolean
  • ID

graphql 也支持个性化实现不同的类型,不过这个实现需要保证可以序列化及反序列化……换句话说如果是 TS 的 class 实现相对而言会有些麻烦,plain object 会容易一些……

非空判断

graphql 默认情况下是支持空值的,如果想要执行非空查询,就需要在定义的时候添加 !,如:

type Query {job: Job
}type Job {id: ID!title: Stringdescription: String
}

这个时候,如果传来的值——🆔 出现 null 的情况,graphql 服务端就会跑出异常

返回数组

这是另一个比较常见的需求,这里修改如下:

  • type def
    type Query {jobs: [Job!]
    }type Job {id: ID!title: Stringdescription: String
    }
    
    注意这里用的是 [Job!] ,非空判断放在 Job
  • resolver
    export const resolvers = {Query: {jobs: () => {return [{id: "test-id",title: "The Title",description: "The description",},];},},
    };
    
    暂时最为 placeholder

最后返回结果如下:

Resolver Chain

在 graphql 里,每一个字段都有独自的 resolver,当查询数据时,graphql 会按照层级调用对应的 resolver,形成一个 resolver chain

这里修改代码如下:

  • typedef
    type Query {job: Jobjobs: [Job]
    }type Job {id: ID!date: String!title: String!description: String
    }
    
  • resolver
    import { getJobs } from "./db/jobs.js";export const resolvers = {Query: {job: () => {return {id: "test-id",title: "The Title",description: "The description",date: "2023-01-01",};},jobs: async () => getJobs(),},Job: {date: (parent) => {return toIsoDate(parent.createdAt);},},
    };function toIsoDate(value) {return new Date(value).toISOString().slice(0, 10);
    }
    
    这里的 date: () => {} 就是一个 resolver chain,其中 parent 对应的是传进来的值本身——对于 date 来说, parent 就是 job ,因此可以通过这个 parent 获取合适的数据进行返回
    这里暂时只会了解 parent 的用法,其他包括 args, context,用到再谈
    最终的实现效果如下:
    result:

这里补充一下 job 的数据库格式:

date 是不存在的,需要通过 createdAt 手动转换

文档注释

实现如下:

type Query {job: Jobjobs: [Job]
}type Job {id: ID!"""The __date__ when the job was published, in ISO-8601 format. e.g. `2022`12`31`"""date: String!title: String!description: String
}

就是提供了文档的方法,需要和普通的 # 注释分开,这个注释不会显示在文档里,只是给开发看的

关联对象

graphql 中实现关联对象相对而言比较简单,不过同样需要在 resolver 中查找关联对象,实现如下:

  • update
    type Query {job: Jobjobs: [Job]
    }type Company {id: ID!name: String!description: String
    }"""
    Represents a job ad posted to the board.
    """
    type Job {id: ID!"""The __date__ when the job was published, in ISO-8601 format. e.g. `2022`12`31`"""date: String!title: String!description: Stringcompany: Company!
    }
    
  • resolver
    import { getJobs } from "./db/jobs.js";
    import { getCompany } from "./db/companies.js";export const resolvers = {Query: {job: () => {return {id: "test-id",title: "The Title",description: "The description",date: "2023-01-01",};},jobs: async () => getJobs(),},Job: {date: (parent) => {return toIsoDate(parent.createdAt);},company: (job) => {return getCompany(job.companyId);},},
    };function toIsoDate(value) {return new Date(value).toISOString().slice(0, 10);
    }
    

最后实现效果如下:

React 中获取 graphql 数据

前面提到了,会用到 graphql-request 这个 package,实现的方式为:

import { GraphQLClient, gql } from "graphql-request";const client = new GraphQLClient("http://localhost:9000/graphql");export async function getJobs() {const query = gql`query {jobs {iddatetitlecompany {idname}}}`;const { jobs } = await client.request(query);return jobs;
}

这种调用的方式类似于使用 axios 进行一个 fetch,在 component 中需要调用这个方法,获取对应的数据,如:

import { useEffect, useState } from "react";
import JobList from "../components/JobList";
import { getJobs } from "../lib/graphql/queries";function HomePage() {const [jobs, setJobs] = useState([]);useEffect(() => {getJobs().then((data) => {setJobs(data);});}, []);return (<div><h1 className="title">Job Board</h1><JobList jobs={jobs} /></div>);
}export default HomePage;

效果如下:

通过 id 获取数据

这个部分主要牵扯到通过 graphql 传值,也是一个新的知识点

  • TypeDef
    type Query {job(id: ID!): Jobjobs: [Job]
    }type Company {id: ID!name: String!description: String
    }"""
    Represents a job ad posted to the board.
    """
    type Job {id: ID!"""The __date__ when the job was published, in ISO-8601 format. e.g. `2022`12`31`"""date: String!title: String!description: Stringcompany: Company!
    }
    
  • resolvers
    上文提到过,第二个参数为 args,可以通过这个参数获取 argument:
    import { getJobs, getJob } from "./db/jobs.js";
    import { getCompany } from "./db/companies.js";export const resolvers = {Query: {job: (_root, { id }) => {return getJob(id);},jobs: async () => getJobs(id),},Job: {date: (parent) => {return toIsoDate(parent.createdAt);},company: (job) => {return getCompany(job.companyId);},},
    };function toIsoDate(value) {return new Date(value).toISOString().slice(0, 10);
    }
    

最终实现效果如下:

sandbox 中的调用方法和前端基本上是一样的——具体调用的方法还是需要参考一下 client 端是怎么包装的,当前的使用场景如下:

  • query 部分更新
    import { GraphQLClient, gql } from "graphql-request";const client = new GraphQLClient("http://localhost:9000/graphql");export async function getJobs() {const query = gql`query {jobs {iddatetitlecompany {idname}}}`;const { jobs } = await client.request(query);return jobs;
    }export async function getJob(id) {const query = gql`query ($id: ID!) {job(id: $id) {iddatetitledescriptioncompany {idname}}}`;const { job } = await client.request(query, { id });return job;
    }
    
  • component 部分更新
    import { useParams } from "react-router";
    import { Link } from "react-router-dom";
    import { formatDate } from "../lib/formatters";
    import { useEffect, useState } from "react";
    import { getJob } from "../lib/graphql/queries";function JobPage() {const { jobId } = useParams();const [job, setJob] = useState(null);useEffect(() => {getJob(jobId).then((job) => {setJob(job);});}, [jobId]);if (!job) {return <div>Loading...</div>;}return (<div><h1 className="title is-2">{job.title}</h1><h2 className="subtitle is-4"><Link to={`/companies/${job.company.id}`}>{job.company.name}</Link></h2><div className="box"><div className="block has-text-grey">Posted: {formatDate(job.date, "long")}</div><p className="block">{job.description}</p></div></div>);
    }export default JobPage;
    

最终实现效果如下:

Bidirectional Associations

双向关联

也就是 A ↔ B,在 graqhql 里面的实现就非常简单了

  • type def 更新
    type Query {job(id: ID!): Jobjobs: [Job]company(id: ID!): Company
    }type Company {id: ID!name: String!description: Stringjobs: [Job!]
    }"""
    Represents a job ad posted to the board.
    """
    type Job {id: ID!"""The __date__ when the job was published, in ISO-8601 format. e.g. `2022`12`31`"""date: String!title: String!description: Stringcompany: Company!
    }
    
  • resolvers 更新
    import { getJobs, getJob, getJobsByCompany } from "./db/jobs.js";
    import { getCompany } from "./db/companies.js";export const resolvers = {Query: {job: (_root, { id }) => {return getJob(id);},jobs: async () => getJobs(),company: (_root, { id }) => {return getCompany(id);},},Job: {date: (parent) => {return toIsoDate(parent.createdAt);},company: (job) => {return getCompany(job.companyId);},},Company: {jobs: (parent) => {return getJobsByCompany(parent.id);},},
    };function toIsoDate(value) {return new Date(value).toISOString().slice(0, 10);
    }
    

其实大部分的实现还是依赖于 resolver 部分的实现,react 代码没啥好更新的——毕竟这是 graphql 的课,实现效果是这样的:

递归调用

这是一个非常有趣的情况,使用如下:

这种业务场景其实比较适合流媒/社媒的场景——考虑到 graphql 是 meta 开源的,自然也能理解这样业务场景:

User A
├── Friend B
│   ├── Friend D
│   └── Friend E
└── Friend C└── Friend F

http://www.hkcw.cn/article/WvNqBgHCLJ.shtml

相关文章

尊界S800靠什么卖到了一百万 豪华设计与技术突破

华为与江淮合作打造的尊界首款车型S800正式上市,共分4个版本,售价区间为70.8-101.8万元,选装满配后最高可达112.8万元。余承东在直播中透露,目前尊界S800的大定订单已突破1000台,其中70%为超百万元的顶配版本。作为华为和江淮首次涉足百万豪车领域的产品,尊界S800的设计和…

操作系统学习(九)——存储系统

一、存储系统 在操作系统中&#xff0c;存储系统&#xff08;Storage System&#xff09; 是计算机系统的核心组成部分之一&#xff0c;它负责数据的存储、组织、管理和访问。 它不仅包括物理设备&#xff08;如内存、硬盘&#xff09;&#xff0c;还包括操作系统提供的逻辑抽…

山西临汾一男子脖子被扎多根烧烤签 伤者暂已脱险

6月2日凌晨2时许,有网友发帖称山西临汾一名小伙脖子上被扎了多根烧烤签。据网友发布的视频显示,小伙脖子上扎了四根金属签子,签上还有烧烤肉串,急救人员小心翼翼将其带至病床。联系到发帖网友后得知,他是一名参与此次急救的急救人员。事发于2日凌晨零时前后,一家烧烤店内…

“香会”现场美国亚太盟友难掩焦虑 对美政策感失望

第22届香格里拉对话会于5月30日至6月1日在新加坡举行,中国人民解放军国防大学代表团应邀出席。与以往连续多届由国防部长率团参会的形式不同,此次由中国国防大学代表团出席。5月31日,美国防长赫格塞思在大会演讲中渲染“中国威胁”,就涉台、南海等问题发表消极言论。当天傍…

雷阵雨+阵风!温馨提示:假期余额不足!北京节后气温直冲34℃

今天是端午假期的最后一天,市气象台在早上6时发布天气预报。早晨有轻雾,白天晴转多云,西部北部可能出现分散性阵雨或雷阵雨。北风从一级逐渐增强到三四级,阵风可达六七级,最高气温预计为31℃。夜间天气将由多云转晴,北风减弱至三级左右再降至一级,最低气温为15℃。白天风…

郑钦文说会拼搏到最后一刻 法网八强再战萨巴伦卡

北京时间6月2日凌晨,法网女单第四轮上半区四场比赛结束后,八强赛的部分对阵揭晓。中国选手郑钦文将与世界排名第一的萨巴伦卡交手,这是两人时隔半月后的再次碰面,比赛结果备受关注。郑钦文职业生涯首次打进法网女单八强。算上澳网、迈阿密站、马德里站和罗马站,本届法网是…

大连动物园1.5万游客赏萌兽 端午粽香共度佳节

端午节当天,大连森林动物园迎来了1.5万名游客,他们与园内的动物们共度传统佳节。饲养员们化身“创意大厨”,根据不同动物的食性,精心定制了专属“粽子盛宴”。憨态可掬的大熊猫和软萌的小熊猫抱着鲜笋粽子吃得津津有味;金丝猴用灵巧的双手剥开水果粽子大快朵颐;河马姐弟张…

独立候选人纳夫罗茨基赢得波兰总统选举

△纳夫罗茨基(资料图)根据波兰国家选举委员会网站2日公布的统计结果,独立候选人卡罗尔纳夫罗茨基赢得波兰总统选举。波兰国家选举委员会网站数据显示,纳夫罗茨基得票率为50.89%,公民联盟候选人、华沙市长拉法乌特扎斯科夫斯基得票率为49.11%。纳夫罗茨基现年42岁,现任波兰…

全国多地密集上调最低工资 覆盖数千万劳动者

2025年上半年,全国多地密集上调最低工资标准,成为民生领域的一大亮点。截至5月29日,已有重庆、四川、山西、广东等8个省份正式实施新标准,覆盖数千万劳动者。最低工资标准通常分为月最低工资标准和小时最低工资标准两种形式。前者适用于全日制就业劳动者,后者适用于非全日…

俄媒称乌克兰仅可能摧毁3架飞机 乌方说法被指谎言

俄罗斯“与假新闻作战”网站发布文章称,通过分析乌克兰方面发布的视频可以确认,乌总统泽连斯基所谓“已摧毁34%俄罗斯远程机队”的说法并不属实。俄方认为,乌克兰实际上可能仅摧毁了两架图-95战略轰炸机及一架安-12运输机,其余受损飞机在维修后均可恢复作战能力。乌克兰国家…

在EA工具中绘制活动图的控制流箭头线的“水平或垂直”弯曲效果

1 问题描述 之前用Visio绘制模型图时&#xff0c;直接用鼠标拖动&#xff0c;即可将箭头线转换成“水平或垂直”弯曲状。今天用EA绘制活动图时&#xff0c;经过6分钟左右的探索&#xff0c;才知如何在EA工具中将直的控制流线转换成“水平或垂直”弯曲状。写下以做经验分享。 …

云南多路段受到强降雨影响 多地交通中断抢险中

受持续强降雨影响,5月31日至6月1日,云南省迪庆藏族自治州境内多条国省干线公路遭遇塌方、泥石流、山洪等自然灾害,导致交通中断。迪庆公路局迅速启动应急预案,投入力量昼夜抢险。香格里拉方向,5月31日0时,G215K2974+300(争归村)边坡坍塌致全幅阻断,经连夜抢修于1时20分…

大熊猫妹珠过端午仪式感满满 享用特制大餐

今年端午假期又恰逢儿童节。在广州,作为全球唯一存活的大熊猫三胞胎中的大姐姐“萌萌”的首个幼崽——雌性大熊猫“妹珠”,享用过节大餐。记者:黄国保新华社音视频部制作责任编辑:zhangxiaohua

车圈大佬为何掐架 口水战再升级

今年六一儿童节,雷军与余承东在舆论场上再次展开了一场“隔空对话”。6月1日,雷军在社交媒体上宣布小米YU7将于7月量产,并引用了一句“诋毁,本身就是一种仰望”,疑似回应余承东前一天的言论。余承东在粤港澳大湾区车展论坛上提到“其他行业公司只做一款车就卖爆”,并直言…

北京今明两天晴热持续,北风明显,注意防风防晒 户外活动需谨慎

北京市气象台29日6时发布天气预报,今天早晨有轻雾,白天晴间多云。北风一级转南风三级,阵风可达五六级,最高气温33℃。夜间晴间多云,南转北风一二级,最低气温21℃。白天晴热持续,户外活动需注意遮阳防晒并勤补水。午后偏南阵风较大,请注意防风。责任编辑:zx0001

利用栈实现逆波兰表达式

题目链接&#xff1a; https://leetcode.cn/problems/evaluate-reverse-polish-notation/description/ 我们用栈来实现&#xff0c;遇到数字入栈&#xff0c;遇到运算符出栈&#xff0c;用stoi实现字符转整型。 int evalRPN(vector<string>& tokens) {stack<int…

3C All-in-One Toolbox:安卓手机的全能维护专家

在智能手机日益普及的今天&#xff0c;手机的日常维护和性能优化成为了许多用户关注的焦点。无论是清理内存、加速运行速度&#xff0c;还是管理应用程序和监控性能&#xff0c;一个高效、可靠的手机维护工具都能显著提升用户的使用体验。3C All-in-One Toolbox正是这样一款功能…

解决 Win11 睡眠后黑屏无法唤醒的问题

目录 一、问题描述二、解决方法1. 禁用快速启动2. 设置 Management Engine Interface3. 允许混合睡眠其他命令 4. 修复系统文件5. 更新 Windows 或驱动程序6. 其他1&#xff09;更改电源选项2&#xff09;刷新 Hiberfil.sys 文件3&#xff09;重置电源计划4&#xff09;运行系统…

mcp-go v0.31.0 发布!全新功能与关键修复,引领高效开发新时代!

随着云计算和微服务架构的不断普及&#xff0c;开发者对底层通信与服务调用工具的要求日益提升。作为现代服务治理的利器&#xff0c;mcp-go凭借其高性能、易用性和高度扩展性&#xff0c;深受开发者社区的喜爱。2025年5月30日&#xff0c;mcp-go迎来了v0.31.0版本的重磅更新。…

张家界一溶洞垃圾堆至7层楼高 溶洞污染引关注

近日,有网友反映张家界市慈利县一处天然溶洞被人为排污,导致溶洞受到严重污染。相关话题迅速引起公众关注。据慈利县融媒体中心发布的最新视频显示,在七天内,工作人员清理并打捞了杨家坡溶洞内的2.7吨垃圾。视频中可以看到,溶洞内的垃圾被装袋后使用吊机吊出,旁边已经堆放…