当前位置: 首页 > news >正文

GraphQL 入门篇:基础查询语法

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

相关文章:

  • 麦克风和电脑内播放声音实时识别转文字软件FunASR整合包V5下载
  • 科技修真的解决方案
  • 网页前端开发(基础进阶2)
  • 基于 Flickr30k-Entities 数据集 的 Phrase Localization
  • 【中国企业数字化转型之路】企业的资源投入与数字化转型的产出效益平衡探索(上篇)
  • ps填充图层
  • linux驱动开发(1)-内核模块
  • 大语言模型的推理能力
  • unix/linux source 命令,其内部结构机制
  • 一键开关机电路分析
  • 从线性代数到线性回归——机器学习视角
  • Amazon Augmented AI:人类智慧与AI协作,破解机器学习审核难题
  • QT中子线程触发主线程弹窗并阻塞等待用户响应-传统信号槽实现
  • 18. Qt系统相关:多线程
  • HackMyVM-Teacher
  • java基础学习(二十)
  • 机器学习:逻辑回归与混淆矩阵
  • Java正则表达式完全指南
  • 深度学习入门Day1--Python基础
  • 【复杂网络分析】什么是modularity?
  • 德州哪里有做网站推广的/班级优化大师的功能有哪些
  • 临沂网站建设设计公司/seo中文
  • 潮州网站网站建设/百度竞价排名推广
  • 做视频网站的备案要求/关键词点击排名系统
  • 魏县企业做网站推广/百度热搜榜排名今日第一
  • 修改wordpress密码/庆云网站seo