将一个纯本地应用移植到 Web 端
共 2996字,需浏览 6分钟
·
2021-01-21 14:46
点击上方“逆锋起笔”,公众号回复 pdf 领取大佬们推荐的学习资料
作者 | James Long
译者 | 王强
策划 | 李俊辰
在研究一个奇怪的缓存错误(https://actualbudget.com/blog/cursed-caching-curious)时我得到了启发,于是去重新看了一下 Actual 是如何在 Web 端本地存储数据的。这里我需要解释一些历史背景:多年前,Actual 原本是一个单纯的桌面应用程序来着。这意味着我们的所有数据都会存储在本地,没有服务器,自然也不会在网络上存储任何内容。
然后我意识到了移动平台的重要性,并且发觉大多数用户不想为这样的事情而担惊受怕:某天设备失手扔进了大海,然后数据也一并烟消云散。正因如此,同步引擎诞生了。从那时起,桌面和移动应用程序就可以愉快地同步它们的数据了。一份数据副本被保存在服务器上,这样用户就可以在登录后轻松查看他们的数据。如果担心隐私安全问题,应用程序可以启用端到端加密。
去年,我开始嫉妒 Web 应用。看看那些应用吧,部署起来那么轻松方便……它们可以那么直截了当地将用户带入应用,用不着麻烦的安装过程。可是在桌面端,我得先要求用户下载 80MB 大小的文件,然后他们才能开始运行应用。这种下载需求当然会严重影响用户转化率,还会让登录流程、支持、A/B 测试以及所有事务做起来都麻烦许多。
我很喜欢桌面应用,因为你可以在桌面端用上好得多的技术(例如原生的 sqlite3);桌面应用的速度也非常快(无需网络调用),并且用户可以完全掌控自己的数据。但我也不得不承认,Web 带来的种种优势让桌面端的这些好处相形见绌。
https://www.kalzumeus.com/2009/09/05/desktop-aps-versus-web-apps/
我开始考虑开发 Actual 的 Web 版本。经过一番研究,做了点技术活儿后,我没有改动整个架构就移植到了 Web 端。
https://app.actualbudget.com/
这意味着你的所有数据仍会存储在浏览器本地,并且没有网络调用。它是在浏览器 [注 0] 中运行的完全 100%的“本地”应用。
我还没有对这个 Web 版本大肆宣传,因为它还没有经过足够的测试,并且有不少内容需要改进,例如采用代码延迟加载技术来加快加载速度。我最担心的是数据存储层。由于 所有数据都在本地存储,因此如果本地环境出现了什么问题,用户就可能会丢失数据。而且因为我们要把所有内容都存储在本地,这给浏览器的持久数据库也带来了巨大压力。关注公众号 逆锋起笔,回复 pdf,下载你需要的各种学习资料。
需要明确的是:我们不会弃用桌面版本。但将来,Web 版本将成为 Actual 的主要平台,如果用户需要则可以选择下载桌面版本。
它的工作机制不太常见。下面我从高级层面做一概述:
Actual 使用的是 sqlite3。这是一个硬性要求。这款应用会运行大量复杂的 SQL 查询以汇总财务数据,这是它的专长所在。查询都很容易表达,而且运行速度非常快。
在桌面和移动端,我们使用的是原生 sqlite3,但 Web 端不支持 sqlite3。为了解决这个问题,Actual 使用了 sqlite3 的一个 wasm 版本并创建了一个内存内数据库。
显而易见的问题是持久性。进行更改时,我们需要将其保留在某个位置,以便在用户重新加载时避免丢失数据。所幸我们使用的是基于状态的 CRDT,所有更新都以一个“消息”列表的形式发布。如果用户在线,这些消息将同步到我们的服务器,这样当用户重新加载时,所有数据都应该同步。
不过,每次打开应用时都要求进行大量同步操作并不是理想的选择。另外,如果你处于离线状态,应用就无法承受任何数据丢失的风险。为了解决这个问题,Actual 将每条消息都保留在 IndexedDB 中。当应用程序打开时,它将应用来自本地 IndexedDB 的所有消息以获取最新信息。
要求在加载时应用所有消息也不是理想的选项。这种方法无法扩展——如果用户使用 Actual 已经有好几个月,就会累积成千上万条消息。IndexedDB 会无限增长下去,并且应用加载速度会变得越来越慢。为了解决这个问题,当存储的消息超过阈值时,它会将整个 sqlite3 db 刷新到 IndexedDB 并清除所有消息。
这意味着 sqlite3 db 的一个二进制表示形式和消息列表都保存在 IndexedDB 中。在加载时,应用会从快照创建内存内的 sqlite3 db,并应用 IDB 中剩余的所有消息。
其实,这种方法和预写日志的工作机制很像。
我之前比较担心 IndexedDB 的可靠性。从它的文档来看,似乎浏览器可能会根据需要删除数据库,但实际操作中这种情况似乎没有发生 [注 1]。在存储空间不足的移动设备上这个问题可能会更突出,但我并没有趟移动 Web 这潭浑水(而是用了原生应用)。我还担心应用会到达 IDB 存储的上限,但正如接下来所解释的那样,这并不是个问题。
这项技术起初只是一项实验,但它的效果很惊艳。我在自己的 Actual 应用里有积累 5 年的数据,而它们在 sqlite3 db 中的大小是 9.7MB。消息表的阈值约为 50KB,因此对于一位已经使用 Actual 长达 5 年的用户,我也不过是在 IndexedDB 中存储总共约 10MB 的数据而已。这离 IndexedDB 的最大存储限制还差得远,目前它的上限至少为 500MB 之多。
到目前为止这个办法效果还不错,但是我希望对它建立 100%的信心。我一直在深入研究各种浏览器是如何在磁盘上存储 IndexedDB 数据的,并发现了我可以做出的一些改进策略。我本想在这篇文章中详细介绍一番,但最后我还是把主题放在了整体概述上。在下一篇文章中,我将深入研究 IndexedDB 是如何在浏览器中工作的。
注释
[0] 虽然我在这篇文章中没有谈论这个话题,不过它意味着整个应用都在浏览器中运行。“后端”运行在一个后台 Worker 线程中,并且一切都是在本地运行的。
[1] 如果本地数据真的被用某种方式破坏或删除掉了,那也不是什么大问题。所有更改仍将发送并存储在服务器上(这也是其他设备同步的方式)。如果出现问题,应用可以从服务器重新下载用户的所有数据。唯一会丢失数据的情况是用户在离线状态下丢掉了本地数据,这也是理所当然的。
https://actualbudget.com/blog/porting-local-app-web
点个『在看』支持下