GraphQL 是由 Facebook 开发的一种 API 查询语言和运行时,它允许客户端精确地指定所需的数据,从而避免了 REST 中常见的过度获取和不足获取的问题。本章将带你走进 GraphQL 的世界,学习如何在 Node.js 中使用 Apollo Server 构建 GraphQL API。
GraphQL 是一种用于 API 的查询语言,它提供了一套完整且易于理解的描述,使得客户端能够准确地获得所需的数据,没有任何冗余。与传统的 REST API 相比,GraphQL 有以下几个显著特点:
/graphql)进行,而不是多个 URL。{
user(id: "1") {
name
email
posts {
title
comments {
content
}
}
}
}
让我们通过一个对比来理解 GraphQL 的优势:
| 特性 | REST | GraphQL |
|---|---|---|
| 端点数量 | 多个(每个资源一个) | 单个(/graphql) |
| 数据获取 | 服务器决定返回哪些字段 | 客户端指定需要的字段 |
| 版本控制 | 通常通过 URL 路径(/v1/users) | 通过 schema 演进,无需版本号 |
| 文档 | 需额外工具如 Swagger | 自文档化(内省系统) |
| 缓存 | 利用 HTTP 缓存机制 | 需要客户端实现(如 Apollo Client) |
GraphQL 的核心是定义 schema,它描述了 API 中可用的类型和操作(查询、变更、订阅)。类型系统包括标量类型(String、Int、Float、Boolean、ID)和对象类型。
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!
}
查询用于获取数据,类似于 REST 的 GET 请求。
query GetUsers {
users {
id
name
email
}
}
变更用于修改数据(创建、更新、删除),类似于 REST 的 POST、PUT、DELETE。
mutation CreateUser($name: String!, $email: String!) {
createUser(name: $name, email: $email) {
id
name
email
}
}
解析器是 GraphQL 服务器的实际实现,负责返回每个字段的数据。每个字段对应一个解析器函数。
Apollo Server 是最流行的 GraphQL 服务器实现,与 Express 等框架无缝集成。
创建一个新项目并安装必要的包:
npm init -y
npm install @apollo/server express graphql cors body-parser
下面是一个使用 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();
启动服务器后,打开浏览器访问 http://localhost:4000/graphql,你会看到 Apollo Sandbox 界面。在这里可以执行查询和变更。
示例查询:
{
users {
id
name
posts {
title
}
}
}
解析器函数接收四个参数:parent、args、context、info。
parent:父字段的解析结果,用于处理嵌套字段。args:客户端传递的参数。context:共享上下文,可用于认证、数据库连接等。info:查询的 AST 信息,高级用途。在上面的例子中,我们使用了 parent 来解析 User.posts 和 Post.author。
上下文是每个请求共享的对象,通常用于传递数据库连接或认证信息。可以在创建 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;
}
}
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' });
在实际项目中,解析器通常会调用数据库。例如使用 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 })
}
};
在前端,可以使用 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));