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 = `#graphql
        type 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: String
      description: 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: String
    description: String
}

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

返回数组

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

  • type def
    type Query {
        jobs: [Job!]
    }
    
    type Job {
        id: ID!
        title: String
        description: 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: Job
        jobs: [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: Job
  jobs: [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: Job
      jobs: [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: String
      company: 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 {
        id
        date
        title
        company {
          id
          name
        }
      }
    }
  `;
  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!): Job
      jobs: [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: String
      company: 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 {
            id
            date
            title
            company {
              id
              name
            }
          }
        }
      `;
      const { jobs } = await client.request(query);
      return jobs;
    }
    
    export async function getJob(id) {
      const query = gql`
        query ($id: ID!) {
          job(id: $id) {
            id
            date
            title
            description
            company {
              id
              name
            }
          }
        }
      `;
      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!): Job
      jobs: [Job]
      company(id: ID!): Company
    }
    
    type Company {
      id: ID!
      name: String!
      description: String
      jobs: [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: String
      company: 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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值