Thinking in GraphQL #Facebook Relay文档翻译#

原文地址:Thinking in GraphQL
上一篇 Tutorial
Relay文档翻译目录

GraphQL presents new ways for clients to fetch data by focusing on the needs of product developers and client applications. It provides a way for developers to specify the precise data needed for a view and enables a client to fetch that data in a single network request. Compared to traditional approaches such as REST, GraphQL helps applications to fetch data more efficiently (compared to resource-oriented REST approaches) and avoid duplication of server logic (which can occur with custom endpoints). Furthermore, GraphQL helps developers to decouple product code and server logic. For example, a product can fetch more or less information without requiring a change to every relevant server endpoint. It’s a great way to fetch data.

Graph为客户端获取数据提供了新的方式,可以让焦点更集中在产品开发人员和客户端程序的需求上。它可以让开发者明确的指出一个视图需要哪些数据,可以让客户端获取数据在一次网络请求中完成。与传统的REST方案(面向资源的REST方案)相比,GraphQL可以让程序获取数据更高效,减少客户端重复服务器端逻辑。更进一步的,GraphQL帮助开发者将产品代码与服务器端逻辑分开。例如,一款产品可以指定获取更多或更少的数据,而无需在服务器端做相应的改动。总之它是获取数据的好方法。

In this article we’ll explore what it means to build a GraphQL client framework and how this compares to clients for more traditional REST systems. Along the way we’ll look at the design decisions behind Relay and see that it’s not just a GraphQL client but also a framework for declarative data-fetching. Let’s start at the beginning and fetch some data!

在这篇文章中,我们将阐释构建一个GraphQL客户端框架意味什么,它与传统的REST系统有什么不同。沿着这个思路我们将介绍一下Relay背后的设计思路,它不仅仅是一个GraphQL的客户端,而且还是一个“声明式的数据获取”框架。让我们从获取数据开始介绍。

Fetching Data 获取数据

Imagine we have a simple application that fetches a list of stories, and some details about each one. Here’s how that might look in resource-oriented REST:

想象我们有一个简单的应用程序获取一个故事列表和它们每一个的详情信息。在面向资源的REST方案中可能是如下形式:

// Fetch the list of story IDs but not their details:
// 获取故事ID的列表,不包含详情
rest.get('/stories').then(stories =>
  // This resolves to a list of items with linked resources:
  // 解析这个列表获取相应资源
  // `[ { href: "http://.../story/1" }, ... ]`
  Promise.all(stories.map(story =>
    rest.get(story.href) // Follow the links
  ))
).then(stories => {
  // This resolves to a list of story items:
  // `[ { id: "...", text: "..." } ]`
  console.log(stories);
});

Note that this approach requires n+1 requests to the server: 1 to fetch the list, and n to fetch each item. With GraphQL we can fetch the same data in a single network request to the server (without creating a custom endpoint that we’d then have to maintain):

我们看到这个方案向服务器进行了”n+1”次请求,1次用于获取列表,n次用于获取列表中的每一项。而使用GraphQL获取同样的数据我们只需向服务器发送一次请求,并且不需要为此在服务器端编写额外的代码。

graphql.get(`query { stories { id, text } }`).then(
  stories => {
    // A list of story items:
    // `[ { id: "...", text: "..." } ]`
    console.log(stories);
  }
);

So far we’re just using GraphQL as a more efficient version of typical REST approaches. Note two important benefits in the GraphQL version:
目前为止我们看到把GraphQL当作一种更高效的经典REST方案使用的情况。这个GraphQL版本有两个重要的好处:

  • All data is fetched in a single round trip.
  • 所有数据在一次网络请求中完成
  • The client and server are decoupled: the client specifies the data needed instead of relying on the server endpoint to return the correct data.
  • 客户端和服务器端解耦开,客户端只需要声明它需要什么数据,而不是依赖于服务器端返回特定的正确数据。

For a simple application that’s already a nice improvement.
从这个小小的例子上我们已经看到了巨大的进步。

Client Caching 客户端缓存

