当前位置:首页 > 学习笔记 > 正文内容

GraphQL API 开发完全指南:从入门到实战

廖万里21小时前学习笔记2

GraphQL是一种用于API的查询语言,它提供了一种更高效、强大和灵活的替代REST的方案。与传统REST API不同,GraphQL允许客户端精确指定所需数据,避免了过度获取和多次请求的问题。本指南将带你从零开始掌握GraphQL的核心概念和实战开发技能。

一、核心概念

1.1 什么是GraphQL

GraphQL由Facebook于2012年开发,2015年开源。它是一种用于API的查询语言,也是一个满足数据查询的运行时。GraphQL不是数据库,而是一种API架构风格,可以与任何后端存储引擎配合使用。

GraphQL的核心设计理念是:客户端驱动。客户端决定需要什么数据,服务端只返回客户端请求的数据。这种模式解决了REST API中的几个经典问题:

  • 过度获取(Over-fetching):REST接口返回固定字段,客户端可能只需要部分数据
  • 获取不足(Under-fetching):一个页面需要调用多个REST接口获取不同数据
  • 接口版本管理困难:REST接口变更需要版本控制,维护成本高

1.2 核心组件

GraphQL的架构由以下几个核心组件构成:

Schema(模式):定义API的类型系统和数据结构,是客户端和服务端的契约。Schema使用SDL(Schema Definition Language)编写,清晰描述数据模型。

Query(查询):客户端用于读取数据的操作,类似于REST的GET请求。

Mutation(变更):客户端用于修改数据的操作,包括创建、更新、删除,类似于REST的POST/PUT/DELETE。

Subscription(订阅):支持实时数据推送,客户端可以订阅数据变更事件,通过WebSocket实现。

Resolver(解析器):服务端处理每个字段查询的函数,负责从数据源获取数据。

1.3 与REST的对比

特性RESTGraphQL
端点数量多个(/users, /posts等)单个(/graphql)
数据获取服务端决定返回内容客户端指定返回字段
请求次数可能需要多次请求一次请求获取所有数据
版本管理需要版本控制无需版本,持续演进
文档需要额外维护自动生成文档

二、核心内容

2.1 Schema定义

Schema是GraphQL的核心,定义了API的类型系统。以下是常用类型定义:

# 标量类型(Scalar Types)scalar DateTimescalar JSON# 枚举类型enum UserRole {  ADMIN  USER  GUEST}# 对象类型type User {  id: ID!  username: String!  email: String!  role: UserRole!  posts: [Post!]!  createdAt: DateTime!  profile: Profile}type Post {  id: ID!  title: String!  content: String!  author: User!  tags: [String!]!  views: Int!  published: Boolean!  createdAt: DateTime!  updatedAt: DateTime}type Profile {  avatar: String  bio: String  website: String}# 输入类型(用于Mutation参数)input CreateUserInput {  username: String!  email: String!  password: String!}input UpdatePostInput {  title: String  content: String  tags: [String!]  published: Boolean}# 查询根类型type Query {  user(id: ID!): User  users(limit: Int, offset: Int): [User!]!  post(id: ID!): Post  posts(authorId: ID, published: Boolean): [Post!]!  searchPosts(keyword: String!): [Post!]!}# 变更根类型type Mutation {  createUser(input: CreateUserInput!): User!  updateUser(id: ID!, input: UpdateUserInput!): User  deleteUser(id: ID!): Boolean!    createPost(input: CreatePostInput!): Post!  updatePost(id: ID!, input: UpdatePostInput!): Post  deletePost(id: ID!): Boolean!}# 订阅根类型type Subscription {  onPostCreated: Post!  onPostUpdated(postId: ID!): Post!  onUserCreated: User!}# Schema定义schema {  query: Query  mutation: Mutation  subscription: Subscription}

2.2 Resolver实现

Resolver是处理每个字段查询的函数。每个Resolver接收四个参数:

  • parent:父级Resolver返回的对象
  • args:客户端传入的参数
  • context:请求上下文(如认证信息、数据库连接)
  • info:查询的执行状态信息
