[关闭]
@caelumtian 2017-08-22T12:30:04.000000Z 字数 10454 阅读 786

How to leverage Local Storage to build lightning-fast apps

(如何利用 localStorage 来构建更快的应用程序)

英文文献翻译

原文地址

Users love fast, responsive apps. They don’t want to hear about how API calls take time. They just want to see updates immediately. Right now. And we as a developers should strive to provide that. So how can we?
(用户偏爱快速响应的应用程序。他们并不关心API运行所需要的时间,而仅仅是想立即看到变化。所以我们怎么做才能尽力满足用户的需求?)

The solution: storing those changes locally, then synchronizing[同步] them with your servers from time to time. But this gets way more complex when things like connection latency[潜伏] is taken into account.
(解决方案:本地存储应用的更改,然后不定时的与你的服务器同步这些内容。但是当我们考虑到连接延迟问题的时候,这样做将会变得更加复杂。)

Let’s take Medium for example. Medium users can recommend an article to their followers by tapping a little green heart (there’s one on this page, too 😉). By tapping the heart a second time, the user can stop recommending it.
(让我们以一个媒体网站为例,用户可以通过点击 ❤️ 按钮来推荐一篇文章给他的朋友们。当用于再次点击该按钮后,则取消推荐)

The functionality is simple, but the edge[边缘] cases cause lots of problems

(上述案例虽然简单,但有一些极端的例子造成了很多问题)

I don’t know exactly what happens inside Medium’s app, but for simplicity, lets imagine that the first tap adds an item to the recommendation list, and the second one removes it.
Let’s see what kinds of problems this could cause for us if we decided to add similar functionality to our app:

(我们并不知道网站内部发生了什么,为了简单起见,我们可以想象在第一次点击的时候,程序将一个项目添加到了推荐列表中,并且在第二次点击的时候移除这个项目。
下面让我们看看,如果我们开发这样一个简单的应用功能,会遇到哪儿些问题:)

  1. We should take into account that user can start tapping like crazy. This behavior could lead to a stream of events.
    (我们需要考虑如果用户疯狂的点击 ❤️ 按钮,这些行为将会触发一系列响应 事件)
  2. Internet isn’t always fast. On a bad network connection, even simplest API calls could take several seconds to finish. During this time, the user could leave the current screen, then return.
    (网速并不总是快的。在一个网速差的环境下,甚至连最简单的API调用都要花上几秒钟才可以完成。在这段时间用户就有可能离开了当前屏幕,然后才会返回)
  3. From time to time, API calls can fail, and our system needs to be able to recover from such situations.
    (有的时候,API调用可能失败,我们的程序应该能够有能力从在状况下正常运行)
  4. Users can have multiple devices with the same app, or they can use both the mobile app version and the corresponding website in tandem. In either case, we should have a policy[政策] for synchronizing data with our back end to update its state.
    (用户有可能使用不同的设备来打开我们的网站,或者同时在移动设备和PC上面访问我们的网站。不管在哪儿种情况下我们都应该有一个策略来和后端同步数据并更新其状态)

This isn’t a full list of the challenges we face, but these are the ones this article will focus on addressing.
(我们在实际中可能遇到更多的问题,但是本文着重来解决上面提到的问题)

Defining the problem (明确问题)

Before we start discussing implementation[n. 实现], lets define our acceptance[接受] criteria[标准]. The task is to develop a feature that allows the user to add and remove items from a certain list. The list is stored on our back end.
(在讨论如何解决问题之前,我们来先定义要开发功能的实现标准。任务是开发一个可以在列表中添加和删除项目的功能,列表数据存储在后端)
Implementation must fulfill the following requirements:
(功能必须满足如下要求:)

  1. The user interface reacts immediately to the user’s actions. The user wants to see the results of their actions immediately. If later we can’t synchronize those changes, we should notify our user, and roll back to the previous state.
    (用户界面需要立刻响应用户的操作,让用户看到他们操作的结果。如果之后由于某些原因我们不能同步这些更改内容,我们应该通知用户操作失败。并且回滚到之前的状态)

  2. Interaction[n. 互动] from multiple devices is supported. This doesn’t mean that we need to support changes in real time, but we do need to fetch the whole collection from time to time. Plus, our back end provides us with API endpoints[端点] for additions and removals, which we must use to support better synchronization.
    (支持多个设备的交互。这并不意味着我们需要支持实时修改的功能,但是我们需要不断地获取整个数据。此外,后端为我们提供了添加和删除项目的API,我们必须使用它们来支持更好的同步效果)

  3. Integrity[n. 完整;诚实] of the data is guaranteed[adj. 有保证的]. Whenever a synchronization call fails, our app should recover gracefully from errors.
    (保证数据的完整性:无论什么时候一旦数据同步失败,我们的网站都应该从错误中恢复正常状态)

