同步引擎是 Web 应用程序的未来吗?

2025-05-25

同步引擎是 Web 应用程序的未来吗?

看看下面的 GIF——它展示了一个实时的Todo-MVC 演示,跨窗口同步,并流畅地切换离线模式。虽然它只是一个简单的演示应用,但它展示了每个 Web 开发者都应该了解的重要前沿概念。这是一个Replicache演示应用,我将其从 Express 后端和 Web 组件前端移植到 SvelteKit,以了解其背后的技术和概念。我想与大家分享我的学习心得。源代码可在 Github 上获取。

sveltekit-replicache-演示

背景和动机

Web 应用程序面临着一些根本性的难题,而这些问题似乎被大多数 Web 框架忽略了。这些问题极其棘手,以至于只有极少数应用程序能够很好地解决它们,而这些应用程序在各自的领域中遥遥领先于其他应用程序。

以下是我在实际开发的商业应用程序中遇到的一些此类问题:

  1. 让应用即使在与服务器通信时,即使在网络速度慢或不稳定的情况下,也能保持流畅。这不仅适用于初始加载时间,也适用于应用加载后的交互。SPA是解决这个问题的早期尝试,但最终效果并不理想。
  2. 为用户生成的内容(例如网站建设、电子商务、在线课程构建器)实现撤消/重做和版本历史记录。
  3. 当同一用户在多个选项卡/设备上同时打开应用程序时,该应用程序可以正常工作。
  4. 处理运行旧版本前端的长期会话,用户可能不想刷新以避免丢失工作。
  5. 使协作功能/多人功能正常且近乎实时地运行,包括解决冲突。

我在开发一些非常普通的 Web 应用时遇到过这些问题,没什么特别的,而且我相信大多数 Web 应用在发展过程中都会遇到部分或全部这些问题。
我注意到,开发团队在开始开发新产品时,有一个模式就是完全忽略这些问题,即使团队已经意识到了它们。他们的理由通常是“等真正遇到这些问题时再处理”。然后,团队会选择一些成熟的框架(随便选一个你喜欢的),认为这些工具肯定能解决任何可能出现的常见问题。几个月后,当应用的活跃用户达到一万时,现实就开始显现:团队不得不引入一些不完整的、不完善的解决方案,这会增加复杂性,使系统更加迟缓、错误百出,或者重写核心部分(没有人会在发布后立即这样做)。哎哟。
我感受到了这种痛苦。这种痛苦是真实的。于是,
“同步引擎”应运而生。

同步引擎到底是什么?

还记得我说过,有些应用比其他应用更好地解决了这些问题吗?最近著名的例子是LinearFigma。两者都凭借其领先的技术,颠覆了竞争异常激烈的市场。其他例子包括Superhuman和十年前的Trello。当你研究它们的做法时,你会发现它们都遵循着非常相似的模式,并且都在内部开发了各自的实现方式。你可以在以下链接中了解它们是如何做到的(强烈推荐):FigmaLinearSuperhumanTrello(系列)

在系统的核心,始终有一个同步引擎,充当前端和后端之间的持久缓冲区。从高层次上讲,它的工作原理如下:

  • 客户端始终从引擎提供的本地存储中读取和写入数据。就应用代码而言,它在内存中本地运行。
  • 该存储负责乐观地更新状态,将数据本地保存在浏览器的存储中,并与后端来回同步,包括处理潜在的复杂情况和边缘情况。
  • 后端实现了引擎的另一半,允许拉取和推送数据、在数据发生变化时通知客户端、将数据保存在数据库中等。

同步引擎的不同实现会做出不同的权衡,但基本思想总是相同的。

这不是一个新想法但是...

如果您一直在关注 Web 开发领域的趋势,您就会知道同步引擎已成为其中几个领域的核心,即:渐进式 Web 应用程序离线优先应用程序,以及最近流行的术语:本地优先软件。您甚至可能研究过一些提供内置同步引擎的数据库,例如PouchDb,或提供相同功能的在线服务(例如Firestore)。我也研究过,但过去几年我的总体感觉是,它们都没有切中要点。渐进式 Web 应用程序是关于用户在主屏幕上“安装”网站快捷方式,就像它们是原生应用程序一样,尽管无需安装可能是 Web 的“唯一”好处。“离线优先”听起来好像离线模式比在线模式更重要,但对于 99% 的 Web 应用程序来说,情况并非如此。 “本地优先”无疑是迄今为止最好的名称,但官方的“本地优先”宣言讨论的是点对点通信和CRDT(这是一个非常酷的想法,但除了协作文本编辑之外很少用于其他用途),在一个充斥着全客户端-服务器 Web 应用程序的世界里,这些应用程序正试图解决像我上面描述的那样的实际问题。讽刺的是,许多参与当前“本地优先”浪潮的工具都采用了这个名字,但并没有采纳所有原则。

