Node.js GraphQL 入门

GraphQL 是由 Facebook 开发的一种 API 查询语言和运行时,它允许客户端精确地指定所需的数据,从而避免了 REST 中常见的过度获取和不足获取的问题。本章将带你走进 GraphQL 的世界,学习如何在 Node.js 中使用 Apollo Server 构建 GraphQL API。

1. 什么是 GraphQL?

GraphQL 是一种用于 API 的查询语言,它提供了一套完整且易于理解的描述,使得客户端能够准确地获得所需的数据,没有任何冗余。与传统的 REST API 相比,GraphQL 有以下几个显著特点:

  • 精确获取:客户端可以指定需要的字段,服务器返回精确的字段,避免 over-fetching 和 under-fetching。
  • 单一端点:所有操作都通过一个端点(通常是 /graphql)进行,而不是多个 URL。
  • 强类型:GraphQL API 基于类型系统,所有的类型和字段都在 schema 中定义,具有自文档化的优势。
  • 强大的开发者工具:如 GraphiQL 和 GraphQL Playground,提供交互式查询和自动补全。
GraphQL 查询示例:
{
  user(id: "1") {
    name
    email
    posts {
      title
      comments {
        content
      }
    }
  }
}

2. GraphQL vs REST

让我们通过一个对比来理解 GraphQL 的优势:

特性 REST GraphQL
端点数量 多个(每个资源一个) 单个(/graphql)
数据获取 服务器决定返回哪些字段 客户端指定需要的字段
版本控制 通常通过 URL 路径(/v1/users) 通过 schema 演进,无需版本号
文档 需额外工具如 Swagger 自文档化(内省系统)
缓存 利用 HTTP 缓存机制 需要客户端实现(如 Apollo Client)

3. 核心概念

3.1 Schema 和类型

GraphQL 的核心是定义 schema,它描述了 API 中可用的类型和操作(查询、变更、订阅)。类型系统包括标量类型(StringIntFloatBooleanID)和对象类型。

type User {
  id: ID!
  name: String!
  email: String!
  posts: [Post!]!
}

type Post {
  id: ID!
  title: String!
  content: String!
  author: User!
}

type Query {
  users: [User!]!
  user(id: ID!): User
  posts: [Post!]!
}

type Mutation {
  createUser(name: String!, email: String!): User!
}

3.2 查询(Query)

查询用于获取数据,类似于 REST 的 GET 请求。

query GetUsers {
  users {
    id
    name
    email
  }
}

3.3 变更(Mutation)

变更用于修改数据(创建、更新、删除),类似于 REST 的 POST、PUT、DELETE。

mutation CreateUser($name: String!, $email: String!) {
  createUser(name: $name, email: $email) {
    id
    name
    email
  }
}

3.4 解析器(Resolver)

解析器是 GraphQL 服务器的实际实现,负责返回每个字段的数据。每个字段对应一个解析器函数。

4. 在 Node.js 中使用 Apollo Server

Apollo Server 是最流行的 GraphQL 服务器实现,与 Express 等框架无缝集成。

4.1 安装依赖

创建一个新项目并安装必要的包:

npm init -y
npm install @apollo/server express graphql cors body-parser

4.2 创建基础服务器

下面是一个使用 Apollo Server 和 Express 的完整示例:

const express = require('express');
const { ApolloServer } = require('@apollo/server');
const { expressMiddleware } = require('@apollo/server/express4');
const bodyParser = require('body-parser');
const cors = require('cors');

// 模拟数据
const users = [
  { id: '1', name: 'Alice', email: 'alice@example.com' },
  { id: '2', name: 'Bob', email: 'bob@example.com' }
];

const posts = [
  { id: '1', title: 'Post 1', content: 'Content 1', authorId: '1' },
  { id: '2', title: 'Post 2', content: 'Content 2', authorId: '1' },
  { id: '3', title: 'Post 3', content: 'Content 3', authorId: '2' }
];

// 定义 GraphQL schema(类型定义)
const typeDefs = `#graphql
  type User {
    id: ID!
    name: String!
    email: String!
    posts: [Post!]!
  }

  type Post {
    id: ID!
    title: String!
    content: String!
    author: User!
  }

  type Query {
    users: [User!]!
    user(id: ID!): User
    posts: [Post!]!
    post(id: ID!): Post
  }

  type Mutation {
    createUser(name: String!, email: String!): User!
  }
`;

