您现在的位置是:网站首页 > GraphQL在Express中的实现文章详情

GraphQL在Express中的实现

GraphQL简介

GraphQL是一种由Facebook开发的API查询语言,它提供了一种更高效、更灵活的方式来获取数据。与传统的REST API相比,GraphQL允许客户端精确指定需要的数据结构,避免了过度获取或不足获取数据的问题。在Express框架中集成GraphQL可以显著提升API的灵活性和开发效率。

Express中集成GraphQL的基本步骤

在Express应用中实现GraphQL需要安装express-graphqlgraphql这两个核心包。首先通过npm或yarn安装它们:

npm install express-graphql graphql

然后创建一个基本的GraphQL服务:

const express = require('express');
const { graphqlHTTP } = require('express-graphql');
const { buildSchema } = require('graphql');

const schema = buildSchema(`
  type Query {
    hello: String
  }
`);

const root = {
  hello: () => 'Hello world!'
};

const app = express();
app.use('/graphql', graphqlHTTP({
  schema: schema,
  rootValue: root,
  graphiql: true,
}));

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

这个例子创建了一个简单的GraphQL端点,当查询hello字段时会返回字符串"Hello world!"。

定义GraphQL Schema

Schema是GraphQL的核心,它定义了API的类型系统和可用操作。在Express中,我们可以使用GraphQL的Schema Definition Language (SDL)来定义schema:

const schema = buildSchema(`
  type Book {
    title: String
    author: String
  }

  type Query {
    books: [Book]
    book(title: String!): Book
  }

  type Mutation {
    addBook(title: String!, author: String!): Book
  }
`);

这个schema定义了一个图书查询系统,包含获取所有图书、按标题查询单本图书以及添加新图书的功能。

实现Resolver函数

Resolver是GraphQL中实际获取数据的函数。每个字段类型都需要对应的resolver:

const books = [
  { title: 'The Hobbit', author: 'J.R.R. Tolkien' },
  { title: '1984', author: 'George Orwell' }
];

const root = {
  books: () => books,
  book: ({ title }) => books.find(book => book.title === title),
  addBook: ({ title, author }) => {
    const newBook = { title, author };
    books.push(newBook);
    return newBook;
  }
};

Resolver函数接收参数并返回对应的数据。对于查询操作,参数来自客户端请求;对于变更操作,参数包含客户端提供的输入数据。

处理复杂查询

GraphQL的强大之处在于处理复杂查询和嵌套数据。例如,我们可以扩展图书系统,添加作者信息:

const schema = buildSchema(`
  type Author {
    name: String
    books: [Book]
  }

  type Book {
    title: String
    author: Author
  }

  type Query {
    books: [Book]
    authors: [Author]
  }
`);

const authors = [
  { name: 'J.R.R. Tolkien', books: ['The Hobbit', 'The Lord of the Rings'] },
  { name: 'George Orwell', books: ['1984', 'Animal Farm'] }
];

const books = [
  { title: 'The Hobbit', author: 'J.R.R. Tolkien' },
  { title: '1984', author: 'George Orwell' }
];

const root = {
  books: () => books.map(book => ({
    ...book,
    author: authors.find(author => author.name === book.author)
  })),
  authors: () => authors.map(author => ({
    ...author,
    books: books.filter(book => book.author === author.name)
  }))
};

这样客户端可以灵活地查询图书及其作者信息,或者作者及其作品列表。

错误处理和验证

在GraphQL中实现错误处理需要考虑多种情况。express-graphql中间件提供了基本的错误处理功能:

app.use('/graphql', graphqlHTTP({
  schema: schema,
  rootValue: root,
  graphiql: true,
  customFormatErrorFn: (err) => {
    if (!err.originalError) {
      return err;
    }
    const data = err.originalError.data;
    const message = err.message || 'An error occurred';
    const code = err.originalError.code || 500;
    return { message, status: code, data };
  }
}));

对于业务逻辑错误,可以在resolver中抛出特定错误:

const root = {
  book: ({ title }) => {
    const book = books.find(book => book.title === title);
    if (!book) {
      throw new Error('Book not found');
    }
    return book;
  }
};

性能优化

GraphQL查询可能会引发N+1查询问题。使用DataLoader可以批量加载数据,显著提高性能:

const DataLoader = require('dataloader');

const batchAuthors = async (authorNames) => {
  return authorNames.map(name => 
    authors.find(author => author.name === name)
  );
};

const authorLoader = new DataLoader(batchAuthors);

const root = {
  books: () => books,
  book: ({ title }) => books.find(book => book.title === title),
  author: ({ name }) => authorLoader.load(name)
};

DataLoader会收集单个请求中的所有加载请求,然后批量处理,减少数据库查询次数。

认证和授权

在Express中实现GraphQL的认证授权通常结合中间件:

const authMiddleware = (req, res, next) => {
  const token = req.headers.authorization;
  if (!token) {
    throw new Error('Not authenticated');
  }
  // 验证token逻辑
  next();
};

app.use('/graphql', authMiddleware, graphqlHTTP({
  schema: schema,
  rootValue: root,
  graphiql: true
}));

