笔记:模块化你的GraphQL

696 阅读3分钟

notes: blog.apollographql.com/modularizin…

As your GraphQL application grows from demo, to proof of concept, to production, the complexity of your schema and resolvers will grow in tandem. To organize your code, you’ll want to split up your schema types and the associated resolvers into multiple files.

We can arrange our schema and resolver code in separate files with just a few simple JavaScript concepts.

Schema

If you’re just starting out and have your entire schema defined in one file, it might look much like the below snippet. Here, we’ll call it schema.js:

// schema.js
const typeDefs = `
  type Query {
    author(id: Int!): Post
    book(id: Int!): Post
  }
  type Author {
    id: Int!
    firstName: String
    lastName: String
    books: [Book]
  }
  type Book {
    title: String
    author: Author
  }
`;
makeExecutableSchema({
  typeDefs: typeDefs,
  resolvers: {},
});

We’d like to put the schema types for Author and Book in files called author.js and book.js.At the end of the day, the schema definitions we’ve written in the Schema Definition Language (SDL) are just strings. Treating them as such, we have a simple way of importing type definitions across different files: Split up the string into multiple strings that we can combine later. This is what author.js would look like:

// author.js
export const typeDef = `
  type Author {
    id: Int!
    firstName: String
    lastName: String
    books: [Book]
  }
`;

Here’s book.js:

// book.js
export const typeDef = `
  type Book {
    title: String
    author: Author
  }
`;

Finally, we pull it all together in schema.js:

// schema.js
import { typeDef as Author } from './author.js';
import { typeDef as Book } from './book.js';
const Query = `
  type Query {
    author(id: Int!): Post
    book(id: Int!): Post
  }
`;
makeExecutableSchema({
  typeDefs: [ Query, Author, Book ],
  resolvers: {},
});

Resolvers

We want to be able to move each resolver with its associated part of the schema as well. In general, we want to keep resolvers for a certain type in the same file as the schema definition for that type.Expanding on our previous example, here’s our schema.js file with some resolvers added into the picture:

// schema.js
import { typeDef as Author } from './author.js';
import { typeDef as Book } from './book.js';
const Query = `
  type Query {
    author(id: Int!): Post
    book(id: Int!): Post
  }
`;
const resolvers = {
  Query: {
    author: () => { ... },
    book: () => { ... },
  },
  Author: {
    name: () => { ... },
  },
  Book: {
    title: () => { ... },
  },
};
makeExecutableSchema({
  typeDefs: [ Query, Author, Book ],
  resolvers,
});

We can split up the resolvers object. We can put a piece of it in author.js, another in book.js, and then import them and use the lodash.merge function to put it all together in schema.js.Here’s what author.js would look like in that case:

// author.js
export const typeDef = `
  type Author {
    id: Int!
    firstName: String
    lastName: String
    books: [Book]
  }
`;
export const resolvers = {
  Author: {
    books: () => { ... },
  }
};

Here’s book.js:

// book.js
export const typeDef = `
  type Book {
    title: String
    author: Author
  }
`;
export const resolvers = {
  Book: {
    author: () => { ... },
  }
};

Then, we apply lodash.merge in schema.js to put everything together:

import { merge } from 'lodash';
import { 
  typeDef as Author, 
  resolvers as authorResolvers,
} from './author.js';
import { 
  typeDef as Book, 
  resolvers as bookResolvers,
} from './book.js';
const Query = `
  type Query {
    author(id: Int!): Author
    book(id: Int!): Book
  }
`;
const resolvers = {
  Query: { 
    ...,
  }
};
makeExecutableSchema({
  typeDefs: [ Query, Author, Book ],
  resolvers: merge(resolvers, authorResolvers, bookResolvers),
});

And that gives us the resolvers structure that we started out with!

Extending types in multiple files

We’re still defining authors and books as top-level fields on Query within schema.js, even though these are logically tied to Author and Book and should live in author.js and book.js.To accomplish that, we can use type extensions. We can define our existing Query type like this:

const Query = `
  type Query {
    _empty: String
  }
  
  extend type Query {
    author(id: Int!): Author 
  }
  
  extend type Query {
    book(id: Int!): Book 
  }
`;

Note: In the current version of GraphQL, you can’t have an empty type even if you intend to extend it later. So we need to make sure the Query type has at least one field — in this case we can add a fake _empty field. Hopefully in future versions it will be possible to have an empty type to be extended later. Basically, the extend keyword lets us add fields to an already-defined type. We can use this keyword in order to define Query fields relevant to those types in book.js and author.js. We should then also move to defining the Query resolvers for those types in the same place.Here’s what author.js looks like with this approach:

// author.js
export const typeDef = `
  extend type Query {
    author(id: Int!): Author
  }
  type Author {
    id: Int!
    firstName: String
    lastName: String
    books: [Book]
  }
`;
export const resolvers = {
  Query: {
    author: () => { ... },
  },
  Author: {
    books: () => { ... },
  }
};

This is what book.js looks like:

// book.js
export const typeDef = `
  extend type Query {
    book(id: Int!): Book
  }
  type Book {
    title: String
    author: Author
  }
`;
export const resolvers = {
  Query: {
    book: () => { ... },
  },
  Book: {
    author: () => { ... },
  }
};

Just as before, we put it all together in schema.js:

import { merge } from 'lodash';
import { 
  typeDef as Author, 
  resolvers as authorResolvers,
} from './author.js';
import { 
  typeDef as Book, 
  resolvers as bookResolvers,
} from './book.js';
// If you had Query fields not associated with a
// specific type you could put them here
const Query = `
  type Query {
    _empty: String
  }
`;
const resolvers = {};
makeExecutableSchema({
  typeDefs: [ Query, Author, Book ],
  resolvers: merge(resolvers, authorResolvers, bookResolvers),
});

Now, the schema and resolver definitions are properly co-located with the associated types!

Final tips

Wrap your SDL type definitions with a gql tag using graphql-tag. If you’re using a GraphQL plugin for your editor or formatting your code with Prettier, you’ll be able to get syntax highlighting for SDL within your code editor as long as you prefix it with the gql tag.