Phoenix LiveView 简介
这是我在 20200516 Elixir Meetup 线上活动 里介绍 Phoenix LiveView 所用的讲稿。 Slide PDF 版本: 202005 Elixir Meetup - Phoenix LiveView
Demo
刚才陈天老师为我们分享了当 Elixir 有不足的时候,该如何引入其他的语言(Rust)进行补足。而我的分享可能刚好相反,是通过 Elixir 的强处,来实现一些其他语言难以实现的效果。希望能让大家体会到 Elixir 的优美和高效。
今天的主角是 Phoenix LiveView。 Phoenix LiveView 是由 Phoenix 官方团队在 2019 年 3 月正式推出的后端渲染框架。它所要解决的问题是:让开发者们用最小的成本,开发出具有实时交互体验的 Web 应用。那什么是「具有丰富的实时交互体验的 Web 应用」呢?让我们先来看两个 Demo:
数独
第一个 Demo 是一个数独解题器:用户可以在这个 9x9 的表单中填入数字,点击 "Start" 进行解题,并能实时地看到解题算法的每一步更新。更厉害的是,用户可以通过滑块实时地控制解题算法更新的间隔。而这些功能,都是用 Elixir 通过 Phoenix LiveView 加上一些简单的 HTML 和 CSS 实现的。
这个 Demo 是一个 Twitter 克隆。从这个录屏中,我们可以看到,在一个客户端中创建的新推文,被实时地推送到了另外的客户端上。这些功能,就是通过 Phoenix LiveView 在 15 分钟内迅速实现的(不包括前端样式部分)。
Build a real-time Twitter clone in 15 minutes
Agenda
看过了这两个 Demo,相信大家都能体会到 Phoenix LiveView 的强大。那就让我们正式进入今天的主题吧!
What's in this talk
今天的分享主要包含三个部分:
- LiveView 要解决什么问题?通过回答这个问题,我们可以明确 LiveView 的定位,理解它背后的设计逻辑。
- LiveView 把这个问题解决了吗?通过这个问题,我们可以知道 LiveView 的优势与劣势,帮助我们决定是否要学习、使用 LiveView。
- LiveView 是怎么解决这个问题的?最后,我们会一起看看 LiveView 背后的实现机制,更好地理解这个强大的框架。
- LiveView 要解决什么问题?
- LiveView 把这个问题解决了吗?
- LiveView 是怎么解决这个问题的?
What's not in this talk
另外,这次分享并不会介绍如何使用 Phoenix LiveView。大家可以参考官方和社区提供的教程。
怎么使用 Phoenix LiveView
LiveView 要解决什么问题?
LiveView 希望解决的问题其实正是 Phoenix 框架本身在开发之初所要解决的问题:实时的用户交互体验。
实时的用户交互体验
- 在 Web 技术刚出现时的 Web 1.0 时代,通过 HTTP 请求网页,显示静态的文字、图片内容,已经能够满足用户的需求。
- 而在 Web 2.0 时代,GMail 等使用 AJAX 技术的应用不断涌现。这些应用在静态页面的基础上,动态地请求服务器的数据,及时地将数据变动更新到用户端。
- 现在,JavaScript 及其框架不断更新迭代,用户对数据实时性、界面交互性的要求越来越高。提供一个实时的、高度交互的用户体验成为了大部分 Web 应用的基本要求。
- 在搜索时,我们不再满足于一个空荡荡的搜索框,还希望这个搜索框能够补全要搜索的内容。
- 在填写表单时,我们不再满足于在提交表单之后再收到错误提示,而希望在输入时就得到对应的反馈。
- 在聊天时,我们谁也不会通过刷新页面来获取新的消息,而是希望新消息实时地出现在屏幕上。
我们可以看一些具体的例子演示。 (Demo chrismccord/phoenix_live_view_example in Chrome)
Weather
这是 Phoenix LiveView 官方提供的一些样例。
在主页里我们可以看到 LiveView 最基本的应用:天气查询。
这里对天气信息的更新全是在 Elixir 里实现的,我们不需要写一行 JavaScript 代码。
Search
这是另一个样例:词典搜索。
用户在输入时,LiveView 会实时地给出单词的补全。在查询时,LiveView 会请求第三方词典 API,获得单词的解释之后,再将其渲染到客户端。
(Demo)
值得注意的是,在请求第三方客户端时,LiveView 会显示「Searching」,让用户得到适当的反馈。这对于一些后端比较耗时的操作来说,是很有必要的。
Form
第二个例子是一个实时表单。
当用户在表单里填入信息时,LiveView 会实时地给出错误提示。这样一来,用户就不必等到提交表单之后才知道自己的 Email 格式不正确,或者已经被人使用了等错误,避免了重复提交表单的繁琐。
- 搜索框自动补全
- 表单实时验证
- 实时聊天
已有的解决方案
但是,现有的解决方案都不尽如人意。为了实现复杂的实时交互体验,开发团队需要投入大量的时间成本、人力成本开发、维护前后端的应用。交互体验与开发成本之间,似乎构成了一种「鱼和熊掌不可得兼」的对立关系。我们可以先对已有的解决方案进行简单地对比。
单页应用(Single Page Application)
首先,当下最火热的解决方案就是单页应用框架。
它们能够实现最复杂的页面交互效果、逻辑。用户甚至可以在下载了应用之后,离线使用。最典型的例子是 Google Docs 和 Figma 这类协作创造工具。(Google Docs 并不能离线使用,Figma 离线时可使用的功能也是受限的)
但是,实现这些交互的代价可能是巨大的。前后端的分离意味着开发者需要使用两套开发语言、框架,开发效率较低。同时维护前端应用和后端 API 也带来了极高的维护成本。
另外,这个方案对搜索引擎的爬虫并不友好,为了搜索引擎优化(SEO)可能需要服务端渲染(SSR)等额外的技术开销。
- 应用
- Google Docs
- Figma
- 优势
- 可以实现最为复杂的交互、动画效果
- 可以离线使用
- 劣势
- 开发效率低下
- 对 SEO 不友好
AJAX 应用(服务端 API + jQuery)
另外,我们也有比较传统的解决方案:在 Ruby on Rails, Phoenix 这些服务端渲染框架的基础上,在 HTML 页面渲染之后,通过 jQuery 等 JavaScript Library 发起 AJAX 请求,接收到 JSON 返回之后更新用户界面,达到实时交互的效果。
这种方案的部署、维护成本较低,初期的代码复杂度比较低,开发效率较高。但是当应用对实时性、交互性的要求逐渐增加,前端代码的维护成本也是极高的,这也是 React, Vue.js 这些前端框架火热的原因。另外,用较低的代码复杂度换来的也只是少许的实时交互效果,一些复杂的需求(比如,服务端向客户端实时推送数据)就很难实现。而应用页面之间的切换往往还是依赖一次完整的页面请求,相比 API 请求会传输更多的数据,加载速度更慢,用户体验差。
- 例子
- Ruby on Rails
- Phoenix (without LiveView)
- 应用
- Gmail
- 优势
- 初期代码复杂度相对较低,开发效率较高
- 对 SEO 友好
- 劣势
- 实时性弱,数据更新往往需要通过刷新页面来获取
- 请求速度慢(每次刷新都重新获取数据)
介于两者之间的解决方案
当然,在单页应用和 AJAX 应用之间也已经有了许多不同的解决方案。
像 Basecamp 就在 Rails 的基础上开发了 Turbolinks 技术,通过传输 HTML partials 在 AJAX 应用的基础上实现了类似单页应用的效果。
但是,这类已有的解决方案要么存在着性能问题,不适合大规模使用,要么就是在开发效率和实时性之间做了取舍。鱼和熊掌难以兼得。
- 例子
- Turbolinks
- Stimulus
- 应用
- Basecamp
- 开发效率与实时性均在单页应用和 AJAX 应用之间
更快速的开发+更实时的交互
我们可以用这么一张图总结。
- 横坐标代表的是应用交互体验的实时性,从左到右逐渐加强。
- 纵坐标代表的是应用的开发成本,从下到上逐渐增加。
已有的解决方案分布在从左下到右上的对角线上。而 Phoenix LiveView 的目标在右下角。
也就是说,如何快速地、低成本地开发出带有实时交互体验的 Web 应用,并且满足生产环境的性能、SEO 需求,就是 Phoenix 和 Phoenix LiveView 希望解决的问题。
LiveView 把问题解决了吗?
讨论完了 LiveView 的目标,我们现在可以看看在发布了一年之后,LiveView 把这些目标完成得怎么样,具备了哪些优势,还存在哪些劣势。
LiveView 的优势
Phoenix LiveView 因为站在 Erlang OTP, Elixir, Phoenix 这些巨人们的肩膀上,在出生伊始,就具备了其他解决方案无法比拟的四大优势:
- 满足大部分实时交互需求
- 高效的开发体验
- 极致的性能
- SEO 友好
满足大部分实时交互需求
首先,LiveView 目前已经能够支持大部分 Web Application 中需要实时交互的场景。
除了满足基本的 CRUD 操作更新、服务端推送数据更新之外, LiveView 还提供了 JavaScript Hook functions 满足更加定制化的需求(比如:无限滚动加载、DOM 元素更新时的动画效果、与 JavaScript 第三方库的交互 等等)
- CRUD 操作的实时更新
- Twitter Timeline
- 服务端主动推送数据更新至客户端
- Phoenix Live Dashboard
- 提供 JavaScript Hook functions 进行定制
- Infinite scroll
高效的开发体验
其次,LiveView 应用的开发是极其高效的,之前展示的所有例子,核心代码都不超过 100 行。
这里展示的是「天气查询」的 LiveView component 代码,开发者只需要负责实现核心业务的部分:
render
: HTML template- LiveView component callbacks
mount
: component 的初始化handle_event
: 用户事件的处理逻辑
weather
: 后端业务逻辑
而我们作为开发者不用考虑该如何设计后端 API 的请求路径、返回数据结构,如何追踪数据的变化,如何通知客户端数据的更新,如何在前端接收 API 返回、解析、更新 DOM 结构等非核心业务逻辑, LiveView 帮我们打理好了这一切,并且比我们大多数人设计得更好。
这也正是 LiveView 为什么能如此显著地提高开发效率的原因: 开发者只用关心核心业务逻辑
def render(assigns) do ~L""" <div> <form phx-submit="set-location"> <input name="location" placeholder="Location" value="<%= @location %>"/> <%= @weather %> </form> </div> """ end def mount(_params, _session, socket) do {:ok, assign(socket, location: "Shanghai", weather: weather("Shanghai"))} end def handle_event("set-location", %{"location" => location}, socket) do {:noreply, put_location(socket, location)} end defp put_location(socket, location) do assign(socket, location: location, weather: weather(location)) end defp weather(local) do {:ok, {{_, 200, _}, _, body}} = :httpc.request(:get, {~c"http://wttr.in/#{URI.encode(local)}?format=1", []}, [], []) IO.iodata_to_binary(body) end
极致的性能
除去高效的开发体验之外,LiveView 另外一个出彩的地方就是它出色的性能。
正如刚才提到的,LiveView 帮我们实现了对状态的管理、变化追踪、同步,它在这些功能的实现上都是极其高效的:
- LiveView 在渲染模版时会区分出静态的模版内容和可能动态变化的内容;静态的内容不会随着数据变化或者用户的操作而变化,因此 LiveView 只会在初次渲染时将静态内容传输到客户端;之后的数据更新都只包含动态变化的部分
- 而且在传输动态数据的更新时,LiveView 只会传输需要更新的数据,前端再结合已有的静态模版,更新 DOM 结构。
- 最后,LiveView 目前使用 JSON 与前端通信,之后计划增加对 Erlang Term Format 这种数据格式的支持,提高服务端的负载能力,并且进一步减少更新时的数据包大小。
而在可扩展方面,因为有着 Erlang OTP, Elixir, Phoenix 这些得天独厚的优势,无论是纵向扩展还是横向扩展都没有问题。
- 纵向扩展方面,Phoenix Channel 在一台 40 核 128GB 内存的服务器上可以支撑 2000000 的 WebSocket 连接服务。
- 横向扩展方面,Erlang OTP 可以连接多台机器组成集群。
- diff 机制高效,且仍有优化空间
- 区分静态内容与动态内容
- 只传输变化的数据 diff
- JSON -> Erlang Term Format (ETF)
纵向扩展有 Phoenix Channel 做保障
- 横向扩展有 Erlang OTP 做保障
SEO 友好
最后,因为 LiveView 本身就运行在一个 Phoenix 应用中,给爬虫渲染 HTML 这个原本并不复杂的任务根本不在话下。
LiveView 会在请求开始的首次渲染时返回完整的 HTML response。这样,无论是浏览器还是爬虫,都能正常地拿到要渲染的数据。
之后,LiveView 才会通过 JavaScript 调用 WebSocket 启动 LiveView 生命周期,保证浏览器里数据的正常更新。
因此,使用 LiveView 并不会对 SEO 有任何影响,因为这本就是一个服务端渲染的页面。
- 首次渲染默认返回完整的 HTML response
- HTML 渲染结束之后再建立 WebSocket 连接启动 LiveView 生命周期
LiveView 的劣势
说完了 LiveView 的优势,LiveView 当然也有着它的缺陷与劣势。这些劣势主要都是源于 LiveView 对 WebSocket 的依赖。
LiveView 需要 WebSocket 才能正常接收数据更新,因此即使用户把页面缓存之后,在离线状态下也是无法正常使用的。
而如果网络的延迟很高,对 LiveView 应用的用户体验也会有很明显的影响。如果需要实现 CSS 动画或者多人游戏这类对低延迟、高帧率有要求的功能时,LiveView 也显然不是合适的选择。
在此,我必须要为 LiveView 稍作开脱。毕竟,又有多少应用、框架能够实现这个受到物理世界因素限制的需求呢?大部分应用也只是通过缓存、过场动画等用户体验上的改善,让用户体会不到网络延迟的存在,而这些改善用 LiveView 也同样能够实现。而且,LiveView 团队已经在开发相关的功能尝试解决这些问题:
- 在断线状态对客户端更新进行缓存,连线之后上传更新。
- 提供开发环境下的延迟模拟功能,供开发者调试。
- 无法在离线状态下正常使用
- 复杂的本地应用(例:Figma)
- 用户体验容易受到延迟影响
- 游戏
- 动画效果
LiveView 是如何实现的?
那么,如此强大的 LiveView 是如何实现的呢?
- 首先,浏览器通过 Phoenix Channel 与服务器建立 WebSocket 连接。之后会在
phx-*
对应的事件发生后发送 WebSocket 请求至后端服务器。 - 后端服务器的 Phoenix LiveView Channel GenServer 接收到请求之后,会调用对应的 LiveView module 中的
handle_event/3
callback。 - 我们在应用中定义的 LiveView module 对数据状态进行更新,以 tuple 返回值的形式交还给 LiveView Channel GenServer。
- 数据状态更新完成之后,LiveView.Channel 调用 LiveView.Diff 生成 diff 数据。
- LiveView Channel GenServer 推送 diff/redirect/render 更新到客户端。
- 客户端接收数据之后,通过 morphdom 这个 JavaScript library 对 DOM 结构进行更新,并且调用对应的 JavaScript hooks。
这是 LiveView 底层实现细节的一个简单介绍,具体的细节大家可以阅读 phoenix_live_view/channel.ex 中的源码。