其中最吸引我注意和兴趣的是“Replicache”。具体来说,我之所以对它感兴趣,是因为它并非一个自我复制的数据库,也不是一个需要你围绕它构建整个应用的黑盒SaaS服务。相反,它比我在这个领域遇到的任何现成解决方案都提供了更强的控制力、灵活性和关注点分离。

Replicache 是什么?

Replicache 是一个库。在前端,它几乎不需要任何编程,可以像普通的全局存储(类似 Zustand 或 Svelte 的存储)一样高效地运行。它拥有大量的状态(在我们的示例中,每个列表都有自己的存储)。它可以使用一组用户自定义函数(称为“mutators”(类似 Reducers))进行修改,例如“addItem”、“deleteItem”或任何你想要的函数,并且公开了一个订阅函数(我做了简化,完整 API见此处)。

在这个熟悉的界面背后是一个强大且高性能的客户端同步引擎,它可以处理:

  1. 首次将相关数据完整下载到客户端。
  2. 将“变更”拉取和推送到后端。变更是一个事件,它指定了应用了哪个变更器以及使用了哪些参数(以及一些元数据)。
    • 推送时,这些更改会在客户端乐观地应用,如果在服务器上失败,则会回滚。任何其他待处理的更改都将在最上面应用(即变基)。
    • 同步机制还包括连接丢失时的排队更改、重试机制、以正确的顺序应用更改以及重复数据删除。
  3. 将所有内容缓存在内存中(性能)并将其保存到浏览器存储(特别是 IndexedDB)进行备份。
  4. 由于可以从同一应用程序的所有选项卡访问相同的存储,因此引擎会处理所有相关问题 - 例如当模式发生变化但某些选项卡已刷新而某些选项卡尚未刷新并且仍在使用旧模式时该怎么做。
  5. 使用广播频道立即保持所有标签同步(因为依赖共享存储不够快)。
  6. 处理浏览器决定清除本地存储的情况。

你可能已经注意到,这解决了我在文章开头列出的大部分问题。基于突变的特性也使其具备了撤销/重做等功能。

为了使所有这些功能正常工作,后端需要实现 Replicache 定义的协议。具体来说:

  1. 您需要实现推送拉取API。这些端点需要能够像前端一样激活变量器(尽管它们不必运行相同的逻辑)。后端具有权威性,冲突解决由变量器实现中的代码完成。
  2. 您的数据库需要支持快照隔离并在事务内运行操作。
  3. Replicache 客户端会定期轮询服务器以检查更改,但如果您希望客户端之间实现接近实时的同步,则需要实现一种“poke”机制,即通知客户端某些内容已发生更改,需要立即拉取。这可以通过服务器发送事件websockets来实现。这是一个有趣的 API 设计选择——更改永远不会推送到客户端;客户端始终会拉取它们。我相信这样做是为了简化系统并使其易于推理。有一点是肯定的:他们没有强制使用 websockets 是件好事,因为那样会使协议与 HTTP(服务器发送的事件通过普通的 HTTP 连接进行流式传输)不兼容,从而需要额外的基础设施并带来额外的集成挑战。
  4. 根据版本控制策略,您可能需要实现其他操作(例如,createSpace)。

如果你觉得这听起来不简单,那你是对的。我觉得我还没完全理解它与数据库交互的所有细节。我需要做一个后续项目,彻底重构数据库结构,并/或在示例中添加一些有意义的功能(例如版本历史记录),以便更接近完全理解它。关键是,我知道这种程度的控制在构建和维护实际生产应用程序时有多么重要。在我看来,如果能为构建和扩展奠定坚实的基础,那么花一两周时间深入思考和设置应用程序的核心部分是一项非常值得的投资。

移植一个重要的例子

学习任何新东西的最佳(或许也是唯一的)方法就是亲自动手——亲身实践,体验一些会影响真实应用的利弊权衡和影响。当我浏览Replicache 网站上的示例时,我注意到没有 Sveltekit 的示例。自从 Svelte 3 发布以来,我就一直是 Svelte 的忠实粉丝,但直到最近才开始使用 Sveltekit。我认为这将是一个绝佳的机会,可以边做边学,同时创建一个有用的参考实现。

将现有的代码库移植到不同的技术平台很有教育意义,因为在翻译代码的过程中,你会被迫去理解它,并质疑它。在整个过程中,我经历了多次顿悟,一些最初看似奇怪的东西突然就迎刃而解了。

学习内容

