阿里云主机折上折
  • 微信号
您当前的位置:网站首页 > GraphQL在Express中的实现

GraphQL在Express中的实现

作者:陈川 阅读数:4775人阅读 分类: Node.js

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
  }
}

高级功能实现

自定义标量类型

添加日期类型需要以下步骤:

  1. 定义标量类型:
scalar Date
  1. 实现序列化逻辑:
const { GraphQLScalarType } = require('graphql');
const DateScalar = new GraphQLScalarType({
  name: 'Date',
  serialize(value) {
    return new Date(value).toISOString();
  },
  parseValue(value) {
    return new Date(value);
  }
});
  1. 在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)
}

订阅功能实现

  1. 安装额外依赖:
npm install graphql-subscriptions
  1. 创建PubSub实例:
const { PubSub } = require('graphql-subscriptions');
const pubsub = new PubSub();
  1. 扩展Schema:
type Subscription {
  postCreated: Post
}
  1. 实现解析器:
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);
    }
  }
}

文件上传处理

  1. 安装依赖:
npm install graphql-upload
  1. 添加中间件:
const { graphqlUploadExpress } = require('graphql-upload');

app.use(graphqlUploadExpress());
  1. 定义上传类型:
scalar Upload

type Mutation {
  uploadFile(file: Upload!): Boolean
}
  1. 实现解析器:
{
  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

前端川

前端川,陈川的代码茶馆🍵,专治各种不服的Bug退散符💻,日常贩卖秃头警告级的开发心得🛠️,附赠一行代码笑十年的摸鱼宝典🐟,偶尔掉落咖啡杯里泡开的像素级浪漫☕。‌