我们为什么使用 Elixir Liveview 重构前端技术栈

Social Layer 致力于为社区开发去中心化的数字基础设施,围绕这个使命一直在进行大量的功能开发,努力满足各种场景的需求。Web3 和 DWeb 技术的开放性、灵活性是我们可以集成更多的协议和数据,建造没有围墙的花园。

随着产品的不断迭代,我们的功能逐渐增多,代码库的大小逐渐增长。由于选择了 React 作为前端框架,页面加载时间渐渐变慢了。除了以太坊钱包登录,还加上邮箱登录,Zupass 登录,JoyID 登录,Solana 登录等支持。另外还加上了其他协议和交互功能的SDK,生成二维码,扫描二维码,生成海报图片,消息通知等等,都需要额外的代码。代码大小变得可观,加载速度也变慢。经过了代码拆分和延迟加载的优化,依然有很多值得改进的空间。

在2023年,我们引入了 Next.js 重构 Social Layer 的前端代码。Next.js 是一个 React 生态的服务端渲染框架,可以让网页组件在服务端进行数据读取、聚合、渲染,跟 React 组件无缝结合。Next 帮助我们解决了网页首屏加载的问题,速度有大幅的提升,至少不需要一开始就加载大量组件。但是 JS bundle 依然需要在后续加载,后面使用 Next 的 server component,让部分组件的 JS 逻辑完全留在服务端,不再加载。

Next.js 14 发布,提供了 server action 的功能,可以直接在前端 JS 组件里面直接写服务端数据查询的代码,引起了 Twitter 上一阵嘲讽,这不就是古早的 PHP 吗?!我们都很熟!前端开发走到尽头,遇到自身难以逾越的障碍,包括性能、开发复杂度、可维护性的挑战,还是靠后端来解决,最后前后端的界限越来越模糊。

Next.js v14 Next.js v14

在现代的应用需求中,丰富的前端交互能力是必不可少的,但 SPA 的复杂性也成为难以承受之重,浏览器不得不加载大量不必要的代码,开发维护复杂性也成倍增加。有没有办法能够实现丰富的交互,同时保持真正的轻量性呢?对于一个需要快速开发迭代产品功能、维护众多业务的小团队来说,这是非常急需的。我们的目光投向了 Elixir,Phoenix 和 Liveview。

Elixir 是一个基于 Erlang 和 Beam VM 的函数式编程语言,有着 Ruby 的语法和灵活性,非常简洁优雅可读。因为吸引了 Ruby 社区的贡献者,很快就有了 Phoenix 这个 Web 开发框架,并且有大量第三方功能库,支持 Web 开发的不同需要。有 Ecto 这样高质量的数据库访问框架,并且少了 ActiveRecord 的很多不必要的魔法。函数式编程风格和不可变的数据结构让系统变得非常容易调试和测试。继承自 Erlang 的一骑绝尘的 Actor Model 和分布式编程模型,更是如虎添翼,足以支持大规模分布式网络应用。更不用说 Erlang OTP 框架的支持让系统健壮性有了极强的保障。

Phoenix 有一个独特的功能,就是 Liveview,用于开发富交互的实时应用。Liveview 的原理可以想像成 React 的 VirtualDOM,只是把 VirtualDOM 放在服务端。每个浏览器的页面都对应一个服务端进程,通过 WebSocket 连接,后端渲染网页的内容,变成一个VirtualDOM,同时HTML推送到前端,用户操作和数据的实时变更都在服务端触发渲染,渲染好的差异部分(diff)会实时推给前端页面,实现实时视图(Liveview)这样的设计允许数据的即时反馈,服务端和客户端的状态保持同步映射,通过一些事件钩子可以让前端操作也即时推送到服务器。对于开发者来说,一切都是普通的HTML模板渲染;在性能上,每次只需要渲染一个页面,更新时只有差异的部分被传输,保持最低限度的资源消耗。虽然每个客户端页面绑定一个服务端进程,但Elixir的进程开销非常低,维护数万数十万的进程不在话下。

Liveview 允许使用纯服务端渲染的开发模式实现丰富的实时前端交互效果,复杂度大大降低,每个页面也只需要维护单个页面的渲染状态,而不是整个程序的渲染状态。React 应用的 SPA 架构则将整个应用程序打包传输到前端,并且重新实现浏览器本身就有的模板渲染,DOM维护,路由,历史,数据加载,session等管理,服务端渲染回归浏览器本身,保持简单。

前后端一体架构的一大好处是减少了很多服务端到客户端之间的通讯成本,业务逻辑内容可以直接从数据库读取,而且不需要拼装多个接口的数据,减少大量序列化/反序列化的操作开销。从服务端进行数据校验,不需要在客户端重写,并且易于维护。最头疼的是访问权限检验逻辑,判断条件众多,需要多轮读取数据,安全上也比较重要,而服务端实现可以保持灵活性和一致性。

Elixir+Phoenix+Liveview 的架构还有一个好处,就是易于测试。前端 SPA 的集成测试是一个非常头疼的问题,当一切运行在浏览器里,还有区块链钱包的集成,测试时就需要启动整个浏览器,注入虚拟钱包,通过复杂的异步操作执行到某个状态,执行时间也非常长。而如果是 Liveview,输出的只是 HTML片段,完全可以通过比较文本来进行测试,Elixir 语言本身的不可变性让测试轻而易举,可以 mock 复杂的数据和运行环境。

Liveview 让后端服务管理客户端数据状态,相当于一个远程的数据绑定,这对数据为中心的应用比较合适,但是很多的页面交互是局部的,跟数据无关。比如下拉菜单,弹出预览窗口,生成图片等等,不需要服务端提供新的数据,只需要本地局部状态的变更,用户也希望更低的延迟。Liveview 提供了一些页面逻辑的JS钩子。对于复杂的可充用交互逻辑,我们会使用 Web Component,这是W3C标准下浏览器支持的通用扩展方式,运行开发者创建自定义的HTML组件,比如 <dropdown /> 菜单,对于服务端渲染页面来说只是一个普通的标签。可以借助 litjs 这样的轻量框架封装 Web Component,也有 shoelace 这样的组件库。这解决了页面交互复杂性的问题,又不引入新的开发复杂性。我们应该利用 Web 本身的扩展能力,而不是自己把应用做成一个浏览器。

Phoenix Liveview 让我们可以回到简洁优雅、高集成度、易于维护的Web开发模式,更紧密的数据集成,减少需要维护的状态,同时提供可扩展性和可伸缩性。更重要的是,开发页面的时候你觉得自己只是在写一个页面,而不是一个程序;搜索引擎和浏览器读取网站地址的时候,也觉得是读取一个页面,而不是一个程序。万维网被设计的时候,以超文本和超链接作为核心,是一个以内容为中心的网络,赋予内容可互动性和可连接性(HTML)。随着 Web2 的发展,为了更好的交互性,人们走向了脚本在前端生成渲染内容的方式,从 content web 走向 scripting web,网站不再是内容而是一个程序。Liveview 这样的开发框架,让我们回到以内容为中心的设计方式,避免管理额外的状态,又不牺牲丰富的交互能力。