斯维尔特基特

  1. Sveltekit本身不支持 WebSockets,尽管它支持服务器发送事件,但方式很笨拙。Express 对两者都支持。因此,我使用svelte-sse来处理服务器发送事件。我遇到的一个有点烦人的怪癖是,由于 svelte-sse 返回一个 Svelte 存储,而我的应用程序并未订阅该存储(应用程序不需要读取该值,只需像我上面描述的那样触发拉取),所以整个事情都被编译器优化掉了。我最初很困惑为什么消息没有发送过来。我最终不得不为该行为实现一种解决方法。我不怪这个库的作者;他们认为会向客户端发送一个有意义的值,但“poke”并非如此。
  2. SvelteKit 基于文件系统的路由、加载函数、布局和其他功能,与原版 Express 后端相比,使得代码库更加井然有序,样板代码更少。毋庸置疑,在前端,Svelte 远远领先于 Web 组件,这使得前端代码库在功能更丰富的情况下,更加精简、可读性更强(原版 TodoMVC 缺少“全部标记为完成”和“删除已完成”等功能)。
  3. 总的来说,我很喜欢 Sveltekit,并计划在未来继续使用它。如果你还没有尝试过,官方教程是一个很棒的入门指南。

复制缓存

总的来说,Replicache 给我留下了非常深刻的印象,强烈推荐大家尝试一下。在基础层面上(目前为止我尝试过的所有功能),它运行良好,兑现了所有承诺。话虽如此,以下是我遇到的一些常见问题(与待办事项应用无关)以及相关想法:

  • 与性能相关:
    • 当有大量数据需要下载(例如数十MB)时,初始加载时间(首次加载,即任何数据被拉取到客户端之前的加载时间)可能会很长。对于用户在初始加载后会花费大量时间的生产力应用来说,这种加载时间不太敏感,但仍然需要注意。潜在的缓解措施:部分同步(例如,Linear 只发送未解决的问题或过去一周内已关闭的问题,而不是发送所有问题)。
    • 网络通信(?) ——起初,我感觉客户端和服务器之间来回有大量的通信,各种推送、拉取和戳取调用四处飞舞。深入研究后,我意识到我的直觉是错误的。通信确实很频繁,但由于变更非常紧凑,戳取调用也很小(没有负载),所以它比普通的 REST/GraphQL 应用要小得多。此外,浏览器完全重新加载(刷新按钮或在关闭页面后在新选项卡/窗口中再次打开页面)会从浏览器存储中加载大部分数据,只需要从服务器拉取差异数据,这就引出了我的下一个观点。
    • 长时间离线后恢复:我还没有测试过这个问题,但这似乎确实令人担忧。如果我离线工作了几天,在团队在线的情况下进行更新并进行更改,会发生什么情况?当我恢复在线时,我可能会有大量的差异需要推送和拉取。此外,冲突解决可能会变得非常困难。对于每个具有离线模式的协作应用程序来说,这都是一个问题,并非 Replicache 独有。Replicache 文档对这种情况提出了警告,并建议实施“历史记录概念”作为潜在的缓解措施。
    • 那么打包文件大小呢?Replicache压缩后大小为 34kb,考虑到实际使用体验,这绝对是物超所值。
    • Replicache 网站上的这个页面让我认为,在一般情况下,性能应该非常好。
  • 功能相关:
    • 与原生移动或桌面应用不同,用户可能会丢失其工作的本地副本,因为浏览器的存储无法提供与设备文件系统相同的保障。浏览器可以决定在特定条件下删除应用的所有数据。如果用户在线,并且有一些工作尚未推送到服务器,那么在这种情况下,这些工作就会丢失。再次强调,这个问题并非 Replicache 独有,所有支持离线模式的 Web 应用都会受到影响。根据我的了解,它不太可能影响大多数用户。这只是需要注意的一点。
    • 令我惊讶的是,我移植的 Todo 示例中后端数据库的模式并没有我期望 SQL 数据库所具备的“正确”关系定义。它没有包含“id”、“text”或“completed”字段的“items”表。我希望它存在的原因与我最初想要一个关系数据库的原因相同——能够轻松地对系统中的数据进行切片和切块(以前我总是因为没有关系数据库而忽略了这一点)。我认为这不是什么大问题,因为只要协议按照规范实现,Replicache 就应该是与后端无关的。我可能会尝试重构数据库作为后续练习,看看这在复杂性和人机工程学方面意味着什么。
    • 我发现版本历史记录和撤消/重做功能在用户可编辑内容的应用中非常有用且值得考虑。关于撤消/重做,官方有一个软件包,但似乎缺乏对多人游戏用例的支持(这正是问题的根源)。至于版本历史记录,Replicache 文档提到了“历史记录的概念”,但建议如有需要可以咨询他们。这让我觉得实现起来可能并不容易。这是后续任务的另一个想法。
    • 协作文本编辑- 现有的冲突解决方法对于需要CRDTOT 的协作文本编辑不太适用。我很好奇将 Replicache 与Yjs之类的工具集成起来有多容易。官方有一个示例代码库,但我还没有研究过。
  • 与缩放相关:
    • 由于服务器是有状态的(保持 HTTP 连接开放以接收服务器发送的事件),我很好奇它的扩展性如何。我之前开发过超过 10 万用户的生产系统,也使用过 WebSocket,所以我知道这没什么大不了的,但还是值得考虑一下。
  • 其他:
    • 理论上,Replicache 可以添加到现有应用中,而无需重写前端(只要应用已经使用了类似的存储)。后端可能比较棘手。如果你的数据库不支持快照隔离,那你就没那么幸运了;即使数据库支持,现有架构和现有端点也可能需要进行大量改造。如果你打算使用它,最好从第一天就开始着手(如果可以的话)。
    • Replicache尚未开源(目前为止!请参阅下文),并且只有在小型或非商业的情况下才是免费的。考虑到开发它所投入的工作量(超过 2 年)以及所展示的工程质量,这似乎是公平的。话虽如此,与选择免费的开放库相比,采用 Replicache 更像是一种承诺。如果您是二级及以上的付费客户,您将获得源代码许可证,这样如果 Replicache 由于某种原因关闭,您的应用程序是安全的。另一种选择是推出您自己的同步引擎,就像大公司(Linear、Figma)所做的那样,但要达到 Replicache 提供的质量和性能绝非易事。
    • 剧情反转(最后一刻修改):就在我准备发布这篇文章的时候,我发现 Replicache 即将开源,而且它的母公司计划推出一款名为“Zero”的新同步引擎。官方公告如下:“我们将开源 Replicache 和 Reflect。一旦 Zero 准备就绪,我们将鼓励用户迁移。” 讽刺的是,Zero 似乎又是一个自动同步后端数据库和前端数据库的解决方案,至少对我个人而言,这似乎不太吸引人(因为我希望实现关注点和控制点的分离)。话虽如此,这些人都是这个领域的专家,而我只是个网民,所以我们只能拭目以待。与此同时,我打算继续使用 Replicache。

