Phoenix LiveView 到现在仍常被压成两句半真口号:一边说它让团队告别 JavaScript,一边说它只是借 websocket 把单页应用偷偷带回浏览器。Chris McCord 在 ElixirConf 2022 的 Phoenix + LiveView 更新演讲有价值,正因为他把讨论拉回更窄、更耐用的工程命题:UI 的调用约定留在服务器端组件里说明,浏览器负责渲染、接收 patch,以及一小组有明确职责的交互动作。[1]
当前文档也支持这条读法。LiveView 的 welcome 指南把 LiveView 写成一个进程:接收事件,更新 state,再渲染 diff;第一次页面输出仍是普通的 server-rendered HTML,随后才交给持久连接接续。[2] 组件文档继续说明,attr/3 与 slot/3 声明会给 function components 带来编译期校验与显式输入约定,也能收进 phx-*、aria-*、data-* 这类全局 HTML attributes。[3] Phoenix.LiveView.JS 文档补上最后一块:客户端工具要和 DOM patch 配合,所以浏览器端动作可以跨过后续 server patch 保留效果,而不会分叉出另一套应用模型。[4]
把演讲和文档合起来看,LiveView 押注的核心很清楚:客户端不要再持有第二份 UI 契约。[1][2][3][4][5][6] Phoenix 希望组件写明自己接收什么,generators 交付可复用的调用部件,浏览器运行时小到足以让 server re-render 继续成为权威来源。这比“不要 JavaScript”的贴纸说法更像一个扎实的工程判断。
配图说明:题图使用 Chris McCord 的真实 ElixirConf 讲者肖像。这张图合适,因为本文围绕的是维护者本人对 Phoenix 与 LiveView 设计方向的解释,重心落在职责分配,而不在装饰性的浏览器截图或泛化的代码编辑器照片。[7]
大约从 9:00 开始,组件从 snippets 转向可检查的调用约定
视频里第一段真正决定理解方向的内容,出现在 McCord 讲 function components 的 declarative attributes 与 slots 时。[1] 他展示的重点落在一种能同时被 compiler 与 editor 读懂的组件调用方式上,语法清爽只是表层收益。演讲中那个 table 风格的 component,既能声明 required attributes,也能借 slots 接收任意 markup,还能在 runtime 之前暴露调用错误。[1] 这件事重要,是因为旧式模板组合常把真正的约定埋在说明文字、团队习惯或示例里,只有出错后才变得精确。
组件文档把这层意思写得更正式。attr/3 允许组件声明自己期待哪些 attributes,哪些必须传入,并在调用方破坏约定时发出 compile-time warnings。[3] slot/3 把同样的办法带到 HEEx content blocks 上,包括 required inner blocks 与 slot 自身的形状限制。[3] 有了这套工具,component call 就不再只是“某个最好记得怎么用的模板 helper”,而开始接近一种带类型感的调用面,即便最终输出仍然是 HTML。
最能说明问题的是 global attributes。McCord 特别强调,调用方应该能继续传入 accessibility attributes、phx-click 一类绑定,以及相关 markup,组件作者用不着手工重写整个 HTML 世界。[1] 文档把同一设计写得更明确:声明 :global 属性后,组件可以接收标准 HTML attributes 与默认的 phx-、aria-、data- 前缀,同时其余输入仍保持显式。[3] 这项设计处理的不是小便利;Phoenix 借它避免开发者在僵硬组件 API 与散乱 markup 池之间二选一。
演讲反复回到 compile-time niceties,原因也在这里。warnings 本身不是终点;真正的变化,是 markup call 被编译器看见以后,LiveView 在团队里更容易扩大使用。原本藏在 controller views、helper functions 或复制片段里的暗约定,会被抬到调用处。能让编译器检查的组件契约,已经走到 design system 的半路,即使现场还没有人把这个词说出口。
大约从 20:07 开始,McCord 把 LiveView 讲成一条去掉翻译层的事件回路
演讲中段有一段话,最能解释 LiveView 和普通前端堆栈的差异。McCord 先摆出常见路径:服务器命名契约,序列化出去,浏览器再重建 client-side models、push 行为与各种数据层;随后他把 LiveView 放到更直接的事件与渲染回路里。[1] 他真正反对的是重复。如果服务器已经握着权威 state transitions,大量常规产品工作还要再维护一份 JSON serializers、GraphQL schemas、client stores 与手写 websocket choreography,只是在把同一件事再描述一次。
welcome 指南把这套想法落成操作模型。LiveViews 是接收 events、更新 immutable state、再把相关 HTML 片段以 diffs 推出的进程。[2] 首屏仍走普通 HTTP,这对 initial paint 与索引更自然;之后持久连接让后续更新变轻。[2] 这和经典 SPA 的平衡点不同。浏览器依然活跃,只是默认不再为每个交互长期持有一份独立的业务状态模型。
把 LiveView 读成 no JavaScript,会漏掉重点。LiveView 没有删除客户端,它收窄了团队必须先搭专用前端应用的范围。McCord 把 LiveView 对 HTTP 栈的影响,类比成 utility CSS 对前端样式流转的影响时,重点落在少维护一层命名与翻译。[1] 在许多日常产品页面里,服务器和浏览器之间不用持续互译两套模型;主事件回路留在一处,浏览器专心处理 patch 应用与即时交互。
这份分工也解释了为什么 LiveView 一旦被迫模仿厚客户端工具,优势会很快变钝。它省下来的,是重复描述状态与界面的工程税。团队若按旧习惯重新搭一套平行 client model,LiveView 最有辨识度的好处会先被自己抵消。
大约从 25:00 开始,core_components.ex 暴露出 Phoenix 更强的野心:交付可迁移的调用约定,而不仅是默认 markup
McCord 讲 generated components 那一段,很容易被看成样式故事,因为屏幕上的 Tailwind 很显眼。[1] 更重要的主张藏在样式之下。Phoenix 1.7 的 generators 开始输出一组以调用约定为核心的 reusable components,例如 header、simple_form、input 与 modal,并把它们收进 core_components.ex,这样 scaffolded code 依赖的就是稳定的 component calls,而不是铺开的、一次性的 markup。[1][5] 官方 release post 用 Phoenix 自己的语言说了同一件事:generators 使用一组 core UI components,团队可以替换这些函数的具体实现,同时保留 generators 本身带来的价值。[5]
这一步很细,但分量很重。很多技术栈里的 generated UI,一旦团队换掉设计语言,就会迅速变成可丢弃代码。McCord 给出的方向相反。如果 generators 输出的是连贯的 component calls,而不是写死的 page fragments,Phoenix 在项目第一周之后仍能继续帮忙。[1] 把底层实现换成 Bulma、Bootstrap,或者团队自己的 house style,外层调用仍说同一种话。[1][5] Nimble 对 Phoenix 1.7 的总结,把它称作 controller 与 LiveView 代码之间统一的 HTML rendering approach,换成工程语境,就是 Phoenix 想减少同一应用内部的模板方言漂移。[6]
到这里,组件契约从优雅概念进入维护日常。稳定 call site 有实实在在的价值:团队修改样式栈、无障碍默认值或 markup conventions 时,可以集中改一层组件函数,而不用逐页拆开 generator 输出。Phoenix 交给你的不只是一套默认外观,它还把可替换的位置留了出来。
顺着这个角度看,core_components.ex 不是 starter-kit 糖衣。它是 Phoenix 对长期杠杆位置的一次下注。真正的杠杆,在于让 UI surface 足够 declarative,使 generators、团队代码与社区组件能在同一套调用方式上会合。只要这件事成立,“generated code”就不再天然等于“项目一开始就要扔掉的代码”。
大约从 39:00 开始,受限客户端被讲清楚:connection states 与 JS commands 都按 patch-safe 规则行动
视频后段的 generated connection component,是浏览器职责最容易看见的一段。[1] McCord 展示了一个带有 disconnected、connected 与 loading slots 的 component,并指出客户端一侧真正做的事情很薄:客户端判断当前处于哪种连接状态,component 只负责渲染对应 slot 的内容。[1] 这几乎就是 LiveView 更大设计的一张缩略图。浏览器当然有工作,但那份工作被收进服务器拥有的组件契约里。
Phoenix.LiveView.JS 文档把这条限制写得更清楚。JS commands 可以添加或删除 classes、设置 attributes、隐藏或显示元素、dispatch events,也可以把 richer events 推回服务器;这些操作都对 DOM patch 友好,因此它们能跨过后续 patch 保持效果,而不会悄悄另起一台与服务器 render 竞争的状态机。[4] 文档甚至直接写到 optimistic composition:你可以先 push event,再立刻在客户端隐藏 modal,同时保留 server-interacting commands 的顺序保证。[4] 这是一层受限的 JS 运行时,不是一场针对 JavaScript 的意识形态战争。
这也回应了常见质疑。既然浏览器仍然处理 interaction polish、loading states 与直接的 DOM effects,LiveView 简化的对象到底是什么?答案在于 Phoenix 把这些客户端行为压进 patch-safe commands 与 declarative component calls 里,而没有把它们提升成另一套独立应用。[1][4] 客户端还在,团队却不用为每个普通页面转场、modal 或 form flow 再写一遍产品形态的第二程序。
把整支视频收回来,最值得留下的判断是这一条:LiveView 最适合被理解为一份严格的职责分配。组件用 attr 与 slot 在服务器一侧声明输入;LiveViews 把 state 与 event handling 留在同一条 process-oriented loop 里;generators 输出可迁移的 component calls;浏览器运行时则有意保持很小、懂得配合 patch,并把优势集中在浏览器真正擅长的工作上。[1][2][3][4][5][6] 这套做法的目标,是阻止客户端出于惯性变成第二个 truth source。
来源
- ElixirConf,《ElixirConf 2022 - Chris McCord - Phoenix + LiveView Updates》,YouTube 视频,发布于 2022 年 9 月 8 日。
- Phoenix LiveView 文档,《Welcome》——把 LiveViews 定义为接收事件、更新状态、通过持久连接输出 diffs 的 server-rendered HTML 进程。
- Phoenix LiveView 文档,《
Phoenix.Component》——attr/3、slot/3、compile-time validations,以及phx-*、aria-*、data-*这类 global attributes。 - Phoenix LiveView 文档,《
Phoenix.LiveView.JS》——对 DOM patch 友好的 client commands、push 选项、loading states 与 optimistic client behavior。 - Phoenix Framework Blog,《Phoenix 1.7.0 released: Built-in Tailwind, Verified Routes, LiveView Streams, and core component-based generators》。
- Nimble,《Phoenix 1.7: A Major Step for the Phoenix Framework》——对 unified HTML rendering,以及 controller 与 LiveView 组件调用方式趋同的总结。
- ElixirConf 讲者页面资源——本文所用 Chris McCord 肖像图的来源文件。