Luckily, we don’t need to implement the whole feature, but rather develop a storage mechanism[n. 机制|原理] that will allow us to implement[vt. 实现] it. Let’s investigate[v. 调查|研究] different ways to meet these requirements.
(幸运的是,我们并不需要实现所有,而是开发一种可以实现它的数据存储机制。让我们来探究不同的实现方案)

The straight-forward approach[n. 方法]

(直接的方法) [好奇怪的名称]

The first solution that comes to mind is to store a local copy of the list, then update it when the user makes a change.
(第一种解决办法是在 localStorage 中存储一份列表数据的副本,当用户进行操作时,我们也同时更新 localStorage 中的数据)

Most of the problems with this approach are related[adj. 有关系的] to race[?不太懂] conditions or API call failures, for example:

(这种解决问题的方案大多数与竞争条件或者API调用失败有关,例如:)

  1. Collisions[n. 撞击冲突] between fetching and changing the list. Lets imagine that we started fetching items from our back end to update our local storage, and the user made a change before that operation finished. This would lead to a merge conflict[n. 冲突] between fetched list and the local one. So we need to distinguish[vt. 区分], for example, between an item that wasn’t added yet and an item that was already removed from the web or another device.
    (获取并修改列表之间冲突。让我们设想这样一个场景,网站从后端获取列表数据来更新我们的 localStorage。用户这个在更新没有完成之前,修改了数据。这将导致获取到的列表和本地列表之间产生合并冲突。为此我们需要区分那些还没有添加的项目和已经从web中或其他设备上删除的项目)

  2. API call failure. Users can make lots of changes quickly, and they can also revert them quickly. For example, users can add an item to a list, then remove them, then add them back. If the first addition fails, then we should recover from it. In this case, we need to remove the item from the list. But that would ruin the integrity of our data, because the item should actually be on the list, since the last call we made was an addition and it wasn’t finished yet.

(API调用失败。用户可能进行快速的大量的修改操作,也有可能是恢复操作。例如,用户可以添加项目到列表,然后删除它们,然后又添加回来。如果第一次操作失败,我们应该复原列表即从列表中删除该项。但是这样会破坏我们数据的完整性,因为该项目实际是应该在列表中的。我们最后一次调用时添加操作,而且它还没有完成)

Even though there could be a way to make this approach work, I would argue that local storage should keep more information than just the final expected result. This will allow us to recover from all problems we may encounter[vt. 遭遇].

(因此,我认为应该保留更多的信息在 localStorage 中,而不仅仅只有最终的预期效果。这样我们才有能力从可能遇到的问题中恢复过来)

Let’s keep history of everything the user does

(保留用户的操作历史记录)

Here’s a different approach: let’s keep the list we fetched from the API, as well as record everything the user has done. Every record would match an API call (“add” and “remove” respectively[adv. 分别的]).
(这里有一个不同的方案:我们保留冲后端获取到的列表,并记录用户的所有操作。每个记录都会匹配一个后端API的调用(分别是'add'和'remove'))

Once our API call finishes, we can update our local copy and remove the record from our history. When we want to synchronize the user’s browser with our back end, we just fetch the version of that list and replace our copy.
(一旦API调用完成,我们更新本地副本数据并从历史记录中删去记录。当我们想和后端同步用户浏览器数据,我们仅仅获取列表的版本然后替换我们的副本)

We no longer have any problems with API call failure, because we know the exact state before the call, and can just drop that record from the history without losing data integrity.
(我们不在有任何API调用失败的问题,因为我们明确知道API调用前列表的状态,并且我们可以从历史记录中删除该记录,从而保证数据的完整性。)

The main problem with this is performance. Every time we want to check whether a particular item is in the list, we need to go through all the records to calculate what our user should expect to see.
(这么做主要会带来性能问题。每次检查一个特性的项目是否在列表中,我们都需要通过所有记录来计算用户期望看到的内容。)

Of course, performance depends on the amount of interactions our user can do within a certain timeframe, and the way the data is stored. Plus keep in mind that premature optimization is the root of all evil, so if you don’t have this problem, then probably this is a way to go.
(当然,这些性能都取决于在一定时间内用户进行的交互次数以及数据的存储方式。下面一句不理解?)

I think that this approach is great when user creates the content in the app, because it exposes lots of ways for handling
synchronization issues. But our problem is simpler than that, so we should be able to make some optimizations and further increase performance.
(我认为,这种方案非常利于用户在应用中创建内容的场景,因为它提供了许多解决同步问题的方案。但是我们的问题比这更简单,所以我们应该能够进行一些优化来提升性能)

The middle ground

(中间地带)