const { db } = require("./database");const resolvers = {  Query: {    // 获取单个用户    async user(parent, { id }, context, info) {      const user = await db.users.findById(id);      if (!user) {        throw new Error("用户不存在");      }      return user;    },    // 分页获取用户列表    async users(parent, { limit = 10, offset = 0 }, context) {      const users = await db.users.findAll({        limit,        offset,        order: [["createdAt", "DESC"]]      });      return users;    },    // 获取文章    async post(parent, { id }, context) {      const post = await db.posts.findById(id);      if (!post) {        throw new Error("文章不存在");      }      return post;    },    // 搜索文章    async searchPosts(parent, { keyword }, context) {      const posts = await db.posts.findAll({        where: {          [Op.or]: [            { title: { [Op.like]: `%${keyword}%` } },            { content: { [Op.like]: `%${keyword}%` } }          ]        }      });      return posts;    }  },  Mutation: {    // 创建用户    async createUser(parent, { input }, context) {      // 参数验证      const existingUser = await db.users.findOne({        where: { email: input.email }      });      if (existingUser) {        throw new Error("邮箱已被注册");      }      // 密码加密      const hashedPassword = await bcrypt.hash(input.password, 10);      // 创建用户      const user = await db.users.create({        ...input,        password: hashedPassword,        role: "USER"      });      return user;    },    // 创建文章    async createPost(parent, { input }, context) {      // 认证检查      if (!context.user) {        throw new Error("请先登录");      }      const post = await db.posts.create({        ...input,        authorId: context.user.id,        published: false      });      // 发布订阅事件      context.pubsub.publish("POST_CREATED", { onPostCreated: post });      return post;    },    // 更新文章    async updatePost(parent, { id, input }, context) {      const post = await db.posts.findById(id);      if (!post) {        throw new Error("文章不存在");      }      // 权限检查      if (post.authorId !== context.user.id) {        throw new Error("无权修改此文章");      }      await post.update(input);      return post;    },    // 删除文章    async deletePost(parent, { id }, context) {      const post = await db.posts.findById(id);      if (!post) {        throw new Error("文章不存在");      }      if (post.authorId !== context.user.id) {        throw new Error("无权删除此文章");      }      await post.destroy();      return true;    }  },  // 类型Resolver(处理关联数据)  User: {    async posts(parent, args, context) {      // 延迟加载,只在请求时查询      const posts = await db.posts.findAll({        where: { authorId: parent.id }      });      return posts;    },    async profile(parent, args, context) {      return await db.profiles.findOne({        where: { userId: parent.id }      });    }  },  Post: {    async author(parent, args, context) {      return await db.users.findById(parent.authorId);    }  },  // 订阅Resolver  Subscription: {    onPostCreated: {      subscribe(parent, args, context) {        return context.pubsub.asyncIterator(["POST_CREATED"]);      }    }  }};module.exports = resolvers;

2.3 服务端搭建

使用Apollo Server快速搭建GraphQL服务:

const { ApolloServer } = require("apollo-server");const { makeExecutableSchema } = require("@graphql-tools/schema");const { PubSub } = require("graphql-subscriptions");const typeDefs = require("./schema");const resolvers = require("./resolvers");// 创建订阅发布器const pubsub = new PubSub();// 创建可执行Schemaconst schema = makeExecutableSchema({  typeDefs,  resolvers});// 创建Apollo Serverconst server = new ApolloServer({  schema,  context: async ({ req }) => {    // 从请求头获取Token    const token = req.headers.authorization || "";    // 验证用户身份    let user = null;    if (token) {      try {        const decoded = jwt.verify(token, process.env.JWT_SECRET);        user = await db.users.findById(decoded.userId);      } catch (err) {        console.error("Token验证失败:", err.message);      }    }    return {      user,      db,      pubsub    };  },  formatError: (error) => {    // 错误格式化    console.error(error);    return {      message: error.message,      code: error.extensions?.code || "INTERNAL_ERROR"    };  },  plugins: [    // 性能监控插件    {      requestDidStart() {        return {          didResolveOperation(requestContext) {            console.log(`查询: ${requestContext.request.operationName}`);          }        };      }    }  ]});// 启动服务器server.listen({ port: 4000 }).then(({ url }) => {  console.log(`GraphQL服务运行在 ${url}`);});

2.4 客户端查询

客户端使用GraphQL查询语言进行数据请求:

# 基础查询query GetUser {  user(id: "1") {    id    username    email    posts {      id      title    }  }}# 带参数的查询query GetPosts($limit: Int!, $offset: Int!) {  posts(limit: $limit, offset: $offset) {    id    title    content    author {      username    }    createdAt  }}# 使用片段复用查询结构fragment PostFields on Post {  id  title  content  views  published}query GetPostsWithFragment {  posts {    ...PostFields  }}# Mutation操作mutation CreatePost($input: CreatePostInput!) {  createPost(input: $input) {    id    title    content    published  }}# Subscription订阅subscription OnPostCreated {  onPostCreated {    id    title    author {      username    }  }}

2.5 Apollo Client集成

前端使用Apollo Client进行数据管理:

import { ApolloClient, InMemoryCache, gql, useQuery, useMutation } from "@apollo/client";// 创建客户端实例const client = new ApolloClient({  uri: "http://localhost:4000/graphql",  cache: new InMemoryCache(),  defaultOptions: {    watchQuery: {      fetchPolicy: "cache-and-network"    }  }});// React Hook使用示例const GET_USER = gql`  query GetUser($id: ID!) {    user(id: $id) {      id      username      email      posts {        id        title      }    }  }`;const CREATE_POST = gql`  mutation CreatePost($input: CreatePostInput!) {    createPost(input: $input) {      id      title      content    }  }`;// 查询组件function UserProfile({ userId }) {  const { loading, error, data } = useQuery(GET_USER, {    variables: { id: userId }  });  if (loading) return 

加载中...

; if (error) return

错误: {error.message}

; return (

{data.user.username}

邮箱: {data.user.email}

文章列表

{data.user.posts.map(post => (
{post.title}
))}
);}// Mutation组件function CreatePostForm() { const [createPost, { loading, error }] = useMutation(CREATE_POST, { onCompleted: (data) => { console.log("文章创建成功:", data.createPost); }, refetchQueries: ["GetPosts"] // 自动刷新相关查询 }); const handleSubmit = (e) => { e.preventDefault(); const formData = new FormData(e.target); createPost({ variables: { input: { title: formData.get("title"), content: formData.get("content") } } }); }; return (

看不清,换一张

◎欢迎参与讨论,请在这里发表您的看法和观点。