Repeatedly refetching information from the server can get quite slow. For example, navigating from the list of stories, to a list item, and back to the list of stories means we have to refetch the whole list. We’ll solve this with the standard solution: caching.
不断重复的从服务器端获取数据会影响性能。例如,从故事列表点击进入详情,再返回故事列表,这个过程意味着我们需要再获取一次整个list。所以我们引入缓存来解决这个问题。

In a resource-oriented REST system, we can maintain a response cache based on URIs:
在面向资源的REST系统中,我们可以根据URIs维护一个响应缓存

var _cache = new Map();
rest.get = uri => {
  if (!_cache.has(uri)) {
    _cache.set(uri, fetch(uri));
  }
  return _cache.get(uri);
};

Response-caching can also be applied to GraphQL. A basic approach would work similarly to the REST version. The text of the query itself can be used as a cache key:
响应返回的方案同样可以适用于GraphQL。一个基本的方案如下,与REST的版本很像。我们使用的查询文本可以作为缓存的key。

var _cache = new Map();
graphql.get = queryText => {
  if (!_cache.has(queryText)) {
    _cache.set(queryText, fetchGraphQL(queryText));
  }
  return _cache.get(queryText);
};

Now, requests for previously cached data can be answered immediately without making a network request. This is a practical approach to improving the perceived performance of an application. However, this method of caching can cause problems with data consistency.
现在,请求之前访问过的数据可以不通过网络请求迅速的得到响应。这是一个实用的方案,可以显著的看到提升应用性能。尽管如此,该方案还会带来数据不一致的问题。

Cache Consistency 缓存一致性

With GraphQL it is very common for the results of multiple queries to overlap. However, our response cache from the previous section doesn’t account for this overlap — it caches based on distinct queries. For example, if we issue a query to fetch stories:
使用GraphQL时会经常遇到不同查询的结果有重叠现象即有交集。但是,我们上面响应缓存的例子并不能应对重叠的情况,它是根据不同查询的字符为key做缓存的。例如,如果我们用如下查询获取故事集合:

query { stories { id, text, likeCount } }

and then later refetch one of the stories whose likeCount has since been incremented:
之后再写一个查询重新获取其中一条故事,恰巧它likeCount增加了。

query { story(id: "123") { id, text, likeCount } }

We’ll now see different likeCounts depending on how the story is accessed. A view that uses the first query will see an outdated count, while a view using the second query will see the updated count.
那么通过不同方式访问该故事的时候,我们会看到不同的likeCount。我们之前用的第一个查询将得到过期的数据,第二个查询得到更新过的数据。

Caching A Graph 缓存一个图

The solution to caching GraphQL is to normalize the hierarchical response into a flat collection of records. Relay implements this cache as a map from IDs to records. Each record is a map from field names to field values. Records may also link to other records (allowing it to describe a cyclic graph), and these links are stored as a special value type that references back into the top-level map. With this approach each server record is stored once regardless of how it is fetched.
我们缓存GraphQL的方案是使原本层级结构的返回数据,压扁到一个records集合中。Relay将缓存用map实现,用record的ID为key。每一个record都是名字到值的一个map。Records不仅可以名字映射到值,还可以映射到其他record。(允许描述一个环状图),这些映射被存储为特殊的类型,持有该map顶层的引用。用这样的方案,每条从服务器来的record只被存储一次,不论它是如何获得的。

Here’s an example query that fetches a story’s text and its author’s name:
例如查询id为1的故事内容和作者姓名:

query {
  story(id: "1") {
    text,
    author {
      name
    }
  }
}

And here’s a possible response:
可能得到的返回结果:

query: {
  story: {
     text: "Relay is open-source!",
     author: {
       name: "Jan"
     }
  }
}

Although the response is hierarchical, we’ll cache it by flattening all the records. Here is an example of how Relay would cache this query response:
虽然返回结果是层次结构的,我们将把它拍扁后缓存起来。下面是Relay会如何缓存该查询结果:

Map {
  // `story(id: "1")`
  1: Map {
    text: 'Relay is open-source!',
    author: Link(2),
  },
  // `story.author`
  2: Map {
    name: 'Jan',
  },
};

This is only a simple example: in reality the cache must handle one-to-many associations and pagination (among other things).
这仅是一个简单的例子,在真实世界中缓存必须能应对一对多关系和分页等问题。

