随着 GraphQL 应用程序从演示、概念验证到生产的发展,Schema 和 resolver 的复杂性也会随之增长。为了组织代码,我们可能需要将 schema type 和相关的 resolver 分割成多个文件。
我们经常收到这样的问题,因为有很多不同的方法来拆分 schema 代码,而且也许看起来你需要复杂的设置来获得好的结果。但事实证明,只需要几个简单的 JavaScript 概念,就可以将 schema 和 resolver 代码分离到单独的文件中。
在这篇文章中,我们介绍了一种直接的方法,对用 graphql-tools
构建的 schema 进行模块化,你可以进行调整,以适应自己的喜好和代码库的风格。
Schema
如果你刚刚起步,并且在一个文件中定义了你的整个 Schema,它可能看起来很像下面的片段。在这里,我们称它为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: {},
});
理想情况下,我们不想把所有的东西都放在一个 schema 定义字符串里,而想把Author
和Book
的 schema 类型分别放在名为author.js
和book.js
的文件中。
我们在 Schema 定义语言(SDL)中编写的 schema 定义只是字符串。对它们,我们有一个简单的方法来导入不同文件中的类型定义——把字符串分割成多个字符串,之后进行组合。这是author.js
在进行上述处理后应该的样子:
// author.js
export const typeDef = `
type Author {
id: Int!
firstName: String
lastName: String
books: [Book]
}
`;
而book.js
应该是这样:
// author.js
export const typeDef = `
type Author {
id: Int!
firstName: String
lastName: String
books: [Book]
}
`;
最后,我们在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: {},
});
我们在这里并没有做任何花哨的事情:我们只是导入恰好包含 SDL 的字符串。请注意,为了方便,你不需要自己组合字符串——makeExecutableSchema
实际上可以直接接受一个类型定义的数组,以适应这种方法。
Resolvers
现在,我们已经有办法将 schema 分解成各个部分,但我们还希望能够将每个 resolver 与对应 schema 相关的部分一起移动。一般来说,我们会需要把某个 schama 的 resolver 与该 schema 的模式定义保存在同一个文件中。
在上一个例子的基础上进行扩展,这是我们的schema.js
文件,其中增加了一些 resolver。
// 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,
});
就像拆分 schema 定义字符串一样,我们也可以拆分resolvers
对象。我们可以把其中的一部分放在author.js
中,另一部分放在book.js
中,然后导入它们,并使用lodash.merge
函数把它们在schema.js
中进行组合。
这是author.js
会变成的样子:
// author.js
export const typeDef = `
type Author {
id: Int!
firstName: String
lastName: String
books: [Book]
}
`;
export const resolvers = {
Author: {
books: () => { ... },
}
};
而book.js
应该变成这样:
// book.js
export const typeDef = `
type Book {
title: String
author: Author
}
`;
export const resolvers = {
Book: {
author: () => { ... },
}
};
然后,在schema.js
中用lodash.merge
把它们组合在一起:
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),
});
这样重构以后的结构与我们一开始的resolvers
结构是完全等价的。
扩展类型
我们仍然在schema.js
中把authors
和books
定义为Query
上的顶层字段,然而,这些字段在逻辑上是与Author
和Book
联系在一起的,它们应该被放在author.js
和book.js
中。
为了达到这个目的,我们可以使用类型扩展。我们可以这样定义现有的Query
类型:
const Query = `
type Query {
_empty: String
}
extend type Query {
author(id: Int!): Author
}
extend type Query {
book(id: Int!): Book
}
`;
注意:在当前版本的 GraphQL 中不能使用空类型,即使你打算在程序的其余部分扩展它。所以我们需要确保原来的 Query 类型至少有一个字段——在这种情况下,我们可以添加一个假的
_empty
字段。在未来的 GraphQL 版本中,我们也许可以使用空类型,然后在程序的其余部分进行扩展。
基本上,extend
关键字让我们可以为已经定义的类型添加字段。我们可以使用这个关键字在book.js
和author.js
中定义与这些类型相关的Query
字段。然后我们还应该在同一个地方为这些类型定义Query resolver
。
下面是这样以后author.js
的样子:
// 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: () => { ... },
}
};
这是book.js
的样子:
// 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: () => { ... },
}
};
我们在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),
});
现在,schema 和 resolver 的定义与相关类型终于被放在一起了。
最后的建议
我们刚刚经历了服务器代码模块化的机制。这里有一些额外的提示,可能会对你了解如何划分代码库有所帮助:
- 在学习、原型设计甚至构建 POC 时,将你的整个 schema 放在一个文件中可能是不错的。这样做的好处是可以快速浏览整个 schema,或者向同事解释。
- 你可以按照功能来组织你的 schema 和 resolver:例如,把与结账系统有关的东西放在一起,在电子商务网站中可能是有意义的。
- 将 resolver 与相关的 schema 定义保存在同一个文件中。这将使你能够有效地对你的代码进行管理。
- 使用graphql-tag将你的 SDL 类型定义用
gql
标签包装起来。如果你的编辑器使用 GraphQL Plugin 或 Prettier 对代码进行格式化,只要在 SDL 的前缀加上 gql 标签,编辑器中就能有对应的语法高亮。