// 解析器
const resolvers = {
  Query: {
    users: () => users,
    user: (_, { id }) => users.find(user => user.id === id),
    posts: () => posts,
    post: (_, { id }) => posts.find(post => post.id === id)
  },
  Mutation: {
    createUser: (_, { name, email }) => {
      const newUser = { id: String(users.length + 1), name, email };
      users.push(newUser);
      return newUser;
    }
  },
  User: {
    posts: (user) => posts.filter(post => post.authorId === user.id)
  },
  Post: {
    author: (post) => users.find(user => user.id === post.authorId)
  }
};

const app = express();
app.use(cors());
app.use(bodyParser.json());

const server = new ApolloServer({ typeDefs, resolvers });

async function startServer() {
  await server.start();
  app.use('/graphql', expressMiddleware(server));

  app.listen(4000, () => {
    console.log('GraphQL server running at http://localhost:4000/graphql');
  });
}

startServer();

4.3 测试 API

启动服务器后,打开浏览器访问 http://localhost:4000/graphql,你会看到 Apollo Sandbox 界面。在这里可以执行查询和变更。

示例查询:

{
  users {
    id
    name
    posts {
      title
    }
  }
}

5. 解析器详解

解析器函数接收四个参数:parentargscontextinfo

  • parent:父字段的解析结果,用于处理嵌套字段。
  • args:客户端传递的参数。
  • context:共享上下文,可用于认证、数据库连接等。
  • info:查询的 AST 信息,高级用途。

在上面的例子中,我们使用了 parent 来解析 User.postsPost.author

6. 使用上下文(Context)

上下文是每个请求共享的对象,通常用于传递数据库连接或认证信息。可以在创建 Apollo Server 时指定:

const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: ({ req }) => {
    // 从请求头获取 token
    const token = req.headers.authorization || '';
    // 验证 token,获取用户信息
    const user = getUserFromToken(token);
    return { user };
  }
});

然后在解析器中通过第三个参数访问:

Query: {
  me: (_, __, { user }) => {
    if (!user) throw new Error('Not authenticated');
    return user;
  }
}

7. 错误处理

GraphQL 支持标准错误处理。你可以在解析器中抛出错误,Apollo Server 会自动捕获并返回格式化的错误信息。

Mutation: {
  createUser: async (_, { name, email }, { db }) => {
    const existing = await db.users.findOne({ email });
    if (existing) {
      throw new Error('User already exists');
    }
    // ... 创建用户
  }
}

也可以使用自定义错误扩展状态码:

throw new ApolloError('User not found', 'USER_NOT_FOUND', { field: 'id' });

8. 与数据库集成

在实际项目中,解析器通常会调用数据库。例如使用 Mongoose:

const User = require('./models/User');
const Post = require('./models/Post');

const resolvers = {
  Query: {
    users: async () => await User.find(),
    user: async (_, { id }) => await User.findById(id)
  },
  User: {
    posts: async (user) => await Post.find({ authorId: user.id })
  }
};

9. 使用 GraphQL 客户端

在前端,可以使用 Apollo Client 或 Relay 消费 GraphQL API。下面是一个简单的浏览器 fetch 示例:

const query = `
  query {
    users {
      id
      name
    }
  }
`;

fetch('http://localhost:4000/graphql', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ query })
})
  .then(res => res.json())
  .then(data => console.log(data));

10. 最佳实践

  • 设计 schema 以业务为中心:而非数据库结构。
  • 使用分页:对于列表字段,使用连接(Connection)模式实现分页。
  • 限制查询深度:防止恶意嵌套查询导致服务器过载。
  • 数据加载器(DataLoader):解决 N+1 查询问题,批量加载关联数据。
  • 使用 GraphQL 接口和联合类型:处理多态关系。
总结: GraphQL 为 API 设计带来了革命性的变化,它赋予了客户端强大的灵活性。通过 Apollo Server,我们可以轻松地在 Node.js 中搭建 GraphQL API。本章介绍了核心概念、基本用法和一些进阶技巧,为你后续深入学习 GraphQL 打下了坚实基础。