Using The Cache 使用缓存

So how do we use this cache? Let’s look at two operations: writing to the cache when a response is received, and reading from the cache to determine if a query can be fulfilled locally (the equivalent to _cache.has(key) above, but for a graph).
所以我们该如何使用缓存呢?我们看这两个操作:当收到返回结果时写缓存,和从缓存中读取数据判断查询是否可以被本地缓存完全满足。(与_cache.has(key)原理相同,不同的是这个针对图而言)

Populating The Cache 填充缓存

Populating the cache involves walking a hierarchical GraphQL response and creating or updating normalized cache records. At first it may seem that the response alone is sufficient to process the response, but in fact this is only true for very simple queries. Consider user(id: "456") { photo(size: 32) { uri } } — how should we store photo? Using photo as the field name in the cache won’t work because a different query might fetch the same field but with different argument values (e.g. photo(size: 64) {...}). A similar issue occurs with pagination. If we fetch the 11th to 20th stories with stories(first: 10, offset: 10), these new results should be appended to the existing list.
填充缓存需要爬GraphQL返回的层次结构,创建或者更新扁平化的缓存记录。看上去我们只需要用返回的数据就足以应付这件事,事实上这样只能应付非常简单的查询。考虑user(id: "456") { photo(size: 32) { uri } },我们如何存储photo?使用photo作为缓存中的字段名是行不通的,因为其他不同的查询可能使用同一个字段,但是用了不同的参数(e.g. photo(size: 64) {...})。类似的情况还出现在分页上。如果我们去获取11到20条故事stories(first: 10, offset: 10),这些新的返回结果应该追加到已有的列表上。

Therefore, a normalized response cache for GraphQL requires processing payloads and queries in parallel. For example, the photo field from above might be cached with a generated field name such as photo_size(32) in order to uniquely identify the field and its argument values.
因此,构建一个扁平化的GraphQL响应缓存,需要同时使用返回的数据和发出的查询。例如,上面的photo字段,可能会用像这样的合成字段名,如photo_size(32),为的是唯一的标记字段和它的参数值。

Reading From Cache 读取缓存

To read from the cache we can walk a query and resolve each field. But wait: that sounds exactly like what a GraphQL server does when it processes a query. And it is! Reading from the cache is a special case of an executor where a) there’s no need for user-defined field functions because all results come from a fixed data structure and b) results are always synchronous — we either have the data cached or we don’t.
读取缓存我们可以爬该查询,解析每个字段。但是等等,这个听起来和GraphQL 服务器处理查询的逻辑一模一样。是的,他们就是一样!读取缓存是一个特例,这里没必要使用用户定义字段的函数,因为所有的结果都来自固定的数据结构,并且结果总是同步的,我们要么缓存着这个数据,要么没有。

Relay implements several variations of query traversal: operations that walk a query alongside some other data such as the cache or a response payload. For example, when a query is fetched Relay performs a “diff” traversal to determine what fields are missing (much like React diffs virtual DOM trees). This can reduce the amount of data fetched in many common cases and even allow Relay to avoid network requests at all when queries are fully cached.
Relay实现了若干查询遍历,他们沿着缓存或返回的数据进行遍历。例如,当一个查询被执行,Relay使用”diff”遍历来确定还缺少哪些字段(与React diffs virtual DOM trees很像)。在大多数情况下,这样可以减少需要获取的数据量,甚至在所需数据都被缓存的情况下,允许Relay避免网络请求。

Cache Updates 缓存更新

Note that this normalized cache structure allows overlapping results to be cached without duplication. Each record is stored once regardless of how it is fetched. Let’s return to the earlier example of inconsistent data and see how this cache helps in that scenario.
注意,扁平化的缓存允许相互覆盖的数据在不复制冗余的情况下被缓存。每一条记录被存储一次,不论它是怎么获取的。让我们回过头来看之前那个数据不一致的例子,看看这种缓存机制是如何应对的。

The first query was for a list of stories:
第一个查询是要获取故事列表:

query { stories { id, text, likeCount } }

With a normalized response cache, a record would be created for each story in the list. The stories field would store links to each of these records.
使用扁平化缓存时,列表中的每一个故事会创建一条记录。stories字段可以存储这些记录的链接。

