Fastify GQL 中文教程
1. 核心概念
Fastify GQL 是一个专为 Fastify 框架设计的 GraphQL 插件,由 Mercurius 团队开发和维护。它提供了一个高性能、易于使用的 GraphQL 服务器实现,与 Fastify 框架无缝集成。
1.1 什么是 Fastify GQL?
Fastify GQL(现在称为 Mercurius)是一个 Fastify 插件,提供了:
- 高性能的 GraphQL 服务器实现
- 与 Fastify 框架的无缝集成
- 支持订阅(Subscriptions)
- 内置的 GraphQL Playground
- 数据加载器(DataLoader)支持
- 错误处理和日志记录
1.2 为什么选择 Fastify GQL?
- 高性能:基于 Fastify 框架,提供了优异的性能
- 易于集成:作为 Fastify 插件,集成非常简单
- 功能完整:支持 GraphQL 的所有核心特性
- 可扩展:通过插件系统支持各种扩展
- 生态系统:与 Fastify 生态系统无缝集成
2. 安装与配置
2.1 基本安装
在 Fastify 项目中安装 Fastify GQL(Mercurius):
npm install mercurius2.2 基本配置
创建一个简单的 Fastify GQL 服务器:
import Fastify from 'fastify';
import mercurius from 'mercurius';
const app = Fastify();
// 定义 schema
const schema = `
type Query {
hello: String
user(id: ID!): User
}
type User {
id: ID!
name: String!
email: String!
}
`;
// 定义解析器
const resolvers = {
Query: {
hello: () => 'Hello world!',
user: (_, { id }) => {
// 模拟从数据库获取用户
return {
id,
name: 'John Doe',
email: 'john@example.com'
};
}
}
};
// 注册 mercurius 插件
app.register(mercurius, {
schema,
resolvers,
graphiql: true // 启用 GraphQL Playground
});
// 启动服务器
app.listen({ port: 3000 }, (err, address) => {
if (err) {
console.error(err);
process.exit(1);
}
console.log(`Server listening at ${address}`);
});2.3 高级配置
Fastify GQL 支持多种高级配置选项:
app.register(mercurius, {
schema,
resolvers,
graphiql: true,
// 启用订阅
subscription: true,
// 上下文创建
context: (request, reply) => {
return {
// 从请求中获取用户信息
user: request.user,
// 数据库连接
db: request.db
};
},
// 错误处理
errorHandler: (error, request, reply) => {
console.error('GraphQL Error:', error);
reply.code(400).send({ error: error.message });
},
// 日志记录
logging: true
});3. 基本使用
3.1 定义 Schema
使用 SDL(Schema Definition Language)定义 schema:
const schema = `
# 定义查询类型
type Query {
# 获取所有用户
users: [User!]!
# 根据 ID 获取用户
user(id: ID!): User
# 获取所有帖子
posts: [Post!]!
# 根据 ID 获取帖子
post(id: ID!): Post
}
# 定义变更类型
type Mutation {
# 创建用户
createUser(name: String!, email: String!): User!
# 更新用户
updateUser(id: ID!, name: String, email: String): User!
# 删除用户
deleteUser(id: ID!): Boolean!
# 创建帖子
createPost(title: String!, content: String!, authorId: ID!): Post!
}
# 定义用户类型
type User {
id: ID!
name: String!
email: String!
# 用户的帖子
posts: [Post!]!
}
# 定义帖子类型
type Post {
id: ID!
title: String!
content: String!
authorId: ID!
# 帖子的作者
author: User!
}
`;3.2 实现解析器
解析器负责处理 GraphQL 查询,返回相应的数据:
// 模拟数据库
const users = [
{ id: '1', name: 'John Doe', email: 'john@example.com' },
{ id: '2', name: 'Jane Smith', email: 'jane@example.com' }
];
const posts = [
{ id: '1', title: 'First Post', content: 'Hello world!', authorId: '1' },
{ id: '2', title: 'Second Post', content: 'GraphQL is awesome!', authorId: '2' }
];
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;
},
updateUser: (_, { id, name, email }) => {
const user = users.find(user => user.id === id);
if (!user) throw new Error('User not found');
if (name) user.name = name;
if (email) user.email = email;
return user;
},
deleteUser: (_, { id }) => {
const index = users.findIndex(user => user.id === id);
if (index === -1) throw new Error('User not found');
users.splice(index, 1);
return true;
},
createPost: (_, { title, content, authorId }) => {
const newPost = {
id: String(posts.length + 1),
title,
content,
authorId
};
posts.push(newPost);
return newPost;
}
},
User: {
posts: (parent) => posts.filter(post => post.authorId === parent.id)
},
Post: {
author: (parent) => users.find(user => user.id === parent.authorId)
}
};3.3 执行查询
启动服务器后,可以通过 GraphQL Playground 或其他客户端工具执行查询:
3.3.1 查询所有用户
query GetAllUsers {
users {
id
name
email
posts {
id
title
}
}
}3.3.2 创建用户
mutation CreateUser {
createUser(name: "Bob Brown", email: "bob@example.com") {
id
name
email
}
}4. 高级特性
4.1 订阅(Subscriptions)
Fastify GQL 支持 GraphQL 订阅,实现实时数据更新:
import Fastify from 'fastify';
import mercurius from 'mercurius';
const app = Fastify();
// 定义 schema
const schema = `
type Query {
message: String
}
type Mutation {
sendMessage(content: String!): Message!
}
type Subscription {
messageAdded: Message!
}
type Message {
id: ID!
content: String!
}
`;
// 订阅发布器
const pubsub = {
subscriptions: new Map(),
publish(channel, payload) {
if (this.subscriptions.has(channel)) {
this.subscriptions.get(channel).forEach(callback => callback(payload));
}
},
subscribe(channel, callback) {
if (!this.subscriptions.has(channel)) {
this.subscriptions.set(channel, new Set());
}
this.subscriptions.get(channel).add(callback);
return () => {
this.subscriptions.get(channel).delete(callback);
};
},
};
// 定义解析器
const resolvers = {
Query: {
message: () => 'Hello from subscription!',
},
Mutation: {
sendMessage: (_, { content }) => {
const newMessage = {
id: String(Date.now()),
content,
};
// 发布消息
pubsub.publish('MESSAGE_ADDED', { messageAdded: newMessage });
return newMessage;
},
},
Subscription: {
messageAdded: {
subscribe: () => {
return {
async *[Symbol.asyncIterator]() {
const queue = [];
const unsubscribe = pubsub.subscribe('MESSAGE_ADDED', (payload) => {
queue.push(payload);
});
try {
while (true) {
if (queue.length > 0) {
yield queue.shift();
} else {
await new Promise(resolve => setTimeout(resolve, 100));
}
}
} finally {
unsubscribe();
}
},
};
},
},
},
};
// 注册 mercurius 插件
app.register(mercurius, {
schema,
resolvers,
graphiql: true,
subscription: true
});
// 启动服务器
app.listen({ port: 3000 }, (err, address) => {
if (err) {
console.error(err);
process.exit(1);
}
console.log(`Server listening at ${address}`);
});4.2 数据加载器(DataLoader)
Fastify GQL 内置支持 DataLoader,用于解决 N+1 查询问题:
import Fastify from 'fastify';
import mercurius from 'mercurius';
import DataLoader from 'dataloader';
const app = Fastify();
// 模拟数据库
const users = [
{ id: '1', name: 'John Doe', email: 'john@example.com' },
{ id: '2', name: 'Jane Smith', email: 'jane@example.com' }
];
const posts = [
{ id: '1', title: 'First Post', content: 'Hello world!', authorId: '1' },
{ id: '2', title: 'Second Post', content: 'GraphQL is awesome!', authorId: '2' },
{ id: '3', title: 'Third Post', content: 'Fastify is fast!', authorId: '1' }
];
// 定义 schema
const schema = `
type Query {
users: [User!]!
posts: [Post!]!
}
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
content: String!
authorId: ID!
author: User!
}
`;
// 定义解析器
const resolvers = {
Query: {
users: () => users,
posts: () => posts
},
User: {
posts: (parent, args, context) => {
// 使用数据加载器
return context.loaders.postsByAuthorId.load(parent.id);
}
},
Post: {
author: (parent, args, context) => {
// 使用数据加载器
return context.loaders.usersById.load(parent.authorId);
}
}
};
// 注册 mercurius 插件
app.register(mercurius, {
schema,
resolvers,
graphiql: true,
// 创建数据加载器
context: () => ({
loaders: {
// 按作者 ID 加载帖子
postsByAuthorId: new DataLoader(async (authorIds) => {
console.log('Loading posts for authors:', authorIds);
return authorIds.map(authorId =>
posts.filter(post => post.authorId === authorId)
);
}),
// 按 ID 加载用户
usersById: new DataLoader(async (userIds) => {
console.log('Loading users by IDs:', userIds);
return userIds.map(userId =>
users.find(user => user.id === userId)
);
})
}
})
});
// 启动服务器
app.listen({ port: 3000 }, (err, address) => {
if (err) {
console.error(err);
process.exit(1);
}
console.log(`Server listening at ${address}`);
});4.3 文件上传
Fastify GQL 支持文件上传:
import Fastify from 'fastify';
import mercurius from 'mercurius';
import fastifyMultipart from 'fastify-multipart';
const app = Fastify();
// 注册文件上传插件
app.register(fastifyMultipart);
// 定义 schema
const schema = `
scalar Upload
type Query {
hello: String
}
type Mutation {
uploadFile(file: Upload!): String!
}
`;
// 定义解析器
const resolvers = {
Upload: mercurius.GraphQLUpload,
Query: {
hello: () => 'Hello world!',
},
Mutation: {
uploadFile: async (_, { file }, reply) => {
const data = await file;
const { filename, mimetype, encoding, fields } = data;
// 处理文件上传
// 这里只是示例,实际应用中需要实现真实的文件处理逻辑
console.log('Uploaded file:', filename, mimetype, encoding);
return `File ${filename} uploaded successfully`;
},
},
};
// 注册 mercurius 插件
app.register(mercurius, {
schema,
resolvers,
graphiql: true
});
// 启动服务器
app.listen({ port: 3000 }, (err, address) => {
if (err) {
console.error(err);
process.exit(1);
}
console.log(`Server listening at ${address}`);
});5. 最佳实践
5.1 项目结构
推荐的 Fastify GQL 项目结构:
├── src/
│ ├── schema/ # Schema 定义
│ │ ├── typeDefs.js # 类型定义
│ │ └── resolvers/ # 解析器
│ │ ├── Query.js # 查询解析器
│ │ ├── Mutation.js # 变更解析器
│ │ └── Subscription.js # 订阅解析器
│ ├── loaders/ # 数据加载器
│ │ ├── userLoader.js # 用户数据加载器
│ │ └── postLoader.js # 帖子数据加载器
│ ├── utils/ # 工具函数
│ ├── plugins/ # Fastify 插件
│ ├── routes/ # REST 路由
│ ├── context.js # 上下文创建
│ └── server.js # 服务器配置
├── package.json
└── .env5.2 性能优化
- 使用数据加载器:避免 N+1 查询问题
- 实现缓存:减少重复请求
- 优化解析器:避免在解析器中执行昂贵操作
- 使用分页:限制大型数据集的返回
- 监控和分析:使用适当的监控工具
5.3 安全性
- 验证和授权:实现用户认证和权限控制
- 输入验证:验证所有用户输入
- 速率限制:防止 API 滥用
- 安全头部:设置适当的安全 HTTP 头部
- 使用 HTTPS:在生产环境中使用 HTTPS
6. 实用案例
6.1 构建博客 API
6.1.1 Schema 定义
const schema = `
type Query {
# 获取所有文章
articles: [Article!]!
# 根据 ID 获取文章
article(id: ID!): Article
# 获取所有用户
users: [User!]!
# 根据 ID 获取用户
user(id: ID!): User
}
type Mutation {
# 创建文章
createArticle(title: String!, content: String!, authorId: ID!): Article!
# 更新文章
updateArticle(id: ID!, title: String, content: String): Article!
# 删除文章
deleteArticle(id: ID!): Boolean!
# 创建用户
createUser(name: String!, email: String!, password: String!): User!
# 用户登录
login(email: String!, password: String!): Token!
}
type Subscription {
# 文章创建订阅
articleCreated: Article!
}
type Article {
id: ID!
title: String!
content: String!
createdAt: String!
updatedAt: String!
authorId: ID!
author: User!
}
type User {
id: ID!
name: String!
email: String!
createdAt: String!
articles: [Article!]!
}
type Token {
token: String!
user: User!
}
`;6.1.2 解析器实现
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
// 模拟数据库
const users = [];
const articles = [];
// 订阅发布器
const pubsub = {
subscriptions: new Map(),
publish(channel, payload) {
if (this.subscriptions.has(channel)) {
this.subscriptions.get(channel).forEach(callback => callback(payload));
}
},
subscribe(channel, callback) {
if (!this.subscriptions.has(channel)) {
this.subscriptions.set(channel, new Set());
}
this.subscriptions.get(channel).add(callback);
return () => {
this.subscriptions.get(channel).delete(callback);
};
},
};
const resolvers = {
Query: {
articles: () => articles,
article: (_, { id }) => articles.find(article => article.id === id),
users: () => users,
user: (_, { id }) => users.find(user => user.id === id),
},
Mutation: {
createArticle: (_, { title, content, authorId }, context) => {
// 验证用户是否登录
if (!context.user) throw new Error('Unauthorized');
const newArticle = {
id: String(articles.length + 1),
title,
content,
authorId,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
articles.push(newArticle);
// 发布文章创建事件
pubsub.publish('ARTICLE_CREATED', { articleCreated: newArticle });
return newArticle;
},
updateArticle: (_, { id, title, content }, context) => {
// 验证用户是否登录
if (!context.user) throw new Error('Unauthorized');
const article = articles.find(article => article.id === id);
if (!article) throw new Error('Article not found');
// 验证用户是否是文章作者
if (article.authorId !== context.user.id) throw new Error('Forbidden');
if (title) article.title = title;
if (content) article.content = content;
article.updatedAt = new Date().toISOString();
return article;
},
deleteArticle: (_, { id }, context) => {
// 验证用户是否登录
if (!context.user) throw new Error('Unauthorized');
const index = articles.findIndex(article => article.id === id);
if (index === -1) throw new Error('Article not found');
const article = articles[index];
// 验证用户是否是文章作者
if (article.authorId !== context.user.id) throw new Error('Forbidden');
articles.splice(index, 1);
return true;
},
createUser: async (_, { name, email, password }) => {
// 检查邮箱是否已存在
if (users.some(user => user.email === email)) {
throw new Error('Email already exists');
}
// 加密密码
const hashedPassword = await bcrypt.hash(password, 10);
const newUser = {
id: String(users.length + 1),
name,
email,
password: hashedPassword,
createdAt: new Date().toISOString(),
};
users.push(newUser);
return newUser;
},
login: async (_, { email, password }) => {
// 查找用户
const user = users.find(user => user.email === email);
if (!user) throw new Error('Invalid credentials');
// 验证密码
const isValid = await bcrypt.compare(password, user.password);
if (!isValid) throw new Error('Invalid credentials');
// 生成 JWT token
const token = jwt.sign({ id: user.id }, 'secret_key', { expiresIn: '1h' });
return {
token,
user,
};
},
},
Subscription: {
articleCreated: {
subscribe: () => {
return {
async *[Symbol.asyncIterator]() {
const queue = [];
const unsubscribe = pubsub.subscribe('ARTICLE_CREATED', (payload) => {
queue.push(payload);
});
try {
while (true) {
if (queue.length > 0) {
yield queue.shift();
} else {
await new Promise(resolve => setTimeout(resolve, 100));
}
}
} finally {
unsubscribe();
}
},
};
},
},
},
Article: {
author: (parent) => users.find(user => user.id === parent.authorId),
},
User: {
articles: (parent) => articles.filter(article => article.authorId === parent.id),
},
};6.1.3 上下文创建和认证
import jwt from 'jsonwebtoken';
// 认证插件
function authPlugin(fastify, options, done) {
fastify.decorateRequest('user', null);
fastify.addHook('preHandler', async (request, reply) => {
const authHeader = request.headers.authorization;
if (authHeader) {
const token = authHeader.split(' ')[1];
try {
const user = jwt.verify(token, 'secret_key');
request.user = user;
} catch (error) {
console.error('Invalid token:', error);
}
}
});
done();
}
// 注册认证插件
app.register(authPlugin);
// 注册 mercurius 插件
app.register(mercurius, {
schema,
resolvers,
graphiql: true,
subscription: true,
context: (request, reply) => {
return {
user: request.user,
// 可以添加其他上下文信息
db: { /* 数据库连接 */ }
};
}
});6.2 集成数据库
6.2.1 与 MongoDB 集成
npm install mongooseimport Fastify from 'fastify';
import mercurius from 'mercurius';
import mongoose from 'mongoose';
const app = Fastify();
// 连接数据库
mongoose.connect('mongodb://localhost:27017/blog', {
useNewUrlParser: true,
useUnifiedTopology: true,
});
// 定义模型
const User = mongoose.model('User', new mongoose.Schema({
name: String,
email: String,
password: String,
}));
const Post = mongoose.model('Post', new mongoose.Schema({
title: String,
content: String,
authorId: mongoose.Schema.Types.ObjectId,
}));
// 定义 schema
const schema = `
type Query {
users: [User!]!
posts: [Post!]!
}
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
content: String!
authorId: ID!
author: User!
}
`;
// 定义解析器
const resolvers = {
Query: {
users: async () => await User.find(),
posts: async () => await Post.find(),
},
User: {
posts: async (parent) => await Post.find({ authorId: parent.id }),
},
Post: {
author: async (parent) => await User.findById(parent.authorId),
},
};
// 注册 mercurius 插件
app.register(mercurius, {
schema,
resolvers,
graphiql: true,
});
// 启动服务器
app.listen({ port: 3000 }, (err, address) => {
if (err) {
console.error(err);
process.exit(1);
}
console.log(`Server listening at ${address}`);
});7. 总结
Fastify GQL(Mercurius)是一个功能强大、高性能的 GraphQL 服务器实现,与 Fastify 框架无缝集成。通过本教程的学习,你应该已经掌握了:
- Fastify GQL 的核心概念和基本原理
- 如何安装和配置 Fastify GQL
- 如何定义 schema 和实现解析器
- 如何使用 Fastify GQL 的高级特性,如订阅、数据加载器和文件上传
- 最佳实践和实用案例
Fastify GQL 基于 Fastify 框架,提供了优异的性能和易于使用的 API,是构建现代化 GraphQL API 的理想选择。无论是构建小型应用还是大型企业系统,Fastify GQL 都能满足你的需求。