本文翻译自:How to leverage Local Storage to build lightning-fast apps
如果原文打不开请移步:这里
用户偏爱快速响应的应用程序。他们并不关心API运行所需要的时间,而仅仅是想立即看到变化。所以我们怎么做才能尽力满足用户的需求?
解决方案:本地存储应用的更改,然后不定时的与你的服务器同步这些内容。但是当我们考虑到连接延迟问题的时候,这样做将会变得更加复杂。
让我们以一个媒体网站为例,用户可以通过点击 ❤️ 按钮来推荐一篇文章给他的朋友们。当用于再次点击该按钮后,则取消推荐。
上述案例虽然简单,但有一些极端的例子造成了很多问题
我们并不知道网站内部发生了什么,为了简单起见,我们可以想象在第一次点击的时候,程序将一个项目添加到了推荐列表中,并且在第二次点击的时候移除这个项目。
下面让我们看看,如果我们开发这样一个简单的应用功能,会遇到哪儿些问题
- 我们需要考虑如果用户疯狂的点击 ❤️ 按钮,这些行为将会触发一系列响应 事件
- 网速并不总是快的。在一个网速差的环境下,甚至连最简单的API调用都要花上几秒钟才可以完成。在这段时间用户就有可能离开了当前屏幕,然后才会返回
- 有的时候,API调用可能失败,我们的程序应该能够有能力从在状况下正常运行
- 用户有可能使用不同的设备来打开我们的网站,或者同时在移动设备和PC上面访问我们的网站。不管在哪儿种情况下我们都应该有一个策略来和后端同步数据并更新其状态
我们在实际中可能遇到更多的问题,但是本文着重来解决上面提到的问题。
明确问题
在讨论如何解决问题之前,我们来先定义要开发功能的实现标准。任务是开发一个可以在列表中添加和删除项目的功能,列表数据存储在后端。功能必须满足如下要求:
- 用户界面需要立刻响应用户的操作,让用户看到他们操作的结果。如果之后由于某些原因我们不能同步这些更改内容,我们应该通知用户操作失败。并且回滚到之前的状态。
- 支持多个设备的交互。这并不意味着我们需要支持实时修改的功能,但是我们需要不断地获取整个数据。此外,后端为我们提供了添加和删除项目的API,我们必须使用它们来支持更好的同步效果。
- 保证数据的完整性:无论什么时候一旦数据同步失败,我们的网站都应该从错误中恢复正常状态。
幸运的是,我们并不需要实现所有,而是开发一种可以实现它的数据存储机制。让我们来探究不同的实现方案。
最直接的方法
第一种解决办法是在 localStorage 中存储一份列表数据的副本,当用户进行操作时,我们也同时更新 localStorage 中的数据。这种解决问题的方案大多数与竞争条件或者API调用失败有关,例如:
获取并修改列表之间冲突
让我们设想这样一个场景,网站从后端获取列表数据来更新我们的 localStorage。用户这个在更新没有完成之前,修改了数据。这将导致获取到的列表和本地列表之间产生合并冲突。为此我们需要区分那些还没有添加的项目和已经从web中或其他设备上删除的项目。API调用失败
用户可能进行快速的大量的修改操作,也有可能是恢复操作。例如,用户可以添加项目到列表,然后删除它们,然后又添加回来。如果第一次操作失败,我们应该复原列表即从列表中删除该项。但是这样会破坏我们数据的完整性,因为该项目实际是应该在列表中的。我们最后一次调用时添加操作,而且它还没有完成。
因此,我认为应该保留更多的信息在 localStorage 中,而不仅仅只有最终的预期效果。这样我们才有能力从可能遇到的问题中恢复过来。
保留用户的操作历史记录
这里有一个不同的方案:我们保留冲后端获取到的列表,并记录用户的所有操作。每个记录都会匹配一个后端API的调用(分别是’add’和’remove’)。
一旦API调用完成,我们更新本地副本数据并从历史记录中删去记录。当我们想和后端同步用户浏览器数据,我们仅仅获取列表的版本然后替换我们的副本。
我们不在有任何API调用失败的问题,因为我们明确知道API调用前列表的状态,并且我们可以从历史记录中删除该记录,从而保证数据的完整性。
这么做主要会带来性能问题。每次检查一个特性的项目是否在列表中,我们都需要通过所有记录来计算用户期望看到的内容。当然,这些性能都取决于在一定时间内用户进行的交互次数以及数据的存储方式。
我认为,这种方案非常利于用户在应用中创建内容的场景,因为它提供了许多解决同步问题的方案。但是我们的问题比这更简单,所以我们应该能够进行一些优化来提升性能。
中间地带
这种方案有足够的信息从负面情况中恢复。我们需要两个额外的列表,一个用于持续添加另一个用于删除。为了确保数据的完整性,你仅仅需要添加一些规则:
添加和删除列表优先于主列表
例如:一个项目同时在删除列表中和主列表中时。如果浏览器检查项目是否在列表中,它应该返回 false。一个项目不能同时出现在两个列表中如果用户对一个项目进行了多次操作,则最后的修改应该具有优先级
例如,如果用户添加了项目然后删除了它,作为结果它应该出现在删除列表中。项目在不在主列表中反而无关紧要。只有某个项目在最后一次调用API完成后,才可以从相应的列表中删除
例如,用户添加了一个项目并删除了它,然后又在第一次调用完成之前添加了它。在这种情况下,该项目应该在添加列表中。但是只有在第二次添加完成后它才应该被删除。我们可以通过为每个条目分配一个ID来实现。在API调用完成后,删除使用这些ID的条目。每次API调用完成后,主列表应该被更新
主列表应该反映后端的实际情况。所以在连续的添加和删除的情况下,即使在客户端看起来,项目并不在列表中,在第一次调用后我们应该把它添加到主列表中。
关于API调用失败
调用API失败的原因是有所不同的。有些是临时的,有些不是。他们当中有些是致命的,有些事可以恢复的。无论解决方案是什么,失败的请求都应该返回一些关于失败原因的有用信息。
我认为HTTP状态吗是完美的。例如,如果状态吗是504网关超时,重新请求将是个不错的方案。但是如果是400请求错误,那么简单的重新请求将不会有任何效果。其中一些,比如401未经授权,可能需要用户额外的操作。在删除项目的时候,410状态码就可能意味着是用户从不同的设备删除了该项目。
总结
- 第一个解决方案是简单的列表,他是快速的,但处理负面情况是困难的
- 第二种方案,我们创建了一个像列表的数据结构,但是保留了所有的更改记录。这有利于解决负面情况,但是速度很慢
- 中间地带解决方案,从外表看依然想一个列表。但是他允许我们平衡性能并且简单快速的从错误中恢复
本文提到的问题只是一个方面。还有就是API调用的数量问题。如果用户执行了大量类似的交互,我们可以尝试最小化API调用的数量。此优化也会影响本地存储的结构。