The second query refetched the information for one of those stories:
第二条查询重新获取其中一个故事的信息:

query { story(id: "123") { id, text, likeCount } }

When this response is normalized, Relay can detect that this result overlaps with existing data based on its id. Rather than create a new record, Relay will update the existing 123 record. The new likeCount is therefore available to both queries, as well as any other query that might reference this story.
当这个返回数据被扁平化,根据id,Relay发现它与现有的数据重叠。Relay不会创建一条新记录,而是更新id为123的记录。所以新的likeCount对两个查询都是有效的,当然其他查询如果用到这条记录,得到的也是一致的。

Data/View Consistency 数据/视图 一致性

A normalized cache ensures that the cache is consistent. But what about our views? Ideally, our React views would always reflect the current information from the cache.
一个扁平化的缓存确保缓存是一致的。但是视图如何呢?理想情况下,我们的React视图将总是从缓存中反映最新的数据。

Consider rendering the text and comments of a story along with the corresponding author names and photos. Here’s the GraphQL query:
考虑渲染一个故事的文字和评论,以及对应的作者名字和照片。以下是GraphQL查询:

query {
  node(id: "1") {
    text,
    author { name, photo },
    comments {
      text,
      author { name, photo }
    }
  }
}

After initially fetching this story our cache might be as follows. Note that the story and comment both link to the same record as author:
当初始化获取该故事信息后,我们的缓存大概是下面这个样子。注意该故事和评论都链接到同一个author记录:

// Note: This is pseudo-code for `Map` initialization to make the structure
// more obvious.
Map {
  // `story(id: "1")`
  1: Map {
    author: Link(2),
    comments: [Link(3)],
  },
  // `story.author`
  2: Map {
    name: 'Yuzhi',
    photo: 'http://.../photo1.jpg',
  },
  // `story.comments[0]`
  3: Map {
    author: Link(2),
  },
}

The author of this story also commented on it — quite common. Now imagine that some other view fetches new information about the author, and her profile photo has changed to a new URI. Here’s the only part of our cached data that changes:
故事的作者也自己进行了评论,这是很常见的。现在假如其他的视图获取了该作者新的信息,并且她的资料照片变换了新的URI。这是我们缓存中唯一变化的地方:

Map {
  ...
  2: Map {
    ...
    photo: 'http://.../photo2.jpg',
  },
}

The value of the photo field has changed; and therefore the record 2 has also changed. And that’s it. Nothing else in the cache is affected. But clearly our view needs to reflect the update: both instances of the author in the UI (as story author and comment author) need to show the new photo.
photo字段的值改变了。因此记录2也改变了。就是这样,缓存中的其他没有被影响。但是我们的视图需要反映这些更新,所以显示了作者的照片的UI都需要显示新的照片。

A standard response is to “just use immutable data structures” — but let’s see what would happen if we did:
一个标准的反应是“用immutable数据结构来处理”,但是让我们看看如果我们这样做会发生什么:

ImmutableMap {
  1: ImmutableMap {/* same as before */}
  2: ImmutableMap {
    ... // other fields unchanged
    photo: 'http://.../photo2.jpg',
  },
  3: ImmutableMap {/* same as before */}
}

If we replace 2 with a new immutable record, we’ll also get a new immutable instance of the cache object. However, records 1 and 3 are untouched. Because the data is normalized, we can’t tell that story’s contents have changed just by looking at the story record alone.
如果我们用一个新的immutable记录替换了2,我们将得到一个新的缓存对象的immutable实例。尽管如此,记录13并没有改变。因为数据是扁平化的,我们不能仅通过看story记录本身就说story的内容改变了。

Achieving View Consistency 获得视图一致性