It’s possible to have just enough information to recover from negative cases. Having two extra lists — one for ongoing[adj 不间断的] additions, and one for ongoing removals — should be enough. To ensure data integrity, you would just need to apply a few rules:
(这种方案有足够的信息从负面情况中恢复。我们需要两个额外的列表,一个用于持续添加另一个用于删除。为了确保数据的完整性,你仅仅需要添加一些规则:)

  1. Lists with additions and removals have priority over the main list. For example, let’s say an item is in both the removals list and in the main list. When the browser checks to see whether the item is in the list, it should return false.

(添加和删除列表优先于主列表。例如:一个项目同时在删除列表中和主列表中时。如果浏览器检查项目是否在列表中,它应该返回 false)

  1. One item can’t be in both lists at once. If the user made multiple actions on a single item, the latest change should have priority. For example, if the user added and then removed the item, as a result it should be in the list for removals. It doesn’t matter whether the item is in the main list or not.
    (一个项目不能同时出现在两个列表中如果用户对一个项目进行了多次操作,则最后的修改应该具有优先级。例如,如果用户添加了项目然后删除了它,作为结果它应该出现在删除列表中。项目在不在主列表中反而无关紧要。)

  2. Only after the last API call for a certain item has finished can it be removed from the corresponding[adj. 相当的|相应的] list. For example, the user could have added the item, removed it, then added it again before the first call is finished. In this case, the item would be in the list for additions. But it should be removed from there only after the second addition is finished. This can be achieved by assigning an ID to each entry in those lists. Later, after API call is finished, the entry would be removed using this ID.
    (只有某个项目在最后一次调用API完成后,才可以从相应的列表中删除。例如,用户添加了一个项目并删除了它,然后又在第一次调用完成之前添加了它。在这种情况下,该项目应该在添加列表中。但是只有在第二次添加完成后它才应该被删除。我们可以通过为每个条目分配一个ID来实现。在API调用完成后,删除使用这些ID的条目)

  3. After every API call, the main list should be updated. The main list should reflect the actual state of the backend to the best of our knowledge. So in the case of consecutive[adj. 连续不断地] addition and removal, even though from app side it would looks like item was not in the list, after the first call we should add it to the main list.

(每次API调用完成后,主列表应该被更新。主列表应该反映后端的实际情况。所以在连续的添加和删除的情况下,即使在客户端看起来,项目并不在列表中,在第一次调用后我们应该把它添加到主列表中。)

A few words about API call failures

(关于API调用失败)

There are different reasons why an API call can fail. Some of them temporary, some of them not. Some of them are fatal, and some of them are possible to recover from. Regardless of the solution, even failed requests should return some information about the cause of the problem.
(调用API失败的原因是有所不同的。有些是临时的,有些不是。他们当中有些是致命的,有些事可以恢复的。无论解决方案是什么,失败的请求都应该返回一些关于失败原因的有用信息)

I think that HTTP status codes are perfect for this. For example, if the status code is 504 Gateway Timeout, then retrying could be a good idea, but if it is 400 Bad Request, then most likely some client logic is wrong and simple retry logic won’t help. Some of them, like 401 Unauthorized, could require some user actions. 410 Gone or 404 Not Found during the removal call could mean that the user removed this item from a different device and most likely we can even tell user that the operation was successful, since user’s intention is fulfilled.

(我认为HTTP状态吗是完美的。例如,如果状态吗是504网关超时,重新请求将是个不错的方案。但是如果是400请求错误,那么简单的重新请求将不会有任何效果。其中一些,比如401未经授权,可能需要用户额外的操作。在删除项目的时候,410状态码就可能意味着是用户从不同的设备删除了该项目。)

Conclusion

(结论)
The first solution was a simple list. It was fast, but handling negative cases was difficult.
(第一个解决方案是简单的列表,他是快速的,但处理负面情况是困难的)

In the second approach, we created a data structure that acts like a list, but persisted[v.persist 持续] the records of all the changes made. This could handle negative cases, but it was much slower.
(第二种方案,我们创建了一个像列表的数据结构,但是保留了所有的更改记录。这有利于解决负面情况,但是速度很慢)

Our middle ground was a solution that — from outside — still acts like a list. But it allows us to balance performance and easily recovery from errors.
(中间地带解决方案,从外表看依然想一个列表。但是他允许我们平衡性能并且简单快速的从错误中恢复。)

The issues mentioned in this article are only one side of the problem. The other is the amount of API calls made. If the user performs a lot of similar interactions, we can try to minimize the amount of API calls made. This optimization affects the structure of our local storage as well.
(本文提到的问题只是一个方面。还有就是API调用的数量问题。如果用户执行了大量类似的交互,我们可以尝试最小化API调用的数量。此优化也会影响本地存储的结构。)

I will discuss this and propose additional solution to these issues in my next articles.

(我将在下一篇文章中讨论这个问题,并提出这些问题的其他解决方案。)

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注