GraphQL在Express中的实现
GraphQL作为一种强大的API查询语言,在现代Web开发中越来越受欢迎。结合Express框架,可以快速构建灵活的数据接口,满足前后端分离的需求。以下从配置、类型定义、解析器到实际查询,逐步展开具体实现方法。
环境准备与基础配置
首先需要安装必要的依赖包。使用npm或yarn初始化项目后,安装以下核心包:
npm install express express-graphql graphql
基础Express服务器配置如下:
const express = require('express');
const { graphqlHTTP } = require('express-graphql');
const { buildSchema } = require('graphql');
const app = express();
app.use('/graphql', graphqlHTTP({
schema: schema,
graphiql: true
}));
app.listen(4000, () => {
console.log('Server running on http://localhost:4000/graphql');
});
关键点说明:
graphqlHTTP
中间件处理所有GraphQL请求graphiql: true
启用交互式查询界面- 默认监听4000端口
类型系统构建
GraphQL的核心是强类型系统。以下示例定义了一个博客系统的数据类型:
type Post {
id: ID!
title: String!
content: String
author: User
}
type User {
id: ID!
name: String!
email: String!
posts: [Post]
}
type Query {
getPost(id: ID!): Post
getAllPosts: [Post]
getUser(id: ID!): User
}
特殊语法说明:
!
表示非空字段[]
表示数组类型ID
是GraphQL内置标量类型
解析器实现
每个字段都需要对应的解析函数。以下是匹配上述类型的解析器:
const root = {
getPost: ({ id }) => {
return db.posts.find(post => post.id === id);
},
getAllPosts: () => db.posts,
getUser: ({ id }) => {
const user = db.users.find(user => user.id === id);
user.posts = db.posts.filter(post => post.authorId === id);
return user;
},
Post: {
author: (parent) => db.users.find(user => user.id === parent.authorId)
}
};
嵌套解析特点:
- 父对象会作为
parent
参数传入 - 可以异步获取数据(返回Promise即可)
- 每个字段可以独立解析
查询与变更操作
基本查询示例
获取所有文章标题的GraphQL查询:
query {
getAllPosts {
title
author {
name
}
}
}
响应数据结构示例:
{
"data": {
"getAllPosts": [
{
"title": "GraphQL入门",
"author": {
"name": "张三"
}
}
]
}
}
带参数的查询
获取特定用户信息的查询:
query GetUser($userId: ID!) {
getUser(id: $userId) {
name
email
posts {
title
}
}
}
对应的查询变量:
{
"userId": "user123"
}
数据变更操作
首先需要扩展Schema:
type Mutation {
createPost(title: String!, content: String): Post
updatePost(id: ID!, title: String): Post
}
然后实现对应的解析器:
const root = {
// ...其他解析器...
createPost: ({ title, content }) => {
const newPost = { id: generateId(), title, content };
db.posts.push(newPost);
return newPost;
},
updatePost: ({ id, title }) => {
const post = db.posts.find(p => p.id === id);
if (title) post.title = title;
return post;
}
};
变更操作调用示例:
mutation {
createPost(title: "新文章", content: "内容") {
id
title
}
}
高级功能实现
自定义标量类型
添加日期类型需要以下步骤:
- 定义标量类型:
scalar Date
- 实现序列化逻辑:
const { GraphQLScalarType } = require('graphql');
const DateScalar = new GraphQLScalarType({
name: 'Date',
serialize(value) {
return new Date(value).toISOString();
},
parseValue(value) {
return new Date(value);
}
});
- 在Schema解析时加入:
const schema = buildSchema(typeDefs);
schema._typeMap.Date = DateScalar;
批量查询优化
使用DataLoader解决N+1查询问题:
const DataLoader = require('dataloader');
const userLoader = new DataLoader(async (userIds) => {
const users = await db.users.find({ id: { $in: userIds } });
return userIds.map(id => users.find(u => u.id === id));
});
// 在解析器中调用
Post: {
author: (parent) => userLoader.load(parent.authorId)
}
订阅功能实现
- 安装额外依赖:
npm install graphql-subscriptions
- 创建PubSub实例:
const { PubSub } = require('graphql-subscriptions');
const pubsub = new PubSub();
- 扩展Schema:
type Subscription {
postCreated: Post
}
- 实现解析器:
Subscription: {
postCreated: {
subscribe: () => pubsub.asyncIterator(['POST_CREATED'])
}
},
Mutation: {
createPost: (args) => {
const post = createPost(args);
pubsub.publish('POST_CREATED', { postCreated: post });
return post;
}
}
错误处理与验证
自定义错误格式
修改graphqlHTTP配置:
app.use('/graphql', graphqlHTTP({
schema: schema,
graphiql: true,
customFormatErrorFn: (err) => {
return {
message: err.message,
locations: err.locations,
stack: process.env.NODE_ENV === 'development' ? err.stack : null
};
}
}));
输入验证
使用自定义指令实现参数验证:
directive @length(max: Int) on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
type Mutation {
createPost(
title: String! @length(max: 100)
content: String @length(max: 1000)
): Post
}
实现验证逻辑:
class LengthDirective extends SchemaDirectiveVisitor {
visitInputFieldDefinition(field) {
const { max } = this.args;
field.type = new GraphQLNonNull(
new GraphQLScalarType({
name: `LengthAtMost${max}`,
serialize: (value) => {
if (value.length > max) {
throw new Error(`长度不能超过${max}字符`);
}
return value;
}
})
);
}
}
性能监控
添加Apollo Tracing支持:
app.use('/graphql', graphqlHTTP({
schema: schema,
tracing: true,
extensions: ({ document, variables, operationName, result }) => {
return {
tracing: result.extensions?.tracing
};
}
}));
生成的响应会包含详细的执行时间数据:
{
"extensions": {
"tracing": {
"version": 1,
"startTime": "2023-01-01T00:00:00.000Z",
"endTime": "2023-01-01T00:00:00.020Z",
"duration": 20000000,
"execution": {
"resolvers": [
{
"path": ["getAllPosts"],
"parentType": "Query",
"fieldName": "getAllPosts",
"returnType": "[Post]",
"startOffset": 1000000,
"duration": 5000000
}
]
}
}
}
}
实际项目结构建议
中型项目的推荐目录结构:
/src
/graphql
/types
post.graphql
user.graphql
/resolvers
post.resolver.js
user.resolver.js
schema.js
/models
post.model.js
user.model.js
server.js
schema.js的合并方式:
const { mergeTypeDefs } = require('@graphql-tools/merge');
const fs = require('fs');
const path = require('path');
const typesArray = [];
fs.readdirSync(path.join(__dirname, 'types')).forEach(file => {
typesArray.push(fs.readFileSync(path.join(__dirname, 'types', file), 'utf8'));
});
module.exports = mergeTypeDefs(typesArray);
身份验证集成
JWT验证中间件示例:
const authMiddleware = async (req) => {
const token = req.headers.authorization?.split(' ')[1];
if (token) {
try {
const user = jwt.verify(token, SECRET);
return { user };
} catch (e) {
throw new AuthenticationError('无效的token');
}
}
return {};
};
app.use('/graphql', graphqlHTTP(async (req) => ({
schema,
context: await authMiddleware(req)
})));
在解析器中获取用户上下文:
{
Query: {
myPosts: (_, args, context) => {
if (!context.user) throw new Error('未授权');
return db.posts.filter(p => p.authorId === context.user.id);
}
}
}
文件上传处理
- 安装依赖:
npm install graphql-upload
- 添加中间件:
const { graphqlUploadExpress } = require('graphql-upload');
app.use(graphqlUploadExpress());
- 定义上传类型:
scalar Upload
type Mutation {
uploadFile(file: Upload!): Boolean
}
- 实现解析器:
{
Mutation: {
uploadFile: async (_, { file }) => {
const { createReadStream, filename } = await file;
const stream = createReadStream();
return new Promise((resolve, reject) => {
stream.pipe(fs.createWriteStream(`./uploads/${filename}`))
.on('finish', () => resolve(true))
.on('error', reject);
});
}
}
}
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn
上一篇:Express与前端框架的集成