There are a variety of solutions for keeping views up to date with a flattened cache. The approach that Relay takes is to maintain a mapping from each UI view to the set of IDs it references. In this case, the story view would subscribe to updates on the story (1), the author (2), and the comments (3 and any others). When writing data into the cache, Relay tracks which IDs are affected and notifies only the views that are subscribed to those IDs. The affected views re-render, and unaffected views opt-out of re-rendering for better performance (Relay provides a safe but effective default shouldComponentUpdate). Without this strategy, every view would re-render for even the tiniest change.
有很多种方法可以让使用了扁平化缓存的视图保持最新。Relay使用的方案是维护一个映射,从每一个UI视图到他们关联的IDs。这种情况下,这个故事视图将订阅story(1),author(2)和comments(3)的更新。当把数据写入缓存的时,Relay跟踪哪些IDs受到影响,并通知这些订阅了那些IDs的视图。受影响的视图重新渲染,未受影响的不重新渲染,这样提高了性能(Relay提供了一个安全高效的默认shouldComponentUpdate)。如果没有这个策略,哪怕是一点小的改动都会引起没一个视图重新渲染。

Note that this solution will also work for writes: any update to the cache will notify the affected views, and writes are just another thing that updates the cache.
注意这个方案在写入的时候同样起作用。任何对缓存的更新都将通知受影响的视图,写入只是另一个更新缓存而已。

Mutations

So far we’ve looked at the process of querying data and keeping views up to date, but we haven’t looked at writes. In GraphQL, writes are called mutations. We can think of them as queries with side effects. Here’s an example of calling a mutation that might mark a given story as being liked by the current user:
目前为止我们已经看如何查询数据,如何保持视图更新,但是我们还没有讲到写入。在GraphQL中,写入被称为mutations。我们可以认为他们是带有副作用的查询。这里有一个例子,调用mutation来表示当前用户赞了这个故事。

// Give a human-readable name and define the types of the inputs,
// in this case the id of the story to mark as liked.
mutation StoryLike($storyID: String) {
   // Call the mutation field and trigger its side effects
   storyLike(storyID: $storyID) {
     // Define fields to re-fetch after the mutation completes
     likeCount
   }
}

Notice that we’re querying for data that may have changed as a result of the mutation. An obvious question is: why can’t the server just tell us what changed? The answer is: it’s complicated. GraphQL abstracts over any data storage layer (or an aggregation of multiple sources), and works with any programming language. Furthermore, the goal of GraphQL is to provide data in a form that is useful to product developers building a view.
注意到我们正在查询的是作为mutation返回结果而可能改变的数据。一个明显的问题是:为什么服务器不能直接告诉我们什么变了?答案是:这很复杂。GraphQL意在抽象任何数据存储层(或者多个源的集合),并且可以与各种编程语言工作。此外,GraphQL的目标是提供格式化的数据,对于产品开发者构建视图是很有用的。

We’ve found that it’s common for the GraphQL schema to differ slightly or even substantially from the form in which data is stored on disk. Put simply: there isn’t always a 1:1 correspondence between data changes in your underlying data storage (disk) and data changes in your product-visible schema (GraphQL). The perfect example of this is privacy: returning a user-facing field such as age might require accessing numerous records in our data-storage layer to determine if the active user is even allowed to see that age (Are we friends? Is my age shared? Did I block you? etc.).
我们发现一个普遍的现象,GraphQL schema与硬盘上存储的数据是不同的或者说区别很大。简单来说,在数据存储上的数据变化,和GraphQL表示的产品视图schema的数据变化之间没有1对1的关系。一个非常恰当的例子是“隐私”,请求用户的年龄字段,可能需要访问数据存储层上的多条记录,用来判断该用户是否允许你看到她的年龄信息(你们是不是朋友?我的年龄是否公开,我是否拉黑你,等等)