是否所有事情都应该使用同步引擎?

不,同步引擎不应该用于所有功能。好消息是,您可以让应用的某些部分使用它,而其他部分仍以常规方式提交表单并等待服务器响应。SvelteKit 和其他全栈框架使这种集成变得简单。
以下情况显然不适合使用同步引擎:

  1. 只有当客户端更改成功的可能性很高(回滚很少发生)且客户端拥有足够多的信息来预测结果时,乐观更新才有意义。例如,在在线测试中,学生的答案必须发送到服务器进行评分,这时乐观更新(以及同步引擎)就不可行。这同样适用于诸如下单或股票交易等关键操作。一个好的经验法则是,任何依赖于服务器且无法离线运行的操作都不应依赖同步引擎。
  2. 任何处理海量数据集的应用都无法在用户设备上顺利运行。例如,创建一个本地优先的谷歌应用或一个处理千兆字节数据以生成结果的分析工具都是不切实际的。然而,在部分同步即可满足需求的场景下,同步引擎仍然大有裨益。例如,谷歌地图可以下载地图并将其缓存在客户端设备上,以便离线运行,而无需始终获取全球所有地点的高分辨率地图。

关于开发人员生产力和 DX

我认为同步引擎可以让开发者体验 (DX) 变得更好。前端工程师只需使用一个可以订阅更新的普通存储,UI 就能始终保持最新。无需考虑获取任何数据、调用 API 或由同步引擎管理的应用部分执行服务器操作。至于后端,我目前还不能透露太多。看起来它不会比传统后端更难,但我不敢肯定。

总结

想象一下,Web 应用的未来将会是规模庞大、实时多人协作的工具,无论网络状况如何都能可靠运行,同时还能让我在本文开头提到的那些棘手问题成为过去。
我强烈建议 Web 开发者们熟悉这些新概念,尝试一下,甚至贡献一份力量。
感谢您的阅读。如果您有任何问题或想法,请留言。祝您一切顺利

附言: Replicache 创始人 Aaron Boodman 的
这段采访太棒了。你先看看,之后再感谢我。

文章来源:https://dev.to/isaachagoel/are-sync-engines-the-future-of-web-applications-1bbi
PREV
如何找到开源项目并做出贡献
NEXT
使用 Prisma、Supabase 和 Shadcn 设置 Next.js 项目。