从 REST 获取
使用RESTDataSource从REST API获取数据
参见@apollo/datasource-rest
README获得关于RESTDataSource
API的完整信息。
类RESTDataSource
简化了从REST API获取数据的过程,并有助于处理缓存、请求去重和错误,同时解决操作。
有关从除了REST API之外的数据源获取更多信息的说明,请参见获取数据。
创建子类
要开始使用,请安装@apollo/datasource-rest
包:
npm install @apollo/datasource-rest
您的服务器应为与每个REST API通信的RESTDataSource定义一个单独的子类。以下是一个RESTDataSource子类的示例,它定义了两个数据检索方法,getMovie和getMostViewedMovies:RESTDataSource
:
import { RESTDataSource } from '@apollo/datasource-rest';class MoviesAPI extends RESTDataSource {override baseURL = 'https://movies-api.example.com/';async getMovie(id: string): Promise<Movie> {return this.get<Movie>(`movies/${encodeURIComponent(id)}`);}async getMostViewedMovies(limit = '10'): Promise<Movie[]> {const data = await this.get('movies', {params: {per_page: limit.toString(), // all params entries should be strings,order_by: 'most_viewed',},});return data.results;}}
import { RESTDataSource } from '@apollo/datasource-rest';class MoviesAPI extends RESTDataSource {baseURL = 'https://movies-api.example.com/';async getMovie(id) {return this.get(`movies/${encodeURIComponent(id)}`);}async getMostViewedMovies(limit = '10') {const data = await this.get('movies', {params: {per_page: limit.toString(), // all params entries should be strings,order_by: 'most_viewed',},});return data.results;}}
您可以通过扩展RESTDataSource类来实施所需的数据检索方法。这些方法应使用内置的便捷方法(例如 get 和 post)进行HTTP请求,帮助您添加查询参数,解析和缓存JSON结果,去重请求和处理错误。更复杂的场景可以直接使用 fetch 方法。fetch 方法返回解析后的主体和响应对象,为读取响应头等使用场景提供了更多灵活性。
向服务器上下文函数添加数据源
您可以将数据源添加到上下文
初始化函数中,如下所示:
interface ContextValue {dataSources: {moviesAPI: MoviesAPI;personalizationAPI: PersonalizationAPI;};}const server = new ApolloServer<ContextValue>({typeDefs,resolvers,});const { url } = await startStandaloneServer(server, {context: async () => {const { cache } = server;return {// We create new instances of our data sources with each request,// passing in our server's cache.dataSources: {moviesAPI: new MoviesAPI({ cache }),personalizationAPI: new PersonalizationAPI({ cache }),},};},});console.log(`🚀 Server ready at ${url}`);
const server = new ApolloServer({typeDefs,resolvers,});const { url } = await startStandaloneServer(server, {context: async () => {const { cache } = server;return {// We create new instances of our data sources with each request,// passing in our server's cache.dataSources: {moviesAPI: new MoviesAPI({ cache }),personalizationAPI: new PersonalizationAPI({ cache }),},};},});console.log(`🚀 Server ready at ${url}`);
Apollo 服务器将调用上下文
初始化函数用于每个传入操作。这意味着:
- 对于每个操作,
上下文
返回一个对象,其中包含您RESTDataSource
子类的新实例(在本例中为
MoviesAPI
和PersonalizationAPI
)。 - 每个
上下文
函数应该为每个操作创建每个RESTDataSource
子类的一个新实例。有关更多信息,请参见以下内容。
您的解析器可以从共享的上下文值
对象中访问您的数据源,并使用它们来获取数据:
const resolvers = {Query: {movie: async (_, { id }, { dataSources }) => {return dataSources.moviesAPI.getMovie(id);},mostViewedMovies: async (_, __, { dataSources }) => {return dataSources.moviesAPI.getMostViewedMovies();},favorites: async (_, __, { dataSources }) => {return dataSources.personalizationAPI.getFavorites();},},};
缓存
该RESTDataSource
类为它的子类提供了两层的缓存:
- 第一层按默认方式去重并发发出的
GET
(和HEAD
)请求。去重键是请求的方法和URL。您可以通过覆盖requestDeduplicationPolicyFor
方法来配置此行为。有关更多详情,请参阅README。
注意:在版本低于RESTDataSource
5之前的版本中,所有发出的GET
请求都会被去重。您可以使用deduplicate-until-invalidated
策略(在README中进一步解释)来实现相同的行为。
- 第二层缓存指定了HTTP缓存头的HTTP响应结果。
这些缓存层使RESTDataSource
类成为一个提供浏览器样式缓存的Node HTTP客户端。以下,我们将深入了解每一层的缓存及其提供的优势。
GET
(和HEAD
)请求和响应
query GetPosts {posts {bodyauthor {name}}}
第一次RESTDataSource
发起GET
请求(例如/authors/id_1
),它会先将请求的URL存储在发送请求之前。接着RESTDataSource
执行请求,并将结果永远存储在与其请求URL相关的缓存中。
如果当前操作中的任何解析器尝试对相同URL进行并行GET
请求,RESTDataSource
将检查其缓存在执行此请求之前。如果缓存中存在请求或结果,RESTDataSource
会返回(或等待返回)存储的结果而不执行另一个请求。
这种内部缓存机制是我们为每个请求创建新的RESTDataSource
实例的原因。否则,即使请求指定它们不应被缓存,响应也会在请求之间进行缓存!
您可以通过覆盖RESTDataSource的cacheKeyFor
方法来更改GET
(和HEAD
)请求在RESTDataSource
的去除重复缓存中的存储方式。
要从RESTDataSource
v5之前恢复去重策略,您可以按以下方式配置requestDeduplicationPolicyFor
:
class MoviesAPI extends RESTDataSource {override baseURL = 'https://movies-api.example.com/';private token: string;constructor(options: { token: string; cache: KeyValueCache }) {super(options); // this sends our server's `cache` throughthis.token = options.token;}protected override requestDeduplicationPolicyFor(url: URL, request: RequestOptions) {const cacheKey = this.cacheKeyFor(url, request);return {policy: 'deduplicate-until-invalidated',deduplicationKey: `${request.method ?? 'GET'} ${cacheKey}`,};}// Duplicate requests are cached indefinitelyasync getMovie(id) {return this.get(`movies/${encodeURIComponent(id)}`);}}
class MoviesAPI extends RESTDataSource {baseURL = 'https://movies-api.example.com/';constructor(options) {super(options); // this sends our server's `cache` throughthis.token = options.token;}requestDeduplicationPolicyFor(url, request) {const cacheKey = this.cacheKeyFor(url, request);return {policy: 'deduplicate-until-invalidated',deduplicationKey: `${request.method ?? 'GET'} ${cacheKey}`,};}// Duplicate requests are cached indefinitelyasync getMovie(id) {return this.get(`movies/${encodeURIComponent(id)}`);}}
要完全禁用请求去重,您可以按以下方式配置requestDeduplicationPolicyFor
:
class MoviesAPI extends RESTDataSource {override baseURL = 'https://movies-api.example.com/';private token: string;constructor(options: { token: string; cache: KeyValueCache }) {super(options); // this sends our server's `cache` throughthis.token = options.token;}protected override requestDeduplicationPolicyFor(url: URL, request: RequestOptions) {const cacheKey = this.cacheKeyFor(url, request);return { policy: 'do-not-deduplicate' } as const;}// Outgoing requests aren't cached, but the HTTP response cache still works!async getMovie(id) {return this.get(`movies/${encodeURIComponent(id)}`);}}
class MoviesAPI extends RESTDataSource {baseURL = 'https://movies-api.example.com/';constructor(options) {super(options); // this sends our server's `cache` throughthis.token = options.token;}requestDeduplicationPolicyFor(url, request) {const cacheKey = this.cacheKeyFor(url, request);return { policy: 'do-not-deduplicate' };}// Outgoing requests aren't cached, but the HTTP response cache still works!async getMovie(id) {return this.get(`movies/${encodeURIComponent(id)}`);}}
指定缓存TTL
📣 新功能!Apollo Server 4中:Apollo Server不再自动为其数据源提供缓存。了解更多详细信息。
如果以下任一条件为真,RESTDataSource
类可以缓存从REST API获取的结果:
- 请求方法为
GET
(或HEAD
),并且响应指定了缓存头(例如,cache-control
)。 - RESTDataSource
实例的
缓存选项指定一个TTL。- 您可以这样做,通过覆盖方法,或者在发起请求的HTTP方法中。
RESTDataSource
确保缓存的信息遵守由这些缓存头指定的TTL(生命周期)规则。
每个RESTDataSource
子类接受一个缓存
参数,您可以在其中指定要使用的缓存(例如:Apollo Server的默认缓存)以存储过去的fetch结果:
// KeyValueCache is the type of Apollo server's default cacheimport type { KeyValueCache } from '@apollo/utils.keyvaluecache';class PersonalizationAPI extends RESTDataSource {override baseURL = 'https://person.example.com/';private token: string;constructor(options: { cache: KeyValueCache; token: string }) {super(options); // this sends our server's `cache` throughthis.token = options.token;}}// server set up, etc.const { url } = await startStandaloneServer(server, {context: async ({ req }) => {const token = getTokenFromRequest(req);// We'll take Apollo Server's cache// and pass it to each of our data sourcesconst { cache } = server;return {dataSources: {moviesAPI: new MoviesAPI({ cache, token }),personalizationAPI: new PersonalizationAPI({ cache }),},};},});
// KeyValueCache is the type of Apollo server's default cacheclass PersonalizationAPI extends RESTDataSource {baseURL = 'https://person.example.com/';constructor(options) {super(options); // this sends our server's `cache` throughthis.token = options.token;}}// server set up, etc.const { url } = await startStandaloneServer(server, {context: async ({ req }) => {const token = getTokenFromRequest(req);// We'll take Apollo Server's cache// and pass it to each of our data sourcesconst { cache } = server;return {dataSources: {moviesAPI: new MoviesAPI({ cache, token }),personalizationAPI: new PersonalizationAPI({ cache }),},};},});export {};
将相同的缓存
传递给多个RESTDataSource
子类实例,启用这些实例可共享缓存结果。
当运行您的服务器多个实例时,您应该使用一个外部共享缓存后端。这样,一个服务器实例可以使用来自另一个实例的缓存结果。
如果您想配置或替换Apollo Server的默认缓存,请参见配置外部缓存以获取更多详情。
HTTP方法
RESTDataSource
包括常用REST API请求方法的便捷方法:get
、post
、put
、patch
和delete
(详情见源代码)。
下面是每个方法的示例
注意上述代码中 encodeURIComponent
的使用。这是一个标准的函数,用于在 URI 中对特殊字符进行编码,从而防止可能的注入攻击向量。
以下是一个简单的例子,假设我们的 REST 端点 responded 到以下 URL
- DELETE
/movies/:id
- DELETE
/movies/:id/characters
一个 "恶意" 的客户端可以提供一个 :id
为 1/characters
的值,以针对 characters
删除端点(当我们试图删除单个 movie
端点)。URI 编码通过将 /
转换为 %2F
来防止这种注入。这样,它可以由服务器正确解码和理解,不会被视为路径段。
方法参数
对于所有 HTTP 方便方法,第一个参数是要发送请求的端点的相对路径(例如,movies
)。第二个参数是一个对象,您可以在其中设置请求的 headers
、params
、cacheOptions
和 body
:
class MoviesAPI extends RESTDataSource {override baseURL = 'https://movies-api.example.com/';// an example making an HTTP POST requestasync postMovie(movie) {return this.post(`movies`, // path{ body: movie }, // request body);}}
设置 fetch 选项
每个 REST 方法传入的第二个参数是一个包含请求选项的对象。这些选项包括通常传递给 fetch
的选项,包括 method
、headers
、body
和 signal
。
如果您正在寻找 Apollo 文档中没有涉及的其它高级选项,您可以将它们设置在这里并参考您的 Fetch API 文档。
this.get('/movies/1', options);
设置超时
要设置 fetch
超时,请通过 AbortSignal
选项提供一个 AbortSignal
,这允许您使用自定义逻辑终止请求。
以下是一个示例,显示每个请求后的简单超时设置
this.get('/movies/1', { signal: AbortSignal.timeout(myTimeoutMilliseconds) });
拦截 fetch 请求
Apollo Server 4 的新特性: Apollo Server 4 现在底层使用 @apollo/utils.fetcher
界面来进行获取。此界面允许您选择自己的 Fetch API 实现。为了确保与所有 Fetch 实现兼容,提供给钩子(如 willSendRequest
)的请求是一个普通的 JS 对象,不是一个具有方法的 Request
对象。
RESTDataSource
包含一个 willSendRequest
方法,您可以使用该方法覆盖以修改在发送之前的外出请求。例如,您可以使用此方法添加头信息或查询参数。此方法通常用于授权或适用于所有发送请求的其他关注点。
数据源 还可以访问 GraphQL 操作上下文,这对于存储用户令牌或其他相关信息非常有用。
如果您正在使用 TypeScript,请确保导入 AugmentedRequest
类型。
设置头信息
import { RESTDataSource, AugmentedRequest } from '@apollo/datasource-rest';import type { KeyValueCache } from '@apollo/utils.keyvaluecache';class PersonalizationAPI extends RESTDataSource {override baseURL = 'https://movies-api.example.com/';private token: string;constructor(options: { token: string; cache: KeyValueCache }) {super(options);this.token = options.token;}override willSendRequest(_path: string, request: AugmentedRequest) {request.headers['authorization'] = this.token;}}
import { RESTDataSource } from '@apollo/datasource-rest';class PersonalizationAPI extends RESTDataSource {baseURL = 'https://movies-api.example.com/';constructor(options) {super(options);this.token = options.token;}willSendRequest(_path, request) {request.headers['authorization'] = this.token;}}
添加查询参数
import { RESTDataSource, AugmentedRequest } from '@apollo/datasource-rest';import type { KeyValueCache } from '@apollo/utils.keyvaluecache';class PersonalizationAPI extends RESTDataSource {override baseURL = 'https://movies-api.example.com/';private token: string;constructor(options: { token: string; cache: KeyValueCache }) {super(options);this.token = options.token;}override willSendRequest(_path: string, request: AugmentedRequest) {request.params.set('api_key', this.token);}}
import { RESTDataSource } from '@apollo/datasource-rest';class PersonalizationAPI extends RESTDataSource {baseURL = 'https://movies-api.example.com/';constructor(options) {super(options);this.token = options.token;}willSendRequest(_path, request) {request.params.set('api_key', this.token);}}
动态解析 URL
在某些情况下,您可能希望根据环境或其他上下文值设置 URL。为此,您可以使用 resolveURL
进行覆盖:
import { RESTDataSource, AugmentedRequest } from '@apollo/datasource-rest';import type { KeyValueCache } from '@apollo/utils.keyvaluecache';class PersonalizationAPI extends RESTDataSource {private token: string;constructor(options: { token: string; cache: KeyValueCache }) {super(options);this.token = options.token;}override async resolveURL(path: string, request: AugmentedRequest) {if (!this.baseURL) {const addresses = await resolveSrv(path.split('/')[1] + '.service.consul');this.baseURL = addresses[0];}return super.resolveURL(path, request);}}
import { RESTDataSource } from '@apollo/datasource-rest';class PersonalizationAPI extends RESTDataSource {constructor(options) {super(options);this.token = options.token;}async resolveURL(path, request) {if (!this.baseURL) {const addresses = await resolveSrv(path.split('/')[1] + '.service.consul');this.baseURL = addresses[0];}return super.resolveURL(path, request);}}
与 DataLoader 一起使用
DataLoader 工具的 DataLoader 被设计用于特定的用例:从数据存储中去重和分批加载对象。它提供了缓存记忆功能,可以在单个 GraphQL 请求期间避免加载相同的对象多次。它还合并了在单个事件循环周期中发生加载,形成一个同时获取多个对象的批处理请求。
DataLoader 对于其预期用途非常有用,但在从 REST API 加载数据时不太有帮助。这是因为其主要功能是 批处理,而不是 缓存。
在 GraphQL 上层 REST API 时,最具帮助的是具有以下功能的资源缓存:
- 跨多个 GraphQL 请求保存数据
- 可以跨多个 GraphQL 服务器 共享
- 提供使用标准 HTTP 缓存控制头进行有效期和失效管理的缓存管理功能
使用 REST API 进行批处理
大多数 REST API 不支持批处理。当它们支持时,使用批处理端点可能会 危害 缓存。当您在批处理请求中获取数据时,收到的响应是对您请求的资源组合的精确响应。除非您再次请求相同的组合,否则未来的同资源请求不会从缓存中提供。
我们建议将批处理限制为不能缓存请求。在这些情况下,您可以在 RESTDataSource
内部作为私有实现细节使用 DataLoader:
import DataLoader from 'dataloader';import {RESTDataSource,AugmentedRequest,} from '@apollo/datasource-rest';import type { KeyValueCache } from '@apollo/utils.keyvaluecache';class PersonalizationAPI extends RESTDataSource {override baseURL = 'https://movies-api.example.com/';private token: string;constructor(options: { token: string; cache: KeyValueCache }) {super(options); // this should send our server's `cache` throughthis.token = options.token;}override willSendRequest(_path: string, request: AugmentedRequest) {request.headers['authorization'] = this.token;}private progressLoader = new DataLoader(async (ids) => {const progressList = await this.get('progress', {params: { ids: ids.join(',') },});return ids.map((id) => progressList.find((progress) => progress.id === id));});async getProgressFor(id) {return this.progressLoader.load(id);}}