Given these real-world constraints, the approach in GraphQL is for clients to query for things that may change after a mutation. But what exactly do we put in that query? During the development of Relay we explored several ideas — let’s look at them briefly in order to understand why Relay uses the approach that it does:
我们看到这些来自真实世界的约束,GraphQL的方案设计是为客户端了查询那些因为mutation而可能发生改变的数据。但是,我们到底在这查询中应该写些什么?在开发Relay的过程中,我们试过若干的主意,我们来简单的了解一下这几个方案,这样可以更好的帮您理解Relay采用了哪种方案以及为什么。

  • Option 1: Re-fetch everything that the app has ever queried. Even though only a small subset of this data will actually change, we’ll still have to wait for the server to execute the entire query, wait to download the results, and wait to process them again. This is very inefficient.
  • 方案1:重新获取应用使用过的每个查询。即使实际只改动了一点点,我们将等待服务器执行完整的查询,等待下载查询结果,等待着接收下一次查询。这个方案很低效。

  • Option 2: Re-fetch only the queries required by actively rendered views. This is a slight improvement over option 1. However, cached data that isn’t currently being viewed won’t be updated. Unless this data is somehow marked as stale or evicted from the cache subsequent queries will read outdated information.

  • 方案2:仅仅重新获取活动视图所需的查询。这个方案1的改进。尽管如此,没有被显示出来的缓存的数据不会重新获取。除非这些数据被标记为过期或者因为查询读到了过期信息而被清出缓存。

  • Option 3: Re-fetch a fixed list of fields that may change after the mutation. We’ll call this list a fat query. We found this to also be inefficient because typical applications only render a subset of the fat query, but this approach would require fetching all of those fields.

  • 方案3:mutation之后针对每个mutation重新查询获取一组固定的字段。我们称它为fat query。我们发现它还是不高效,因为典型的应用通常只需要fat query的一个子集,但是该方案还是要获取全部的fat query。

  • Option 4 (Relay): Re-fetch the intersection of what may change (the fat query) and the data in the cache. In addition to the cache of data Relay also remembers the queries used to fetch each item. These are called tracked queries. By intersecting the tracked and fat queries, Relay can query exactly the set of information the application needs to update and nothing more.

  • 方案4(Relay):重新获取fat query和缓存进行计算的交集。除了缓存了数据之外,Relay还记录了用户获取它们的查询。这些被称之为跟踪查询。通过取跟踪查询与fat query之间的交集,Relay可以精确地查询应用所需的数据,不多不少。

Data-Fetching APIs

So far we looked at the lower-level aspects of data-fetching and saw how various familiar concepts translate to GraphQL. Next, let’s step back and look at some higher-level concerns that product developers often face around data-fetching:
我们看到了数据获取的一些底层原理和一些在GraphQL中实现的一些我们熟悉概念。接下来,我们来看作为产品开发者经常关注的一些数据获取问题:

  • Fetching all the data for a view hierarchy.
  • Managing asynchronous state transitions and coordinating concurrent requests.
  • Managing errors.
  • Retrying failed requests.
  • Updating the local cache after receiving query/mutation responses.
  • Queuing mutations to avoid race conditions.
  • Optimistically updating the UI while waiting for the server to respond to mutations.
  • 获取视图层次结构的所有数据
  • 管理异步状态转移和协调并发请求
  • 错误管理
  • 重试错误的请求
  • 接收到query/mutation响应之后更新本地缓存
  • 让Mutation排队,避免竞争
  • 当等待服务器响应Mutation的时候,有优化策略的更新UI

We’ve found that typical approaches to data-fetching — with imperative APIs — force developers to deal with too much of this non-essential complexity. For example, consider optimistic UI updates. This is a way of giving the user feedback while waiting for a server response. The logic of what to do can be quite clear: when the user clicks “like”, mark the story as being liked and send the request to the server. But the implementation is often much more complex. Imperative approaches require us to implement all of those steps: reach into the UI and toggle the button, initiate a network request, retry it if necessary, show an error if it fails (and untoggle the button), etc. The same goes for data-fetching: specifying what data we need often dictates how and when it is fetched. Next, we’ll explore our approach to solving these concerns with Relay.
我们发现传统的数据获取方案,命令式的APIs,迫使开发者处理很多不重要但是麻烦的事情。例如,考虑优化UI更新。意思是说在用户等待服务器响应的时候给予响应。逻辑听上去很简单,例如,用户点击赞,表示喜欢某个故事,发送请求到服务器。但是实现却很复杂。命令式的方案要求我们实现如下步骤:到UI所在位置,变换按钮,初始化网络请求,如果需要可以重试,如果失败返回错误,并且恢复按钮状态,等等。同样的事情还发生在数据获取上,指定我们需要什么数据还需要指定何时及如何获取。接下来,我们将在Relay中介绍是如何解决这些问题的。

你可能感兴趣的:(Relay文档翻译)