2016 年 5 月 5 日 由 Steven Luscher
我一次又一次地听到来自前端 Web 和移动开发人员的相同愿望:他们渴望利用 Relay 和 GraphQL 等新技术提供的开发效率提升,但他们现有的 REST API 已经积累了多年的发展势头。如果没有明确证明切换的好处的数据,他们很难为 GraphQL 基础设施的额外投资辩护。
在这篇文章中,我将概述一种快速、低投资的方法,您可以使用它在现有的 REST API 之上建立一个 GraphQL 端点,仅使用 JavaScript。在编写这篇文章的过程中,不会对任何后端开发人员造成伤害。
我们将创建一个 *GraphQL 模式* - 一个描述您的数据宇宙的类型系统 - 它将包装对您现有 REST API 的调用。此模式将在客户端接收和解析 GraphQL 查询。这种架构具有一些固有的性能缺陷,但实施速度快,不需要进行任何服务器更改。
想象一个 REST API,它通过一个 `/people/` 端点公开 `Person` 模型及其关联的朋友。
我们将构建一个 GraphQL 模式,该模式对人和他们的属性(如 `first_name` 和 `email`)以及他们通过友谊与其他人的关联进行建模。
首先,我们需要一组模式构建工具。
npm install --save graphql
最终,我们希望导出一个 `GraphQLSchema`,我们可以用它来解析查询。
import { GraphQLSchema } from "graphql"
export default new GraphQLSchema({ query: QueryType,})
在所有 GraphQL 模式的根部是一个名为 `query` 的类型,我们提供其定义,并在此处指定为 `QueryType`。现在让我们构建 `QueryType` - 一个类型,我们将在其上定义人们可能想要获取的所有可能内容。
为了复制我们 REST API 的所有功能,让我们在 `QueryType` 上公开两个字段
每个字段将包含一个返回类型、可选参数定义和一个解析正在查询数据的 JavaScript 方法。
import { GraphQLList, GraphQLObjectType, GraphQLString,} from 'graphql';
const QueryType = new GraphQLObjectType({ name: 'Query', description: 'The root of all... queries', fields: () => ({ allPeople: { type: new GraphQLList(PersonType), resolve: root => // Fetch the index of people from the REST API, }, person: { type: PersonType, args: { id: { type: GraphQLString }, }, resolve: (root, args) => // Fetch the person with ID `args.id`, }, }),});
现在让我们先把解析器留作草稿,然后继续定义 PersonType
。
import { GraphQLList, GraphQLObjectType, GraphQLString,} from 'graphql';
const PersonType = new GraphQLObjectType({ name: 'Person', description: 'Somebody that you used to know', fields: () => ({ firstName: { type: GraphQLString, resolve: person => person.first_name, }, lastName: { type: GraphQLString, resolve: person => person.last_name, }, email: {type: GraphQLString}, id: {type: GraphQLString}, username: {type: GraphQLString}, friends: { type: new GraphQLList(PersonType), resolve: person => // Fetch the friends with the URLs `person.friends`, }, }),});
关于 PersonType
的定义,请注意两点。首先,我们没有为 email
、id
或 username
提供解析器。默认解析器只需访问与字段同名的 person
对象的属性。这在所有情况下都有效,除了属性名称与字段名称不匹配的情况(例如,字段 firstName
与 REST API 响应对象中的 first_name
属性不匹配)或访问属性不会产生我们想要的对象的情况(例如,我们想要 friends
字段的人员对象列表,而不是 URL 列表)。
现在,让我们编写从 REST API 获取人员的解析器。因为我们需要从网络加载,所以我们无法立即返回一个值。幸运的是,resolve()
可以返回一个值或一个值的 Promise
。我们将利用这一点来向 REST API 发起一个 HTTP 请求,该请求最终解析为符合 PersonType
的 JavaScript 对象。
这就是它 - 对模式的完整第一遍
import { GraphQLList, GraphQLObjectType, GraphQLSchema, GraphQLString,} from 'graphql';
const BASE_URL = 'https://myapp.com/';
function fetchResponseByURL(relativeURL) { return fetch(`${BASE_URL}${relativeURL}`).then(res => res.json());}
function fetchPeople() { return fetchResponseByURL('/people/').then(json => json.people);}
function fetchPersonByURL(relativeURL) { return fetchResponseByURL(relativeURL).then(json => json.person);}
const PersonType = new GraphQLObjectType({ /* ... */ fields: () => ({ /* ... */ friends: { type: new GraphQLList(PersonType), resolve: person => person.friends.map(fetchPersonByURL), }, }),});
const QueryType = new GraphQLObjectType({ /* ... */ fields: () => ({ allPeople: { type: new GraphQLList(PersonType), resolve: fetchPeople, }, person: { type: PersonType, args: { id: { type: GraphQLString }, }, resolve: (root, args) => fetchPersonByURL(`/people/${args.id}/`), }, }),});
export default new GraphQLSchema({ query: QueryType,});
通常,Relay 会通过 HTTP 将其 GraphQL 查询发送到服务器。我们可以注入 @taion 的自定义 relay-local-schema
网络层来使用我们刚刚构建的模式解析查询。将此代码放在任何保证在挂载 Relay 应用程序之前执行的地方。
npm install --save relay-local-schema
import RelayLocalSchema from 'relay-local-schema'; import schema from './schema'; Relay.injectNetworkLayer( new RelayLocalSchema.NetworkLayer({ schema }));
就是这样。Relay 会将所有查询发送到您自定义的客户端驻留模式,该模式反过来会通过调用您现有的 REST API 来解析它们。
上面演示的客户端 REST API 包装器应该可以帮助您快速入门,以便您可以尝试 Relay 版本的应用程序(或应用程序的一部分)。
但是,正如我们之前提到的,这种架构由于 GraphQL 仍然调用您的底层 REST API,而这可能非常网络密集,因此存在一些固有的性能缺陷。下一步是将模式从客户端移到服务器端,以最大程度地减少网络上的延迟,并为您提供更多缓存响应的能力。
花接下来的 10 分钟时间观看我使用 Node 和 Express 构建上面 GraphQL 包装器的服务器端版本。
我们上面开发的模式将适用于 Relay,直到您要求 Relay 为您已经下载的记录重新获取数据为止。Relay 的重新获取子系统依赖于您的 GraphQL 模式公开一个特殊字段,该字段可以通过 GUID 获取数据宇宙中的任何实体。我们称之为节点接口。
要公开节点接口,您需要执行以下两项操作:在查询的根部提供一个node(id: String!)
字段,并将所有 ID 切换到 GUID(全局唯一 ID)。
graphql-relay
包含一些辅助函数,可以轻松完成此操作。
npm install --save graphql-relay
首先,让我们将PersonType
的id
字段更改为 GUID。为此,我们将使用graphql-relay
中的globalIdField
辅助函数。
import { globalIdField } from "graphql-relay"
const PersonType = new GraphQLObjectType({ name: "Person", description: "Somebody that you used to know", fields: () => ({ id: globalIdField("Person"), /* ... */ }),})
在幕后,globalIdField
返回一个字段定义,该定义通过将类型名称'Person'
和 REST API 返回的 ID 进行哈希来将id
解析为GraphQLString
。稍后,我们可以使用fromGlobalId
将此字段的结果转换回'Person'
和 REST API 的 ID。
graphql-relay
中的另一组辅助函数将帮助我们开发节点字段。您的工作是为辅助函数提供两个函数
import { fromGlobalId, nodeDefinitions } from "graphql-relay"
const { nodeInterface, nodeField } = nodeDefinitions( globalId => { const { type, id } = fromGlobalId(globalId) if (type === "Person") { return fetchPersonByURL(`/people/${id}/`) } }, object => { if (object.hasOwnProperty("username")) { return "Person" } })
上面的对象到类型名称解析器不是工程学上的奇迹,但您明白了。
接下来,我们只需要将nodeInterface
和nodeField
添加到我们的模式中。以下是一个完整的示例
import { GraphQLList, GraphQLObjectType, GraphQLSchema, GraphQLString,} from 'graphql';import { fromGlobalId, globalIdField, nodeDefinitions,} from 'graphql-relay';
const BASE_URL = 'https://myapp.com/';
function fetchResponseByURL(relativeURL) { return fetch(`${BASE_URL}${relativeURL}`).then(res => res.json());}
function fetchPeople() { return fetchResponseByURL('/people/').then(json => json.people);}
function fetchPersonByURL(relativeURL) { return fetchResponseByURL(relativeURL).then(json => json.person);}
const { nodeInterface, nodeField } = nodeDefinitions( globalId => { const { type, id } = fromGlobalId(globalId); if (type === 'Person') { return fetchPersonByURL(`/people/${id}/`); } }, object => { if (object.hasOwnProperty('username')) { return 'Person'; } },);
const PersonType = new GraphQLObjectType({ name: 'Person', description: 'Somebody that you used to know', fields: () => ({ firstName: { type: GraphQLString, resolve: person => person.first_name, }, lastName: { type: GraphQLString, resolve: person => person.last_name, }, email: {type: GraphQLString}, id: globalIdField('Person'), username: {type: GraphQLString}, friends: { type: new GraphQLList(PersonType), resolve: person => person.friends.map(fetchPersonByURL), }, }), interfaces: [ nodeInterface ],});
const QueryType = new GraphQLObjectType({ name: 'Query', description: 'The root of all... queries', fields: () => ({ allPeople: { type: new GraphQLList(PersonType), resolve: fetchPeople, }, node: nodeField, person: { type: PersonType, args: { id: { type: GraphQLString }, }, resolve: (root, args) => fetchPersonByURL(`/people/${args.id}/`), }, }),});
export default new GraphQLSchema({ query: QueryType,});
考虑以下朋友的朋友的朋友查询
query { person(id: "1") { firstName friends { firstName friends { firstName friends { firstName } } } }}
我们上面创建的模式将为相同数据生成多个往返 REST API 的请求。
这显然是我们想要避免的!至少,我们需要一种方法来缓存这些请求的结果。
我们创建了一个名为 DataLoader 的库来帮助驯服这类查询。
npm install --save dataloader
特别注意,确保您的运行时提供 Promise
和 Map
的原生或填充版本。阅读更多 DataLoader 网站。
要创建 DataLoader
,您需要提供一个方法,该方法可以根据键列表解析对象列表。在我们的示例中,键是我们访问 REST API 的 URL。
const personLoader = new DataLoader(urls => Promise.all(urls.map(fetchPersonByURL)))
如果此数据加载器在其生命周期中多次看到一个键,它将返回响应的记忆(缓存)版本。
我们可以使用 personLoader
上的 load()
和 loadMany()
方法来加载 URL,而无需担心每次 URL 都会多次访问 REST API。以下是一个完整的示例
import DataLoader from 'dataloader';import { GraphQLList, GraphQLObjectType, GraphQLSchema, GraphQLString,} from 'graphql';import { fromGlobalId, globalIdField, nodeDefinitions,} from 'graphql-relay';
const BASE_URL = 'https://myapp.com/';
function fetchResponseByURL(relativeURL) { return fetch(`${BASE_URL}${relativeURL}`).then(res => res.json());}
function fetchPeople() { return fetchResponseByURL('/people/').then(json => json.people);}
function fetchPersonByURL(relativeURL) { return fetchResponseByURL(relativeURL).then(json => json.person);}
const personLoader = new DataLoader( urls => Promise.all(urls.map(fetchPersonByURL)));
const { nodeInterface, nodeField } = nodeDefinitions( globalId => { const {type, id} = fromGlobalId(globalId); if (type === 'Person') { return personLoader.load(`/people/${id}/`); } }, object => { if (object.hasOwnProperty('username')) { return 'Person'; } },);
const PersonType = new GraphQLObjectType({ name: 'Person', description: 'Somebody that you used to know', fields: () => ({ firstName: { type: GraphQLString, resolve: person => person.first_name, }, lastName: { type: GraphQLString, resolve: person => person.last_name, }, email: {type: GraphQLString}, id: globalIdField('Person'), username: {type: GraphQLString}, friends: { type: new GraphQLList(PersonType), resolve: person => personLoader.loadMany(person.friends), }, }), interfaces: [nodeInterface],});
const QueryType = new GraphQLObjectType({ name: 'Query', description: 'The root of all... queries', fields: () => ({ allPeople: { type: new GraphQLList(PersonType), resolve: fetchPeople, }, node: nodeField, person: { type: PersonType, args: { id: { type: GraphQLString }, }, resolve: (root, args) => personLoader.load(`/people/${args.id}/`), }, }),});
export default new GraphQLSchema({ query: QueryType,});
现在,我们的病态查询会生成以下对 REST API 的去重请求集
请注意,您的 REST API 可能已经提供配置选项,让您能够急切地加载关联。也许要加载一个人及其所有直接朋友,您可能需要访问 URL /people/1/?include_friends
。为了在您的 GraphQL 模式中利用这一点,您需要能够根据查询本身的结构(例如,friends
字段是否为查询的一部分)来开发一个解析计划。
对于那些对当前关于高级解析策略的思考感兴趣的人,请关注 拉取请求 #304。
我希望这个演示能够消除您与功能性 GraphQL 端点之间的某些障碍,并激励您在现有项目中尝试 GraphQL 和 Relay。