REST 链接
在您的 GraphQL 查询中调用 REST API。
概览
- 由于它是现有的一组 API,可能由第三方管理,因此您没有访问权限来修改后端。
使用
快速开始
要开始,首先安装 Apollo Client 和我们需要的任何 peerDependencies
:
npm install --save @apollo/client apollo-link-rest graphql qs
之后,您可以开始设置 Apollo Client 实例:
import { ApolloClient, InMemoryCache } from '@apollo/client';import { RestLink } from 'apollo-link-rest';// Set `RestLink` with your endpointconst restLink = new RestLink({ uri: "https://swapi.dev/api/" });// Setup your clientconst client = new ApolloClient({cache: new InMemoryCache(),link: restLink});
现在,我们可以写出第一个 查询:
import { gql } from '@apollo/client';const query = gql`query Luke {person @rest(type: "Person", path: "people/1/") {name}}`;
然后,您可以使用 Apollo Client 获取数据:
// Invoke the query and log the person's nameclient.query({ query }).then(response => {console.log(response.data.person.name);});
选项
restLink 构造函数接受一个选项对象,可用于自定义链接的行为。以下列出支持选项:
选项 | 类型 | 描述 |
---|---|---|
uri | 字符串 | URI 键是请求触发的字符串端点/域名(当 endpoints 提供默认值时,为 可选) |
endpoints: /map-of-endpoints/ | 任何 | 可选 端点映射。如果使用此选项,您需要向 @rest(...)指令提供端点。 |
customFetch? | 任何 | 可选 用于处理 REST 调用的自定义 fetch |
头部信息? | 头部 | 可选 一个对象,表示要将所有请求数据作为头部值发送。有关说明,请参阅此处 |
凭据? | 字符串 | 可选 表示fetch调用应使用的凭据政策的字符串。有关说明,请参阅此处 |
fieldNameNormalizer?: /function/ | 任何 | 可选 一个函数,它接受响应字段名称并将其转换为符合GraphQL的名称。这对于您的REST API返回不能表示为GraphQL的字段是有用的,或者如果您想将JSON中的蛇形命名字段转换为驼峰命名字段。 |
fieldNameDenormalizer?: /function/ | 任何 | 可选 一个函数,它接受符合GraphQL的字段名称并将其转换为特定的端点名称。 |
typePatcher: /map-of-functions/ | 任何 | 可选 一个结构,允许您指定REST响应中的嵌套对象时的`__typename`。 |
defaultSerializer /function/ | 任何 | 可选 一个函数,它将在没有为`@rest`调用定义`bodySerializer`时作为`RestLink`的默认序列化器使用。该函数还将传递当前的`Header`设置,可以在请求发送到`fetch`之前更新。默认方法使用`JSON.stringify`并将`Content-Type`设置为`application/json`。 |
bodySerializers: /map-of-functions/ | 任何 | 可选 结构,允许定义替代序列化器,可以通过它们的键指定。 |
responseTransformer?: /function/ | 任何 | 可选 当您的响应数据结构不同时,使用此函数将响应结构化成Apollo期望的格式。 |
多个端点
如果需要使用多个端点,可以创建您的链接如下
const link = new RestLink({ endpoints: { v1: 'api.com/v1', v2: 'api.com/v2' } });
然后需要指定要使用的端点,在rest指令中:
const postTitleQuery1 = gql`query PostTitle {post @rest(type: "Post", path: "/post", endpoint: "v1") {idtitle}}`;const postTitleQuery2 = gql`query PostTitle {post @rest(type: "[Tag]", path: "/tags", endpoint: "v2") {idtags}}`;
如果有默认端点,可以创建您的链接如下
const link = new RestLink({endpoints: { github: 'github.com' },uri: 'api.com',});
如果没有在query中指定端点,将使用默认端点(在uri选项中指定的端点)。
类型名修补
当发送如下query时:
query MyQuery {planets @rest(type: "PlanetPayload", path: "planets/") {countnextresults {name}}}
外部响应对象(data.planets
)从@rest(...)
指令的type
参数中获得__typename: "PlanetPayload"
。然而,您需要有一个方法来设置PlanetPayload.results
的类型名称。
您可以这样做的一种方式是提供一个typePatcher
:
const restLink = new RestLink({uri: '/api',typePatcher: {PlanetPayload: (data: any,outerType: string,patchDeeper: RestLink.FunctionalTypePatcher,): any => {if (data.results != null) {data.results =data.results.map(planet => ({ __typename: "Planet", ...planet }));}return data;},// ... other nested type patchers},})
如果您有一个非常轻量级的REST集成,可以使用@type(name: ...)
指令。
query MyQuery {planets @rest(type: "PlanetPayload", path: "planets/") {countnextresults @type(name: "Planet") {name}}}
如果您有一组嵌套对象,这种方法是合适的。这种策略的成本是,每个处理这些对象的查询都需要包含 @type(name: ...),这意味着这种方法可能是相当冗长和容易出错的。
您也可以同时使用这两种方法。
query MyQuery {planets @rest(type: "PlanetPayload", path: "planets/") {countnextresults @type(name: "Results") {name}typePatchedResults {name}}}
const restLink = new RestLink({uri: '/api',typePatcher: {PlanetPayload: (data: any,outerType: string,patchDeeper: RestLink.FunctionalTypePatcher,): any => {if (data.typePatchedResults != null) {data.typePatchedResults =data.typePatchedResults.map(planet => { __typename: "Planet", ...planet });}return data;},// ... other nested type patchers},})
警告
需要注意的是,目前typePatcher
无法针对注解了@type
的嵌套对象进行操作。例如,如果您在typePatcher
中定义了failingResults
,则它将不会被修补:
query MyQuery {planets @rest(type: "PlanetPayload", path: "planets/") {countnextresults @type(name: "Planet") {namefailingResults {name}}typePatchedResults {name}}}
为了使这正常工作,您应该尽可能选择一种策略,并坚持下去——要么全部使用typePatcher
,要么全部使用@type
指令。
响应转换
默认情况下,Apollo 预期记录请求根位置有一个对象,集合请求根位置有一个对象的数组。例如,根据 ID(/users/1
)获取用户时,期待以下响应。
{"id": 1,"name": "Apollo"}
当获取用户列表(/users
)时,期待以下响应。
[{"id": 1,"name": "Apollo"},{"id": 2,"name": "Starman"}]
如果您的 API 响应结构与 Apollo 预期的不同,您可以在客户端定义一个 responseTransformer
。此函数接收响应对象作为第一个 Promise
参数,并作为第二个 typeName
参数。它应该返回一个 Promise
,因为它将负责通过调用 json()
、text()
等来读取响应流。
例如,如果记录不在根级别
{"meta": {},"data": [{"id": 1,"name": "Apollo"},{"id": 2,"name": "Starman"}]}
可以使用以下转换器来支持它
const link = new RestLink({uri: '/api',responseTransformer: async response => response.json().then(({data}) => data),});
纯文本、XML 或其他编码的响应可以通过手动解析并将它们转换为 JSON(使用 Apollo 预期格式的 previously 描述的格式)来处理
const link = new RestLink({uri: '/xmlApi',responseTransformer: async response => response.text().then(text => parseXmlResponseToJson(text)),});
自定义端点响应
客户端级别的 responseTransformer
适用于所有响应,跨越所有 URI 和端点。如果您需要为特定端点定义自定义 responseTransformer
,可以为此特定端点定义一个选项对象。
const link = new RestLink({endpoints: {v1: {uri: '/v1',responseTransformer: async response => response.data,},v2: {uri: '/v2',responseTransformer: async (response, typeName) => response[typeName],},},});
使用对象形式时,需要 uri
字段。
自定义抓取
默认情况下,Apollo 使用浏览器 fetch
方法来处理对您的域/端点的 REST
请求。customFetch
选项允许您通过定义一个返回类似 Promise
的对象的处理函数来指定 您自己的 请求处理器:
const link = new RestLink({endpoints: "/api",customFetch: (uri, options) => new Promise((resolve, reject) => {// Your own (asynchronous) request handlerresolve(responseObject)}),});
为了快速解决您的 GraphQL 查询,Apollo 会在尽可能早的时候向相关端点发出请求。这通常是可行的,但仍可能导致一次触发大量 REST
请求;特别是对于深层嵌套的查询 (见 @export
指令)。
某些端点(如公开 API)可能实施 速率限制,从而导致失败响应和未解决的查询。
例如,customFetch
是管理您应用程序的 fetch 操作的好地方。 以下实现确保每次只发出 2 个请求(并发),直到下一批请求开始至少等待 500ms。
import pThrottle from "p-throttle";const link = new RestLink({endpoints: "/api",customFetch: pThrottle((uri, config) => {return fetch(uri, config);},2, // Max. concurrent Requests500 // Min. delay between calls),});
完整选项
以下是自定义 RestLink
的一种方法:
import fetch from 'cross-fetch';import * as camelCase from 'camelcase';import * as snake_case from 'snake-case';const link = new RestLink({endpoints: { github: 'github.com' },uri: 'api.com',customFetch: fetch,headers: {"Content-Type": "application/json"},credentials: "same-origin",fieldNameNormalizer: (key: string) => camelCase(key),fieldNameDenormalizer: (key: string) => snake_case(key),typePatcher: {Post: ()=> {bodySnippet...}},defaultSerializer: (data: any, headers: Headers) => {const formData = new FormData();for (let key in data) {formData.append(key, data[key]);}headers.set("Content-Type", "x-www-form-encoded")return {data: formData, headers};}});
链接上下文
RestLink
有一个 接口 LinkChainContext
,它用作它将查找的 context
结构。它将决定如何满足特定的 RestLink
请求。(请参阅 @apollo/client/link/context
页面了解为什么您可能需要此内容)。
选项 | 类型 | 描述 |
---|---|---|
凭据? | 请求凭据 | 覆盖了 RestLink 级别的 credentials 设置。请在此处查看记录的值。 |
头部信息? | 头部 | 在此 context-link 中提供的附加头信息。此处记录的值 |
headersToOverride? | 字符串数组 | 如果您提供此数组,我们将通过替换根 RestLink 配置中存在的任何匹配头,将您在此链接中提供的头合并。或者,您可以使用 headersMergePolicy 来对合并行为进行更细致的定制。 |
headersMergePolicy? | RestLink.HeadersMergePolicy | 这是一个函数,用于决定本 contextLink 返回的头如何与 RestLink 级别上定义的头合并。如果您不提供此函数,将简单地将头追加。要使用此选项,您可以提供自己的函数来决定如何处理头。 代码参考 |
restResponses? | Response[] | 操作完成后将填充此内容,其中包含在操作期间获取的每个 REST url 的响应。如果您需要访问响应头以获取授权令牌,这很有用。 |
示例
RestLink
使用 的headers
字段,在@apollo/client/link/context
上,您可以组合其他提供附加和动态头的查询。
以下是将请求headers
添加到上下文并检索操作响应头的一种方法:
const authRestLink = new ApolloLink((operation, forward) => {operation.setContext(({headers}) => {const token = localStorage.getItem("token");return {headers: {...headers,Accept: "application/json",Authorization: token}};});return forward(operation).map(result => {const { restResponses } = operation.getContext();const authTokenResponse = restResponses.find(res => res.headers.has("Authorization"));// You might also filter on res.url to find the response of a specific API callif (authTokenResponse) {localStorage.setItem("token", authTokenResponse.headers.get("Authorization"));}return result;});});const restLink = new RestLink({ uri: "uri" });const client = new ApolloClient({cache: new InMemoryCache(),link: ApolloLink.from([authRestLink, restLink])});
链式顺序
如果您正在使用多种链式类型,restLink
应该位于httpLink
之前,因为httpLink
将吞噬应通过apollo-link-rest
路由的任何调用。
例如
const httpLink = createHttpLink({ uri: "server.com/graphql" });const restLink = new RestLink({ uri: "api.server.com" });const client = new ApolloClient({cache: new InMemoryCache(),link: ApolloLink.from([authLink, restLink, errorLink, retryLink, httpLink])// Note: httpLink is terminating so must be last, while retry & error wrap// the links to their right. State & context links should happen before (to// the left of) restLink.});
注意:如果您使用 @apollo/client/link/context
来设置 Headers
,则还需要在该链接之前设置该链接。
@rest 指令
这是您设置要获取的端点的位置。rest 指令可以在查询的任何深度中使用。
参数
一个@rest(…)
指令接受两个必需参数以及若干个可选参数:
选项 | 类型 | 描述 |
---|---|---|
type | 字符串 | 此指令将返回的 GraphQL 类型 |
path | 字符串 | REST API的uri路径。这可能是一条路径或一个完整的URL。如果是路径,则将链接创建时或从上下文提供的端点与之连接,以生成完整的URI 。另请参阅:pathBuilder |
方法? | GET PUT POST DELETE | 发送请求的HTTP方法(即 GET、PUT、POST) |
端点? | 字符串 | 在创建RestLink时,如果提供了端点,则用于在(可选的)endpoints 表中查找端点的键。 |
pathBuilder?: /function/ | 字符串 | 如果提供,则此函数可以控制为该请求创建的路径。 |
bodyKey?: "input" | 字符串 | 这是在构建PUT 或POST 请求的REST请求体时使用的变量 的名称。如果未提供,则默认为 |
bodyBuilder?: /function/ | 字符串 | 如果提供,这是调用一个函数的名称,您将其提供给variables ,当需要构建请求体时,该函数会被调用。这允许您组合参数或在除了JSON之外的其他格式中编码体。 |
bodySerializer?: /字符串 | 函数/ | 字符串 | 在将请求体/头传递给fetch调用之前,查找bodySerializers 中的函数或为该请求的请求体/头提供的自定义序列化函数的字符串键。默认为JSON.stringify 和设置Content-Type: application-json 。 |
变量
您可以在嵌套查询内部或您的指令的路径参数中使用查询变量
query PostTitle {post(id: "1") @rest(type: "Post", path: "/post/{args.id}") {idtitle}}
警告:变量在主路径上不会自动进行encodeURIComponent
调用。
此外,您还可以控制查询字符串:
query PostTitle {postSearch(query: "some key words", page_size: 5)@rest(type: "Post", path: "/search?{args}&{context.language}") {idtitle}}
注意事项
- 这将转换为
/search?query=some%20key%20words&page_size=5&lang=en
- query
context.language / lang=en
是从通过@apollo/client/link/context
链接添加的Apollo上下文中提取的对象。 - 查询字符串参数由npm:qs组装,并对它们进行了
encodeURIComponent
调用。
可用的变量来源包括:
选项 | 描述 |
---|---|
args | 这些是直接传递给此字段参数的东西。在上面的例子中,postSearch 中包含了query 和page_size 在参数中。 |
exportVariables | 这些是在父上下文中标记为@export(as: ...) 的东西。 |
context | 这些是apollo-context,因此您可以通过@apollo/client/link/context 设置全局变量。 |
@rest | 这些包括您传递给@rest() 指令的任何其他参数。这在使用pathBuilder 时可能更有用,如下文所述。 |
pathBuilder
如果上述变量替换选项不足以使用,您可以为您的查询提供pathBuilder
。这将用于动态构建路径。这是一个高级功能,已在源代码中进行了文档化--它还应该被认为是语法不稳定的,并且我们正在寻找反馈!
bodyKey
/ bodyBuilder
在进行 POST
或 PUT
HTTP 请求时,通常需要提供请求体。按照 约定,GraphQL 建议您将输入类型命名为 input
,因此默认情况下我们将在这里查找您的请求体的 JSON 对象。
bodyKey
如果您需要/想要为它命名不同,则可以传递 bodyKey
,我们将查看该变量而不是。
在这个例子中,publish API 接受变量 body
作为请求体,而不是作为输入:
mutation PublishPost($someApiWithACustomBodyKey: PublishablePostInput!) {publishedPost: publish(input: "Foo", body: $someApiWithACustomBodyKey)@rest(type: "Post"path: "/posts/{args.input}/new"method: "POST"bodyKey: "body") {idtitle}}
bodyBuilder
如果您需要以不同的方式结构化您的数据,或者需要自定义编码您的请求体(例如,作为表单编码),您可以提供 bodyBuilder
作为替代:
mutation EncryptedPost($input: PublishablePostInput!$encryptor: any) {publishedPost: publish(input: $input)@rest(type: "Post"path: "/posts/new"method: "POST"bodyBuilder: $encryptor) {idtitle}}
bodySerializer
如果您需要以不同的方式序列化您的数据(例如,作为表单编码),而不是依赖于默认的 JSON 序列化,您提供一个 bodySerializer
。 bodySerializer
可以是形式
的函数 (data: any, headers: Headers) => {body: any, header: Headers}
或字符串键。当使用字符串键时,RestLink
将使用初始化期间可以传入的
bodySerializers对象中的相应序列化器。
mutation EncryptedForm($input: PublishablePostInput!,$formSerializer: any) {publishedPost: publish(input: $input)@rest(type: "Post",path: "/posts/new",method: "POST",bodySerializer: $formSerializer) {idtitle}publishRSS(input: $input)@rest(type: "Post",path: "/feed",method: "POST",bodySerializer: "xml")}
例如,formSerializer
可以定义如下
const formSerializer = (data: any, headers: Headers) => {const formData = new FormData();for (let key in data) {if (data.hasOwnProperty(key)) {formData.append(key, data[key]);}}headers.set('Content-Type', 'application/x-www-form-urlencoded');return {body: formData, headers};}
而"xml"
将在 RestLink
中直接定义。
const restLink = new RestLink({...otherOptions,bodySerializers: {xml: xmlSerializer}})
@export 指令
export 指令重新导出一个 field以供后来(嵌套) query使用。这部分与服务器上支持的语义相同,但在 RestLink
中使用时,您可以使用导出的 variables进行进一步调用(例如,从嵌套 fields的水龙头请求)。
注意:如果您经常使用 @export,您可能更喜欢查看 @apollo/server
。
参数
as: string
: 创建此作为 variable的名称,以便用于以下 selection set
示例
一个用例示例是获取用户列表,然后访问不同端点以使用在 REST 查询参数中导出的 field获取更多数据。
const QUERY = gql`query RestData($email: String!) {users @rest(path: '/users/email?{args.email}', method: 'GET', type: 'User') {id @export(as: "id")firstNamelastNamefriends @rest(path: '/friends/{exportVariables.id}', type: '[User]') {firstNamelastName}}}`;
突变
你还可以使用 apollo-link-rest 编写 变异,例如:
mutation DeletePost($id: ID!) {deletePostResponse(id: $id)@rest(type: "Post", path: "/posts/{args.id}", method: "DELETE") {NoResponse}}
故障排除
这里是几个常见的 apollo-link-rest
问题及解决方案。
缺少字段 __typename in ...
-- 如果你看到这个,可能是你没有提供type:
给@rest(...)
指令。另外,你需要设置一个typePatcher
。Headers is undefined
-- 如果你看到类似的信息,表明你正在浏览或不支持完整Headers
API 的其他 JavaScript 环境。
示例应用
为了帮助你开始,这里有一些示例应用
贡献
请在github上加入我们 apollographql/apollo-link-rest,并在 Apollo GraphQL 社区论坛中。