对于更细粒度的授权控制,可以在resolver中检查权限:

const root = {
  addBook: ({ title, author }, context) => {
    if (!context.user || !context.user.isAdmin) {
      throw new Error('Unauthorized');
    }
    const newBook = { title, author };
    books.push(newBook);
    return newBook;
  }
};

app.use('/graphql', graphqlHTTP({
  schema: schema,
  rootValue: root,
  graphiql: true,
  context: { user: getUserFromRequest(req) }
}));

订阅功能实现

GraphQL订阅允许实现实时功能。虽然express-graphql不支持订阅,但可以结合graphql-wssubscriptions-transport-ws实现:

const { createServer } = require('http');
const { execute, subscribe } = require('graphql');
const { SubscriptionServer } = require('subscriptions-transport-ws');

const server = createServer(app);

server.listen(4000, () => {
  new SubscriptionServer({
    execute,
    subscribe,
    schema
  }, {
    server,
    path: '/subscriptions',
  });
});

然后定义包含订阅的schema:

const schema = buildSchema(`
  type Subscription {
    bookAdded: Book
  }

  type Mutation {
    addBook(title: String!, author: String!): Book
  }

  type Query {
    books: [Book]
  }

  type Book {
    title: String
    author: String
  }
`);

与数据库集成

实际应用中,GraphQL通常与数据库交互。以下是与MongoDB集成的示例:

const { MongoClient } = require('mongodb');

let db;

MongoClient.connect('mongodb://localhost:27017', (err, client) => {
  if (err) throw err;
  db = client.db('library');
});

const root = {
  books: async () => {
    return await db.collection('books').find().toArray();
  },
  addBook: async ({ title, author }) => {
    const result = await db.collection('books').insertOne({ title, author });
    return result.ops[0];
  }
};

对于更复杂的应用,可以考虑使用TypeORM或Prisma等ORM工具。

分页和过滤

实现分页是API的常见需求。GraphQL中可以通过参数实现:

const schema = buildSchema(`
  type Query {
    books(limit: Int = 10, offset: Int = 0): [Book]
  }
`);

const root = {
  books: async ({ limit, offset }) => {
    return await db.collection('books')
      .find()
      .skip(offset)
      .limit(limit)
      .toArray();
  }
};

客户端查询时可以指定参数:

{
  books(limit: 5, offset: 10) {
    title
    author
  }
}

文件上传

处理文件上传需要额外的配置。可以使用graphql-upload

const { graphqlUploadExpress } = require('graphql-upload');

app.use(graphqlUploadExpress());
app.use('/graphql', graphqlHTTP({ schema, rootValue: root }));

然后在schema中定义上传类型:

type Mutation {
  uploadBookCover(bookId: ID!, file: Upload!): Boolean
}

对应的resolver实现:

const { processRequest } = require('graphql-upload');

const root = {
  uploadBookCover: async ({ bookId, file }) => {
    const { createReadStream, filename } = await file;
    const stream = createReadStream();
    // 处理文件流
    return true;
  }
};

版本控制和演进

GraphQL schema会随着时间演进。处理版本变化的策略包括:

  1. 添加新字段而不是修改现有字段
  2. 使用@deprecated指令标记废弃字段
  3. 通过中间件实现版本控制
type Book {
  title: String
  author: String
  isbn: String @deprecated(reason: "Use id instead")
  id: ID!
}

监控和日志

监控GraphQL API性能很重要。可以添加日志中间件:

app.use('/graphql', (req, res, next) => {
  const start = Date.now();
  res.on('finish', () => {
    const duration = Date.now() - start;
    console.log(`GraphQL request took ${duration}ms`);
  });
  next();
}, graphqlHTTP({ schema, rootValue: root }));

对于更详细的性能分析,可以使用Apollo Engine或GraphQL Inspector等工具。

生产环境最佳实践

在生产环境中部署GraphQL服务时,应考虑:

  1. 启用查询复杂度分析防止恶意复杂查询
  2. 实现查询缓存
  3. 设置适当的超时限制
  4. 限制查询深度
app.use('/graphql', graphqlHTTP({
  schema,
  rootValue: root,
  validationRules: [depthLimit(5)],
  graphiql: process.env.NODE_ENV !== 'production'
}));

与现有REST API共存

逐步迁移到GraphQL时,可能需要与现有REST API共存。可以在Express中同时提供两种端点:

// REST端点
app.get('/api/books', (req, res) => {
  res.json(books);
});

// GraphQL端点
app.use('/graphql', graphqlHTTP({ schema, rootValue: root }));

也可以在GraphQL resolver中调用现有的REST API:

const root = {
  books: async () => {
    const response = await fetch('http://localhost:3000/api/books');
    return response.json();
  }
};

我的名片

网名:~川~

岗位:console.log 调试员

坐标:重庆市-九龙坡区

邮箱:cc@qdcc.cn

沙漏人生

站点信息

  • 建站时间:2013/03/16
  • 本站运行
  • 文章数量
  • 总访问量
微信公众号
每次关注
都是向财富自由迈进的一步