WebSockets 直传 HTML:近乎零 JS 的实时网页实践

造一个单页应用(SPA)向来是个复杂拼图:一套 JavaScript 框架负责画视图,一个 API 负责吐 JSON,前后端两套代码库还得靠契约彼此对齐。这是业界默认的专业做法,但默认不等于唯一解。另一种思路近年逐渐走红:服务器直接把拼装好的 HTML 发过来,客户端只管把它摆到该在的位置。所有渲染逻辑留在后端、用同一种语言完成,不需要契约,也不需要 API。这种模式被称为超媒体(hypermedia),也叫 HTML over the wire。

三种传输方式

关键不在于 HTML 怎么「旅行」,而在于它决定了通信的延迟和双向能力。这个家族有三种变体。

  • HTTP:一问一答,代表是 htmx 与 Unicorn。
  • SSE(服务器推送事件):在服务器到客户端之间打开一条单向持续通道,代表是 Datastar。
  • WebSockets:一条永久的双向通道,代表是 Phoenix LiveView 与 Django LiveView。

本文聚焦 WebSockets 这一支——家族里实时、双向的那一种,能让人用近乎零 JavaScript、单一语言、无契约、单一渲染引擎造出一个 SPA。

它从哪来

Chris McCord 是 Elixir 生态最流行框架 Phoenix 的作者。2019 年的 ElixirConf 大会上,他演示了一项叫 LiveView 的技术:只用了 15 分钟,就在不写任何渲染端 JavaScript、也不引入 React、Angular、Vue 等框架的前提下,搭出一个实时运行的 Twitter 克隆。这证明开发者完全可以待在后端,照样高效产出,还顺带获得不错的性能。此后这套方案流行开来,启发其他语言也造出各自的 HTML over WebSockets 实现。LiveView 1.0 于 2024 年 12 月发布,距离首行提交已过去六年。

它怎么运转

乍看之下客户端也用了 JavaScript,但它的任务不是渲染,而是建立 WebSocket 通道、把收到的 HTML 摆到正确位置,再顺手处理动画和事件等次要工作。McCord 的方案核心在于:不把 JSON 发给前端,而是发无需预处理的 HTML,于是渲染负载连同全部逻辑都搬到了后端。

传统流程是这样的:浏览器发出 HTTP 请求,服务器查出数据、拼出 JSON 返回;浏览器再解析 JSON、用自己的渲染引擎把 HTML 造出来。

换成 WebSockets 后,同样的交互走的是一条永不关闭的永久通道,返回的已经是拼好的 HTML,中间没有 JSON。由于通道从不关闭,服务器甚至能「抢跑」——不等客户端请求就主动把变化推过去。连接与认证只在通道开启时发生一次,之后客户端发去一句请求:「打开 /article/2/ 的页面」,服务器查库、用模板引擎渲染、把成品 HTML 发回,客户端把它摆好即可。服务器还可以向所有在线客户端同时广播变化,聊天气泡、实时仪表盘、多人协作游戏几乎「免费」到手。

它的优势

  • 单一渲染引擎,复杂度骤降。
  • 无需另造 API:服务器直接生成 HTML 发出去,没有中间商。
  • 状态活在服务端:每个连接的客户端都有一个记住上下文的进程,与刻意无状态的 htmx 正相反。
  • 直连数据库:没有 JSON 或 GraphQL 中间层。
  • 真·实时:客户端第一时间收到变化,无需轮询服务器。
  • 广播:服务器一次把变化推给所有客户端。
  • 单次交互流量与延迟更低:永久连接免去了每次交互重做 TCP 握手和 HTTP 头。并非「WebSocket 协议本身神奇地快」(HTTP/2、HTTP/3 已大幅收窄请求应答场景的差距),而是省掉了往返、直接发送成品 HTML。
  • 近乎零 JS 的 SPA:不必搬出 React、Angular、Vue 这类重型框架。
  • SEO 尚可言说:首屏 HTML 在服务端渲染,可被索引;但爬虫看不到后续经 WebSocket 抵达的更新,关键内容必须落在首屏响应里。
  • 更抗注入:服务端在发送前完成渲染与转义,一段 <script> 会作为惰性文本到达邻座屏幕,而不会变成可执行代码。正因如此,让聊天变简单的同一套架构,也天然免疫 XSS。

它的代价

  • 服务端更吃资源:要保持一条打开的 WebSocket,通常还要在内存里保存每个客户端的状态。水平扩展时得共享这份状态(Django 下靠 Channels 加 ASGI 服务器加 Redis 做通道层)。不过真正的问题只在大并发时出现,精心设计的架构能缓解。作者本人的站点用接近树莓派 3 的硬件,带着其他服务一起跑,曾扛住 600 名读者同时在线而无碍。
  • 延迟敏感:物理延迟一大,那种「瞬间」的体感就垮了。
  • 无法离线:连接一断,站点就停摆,必须设计重连体验与容错。
  • 学习曲线更陡:跑一个 WebSocket 服务器本就不轻松,还得学会驾驭 LiveView 这套模式。

框架全景

超媒体运动几乎已覆盖每种语言,按传输方式可分为常开双向的 WebSockets 一脉,与 HTTP、SSE 这两支「用不上双向通道时」的平替。

  • Elixir:Phoenix LiveView,走 WebSockets,双向推送,成熟(1.x,LiveView 1.0 于 2024 年 12 月)。
  • Ruby:Hotwire(Turbo + Stimulus),HTTP 加 WebSockets/SSE 流,双向推送,Turbo 8 引入 morphing。
  • Python / Django:Django LiveView、Reactor、djust(带 Rust 虚拟 DOM 的新秀),均走 WebSockets 双向推送;另有 django-unicorn 走 HTTP/AJAX 不推送;Tetra 走 AJAX 加 WebSockets,基于 Alpine.js。
  • C# / .NET:Blazor 交互服务端模式,走 WebSocket(SignalR)。
  • PHP / Laravel:Livewire 3 加 Reverb(Laravel 自家 2024 年推出的 WebSocket 服务器)。
  • 通用 JS:htmx 走 HTTP 加 WS/SSE 扩展,2.0;Datastar 押注 SSE,1.0。

SSE:更便宜的单向平替

WebSockets 很强大,但为每个客户端保持一条双向通道是有代价的,而很多时候你并不需要它。如果数据流主要是服务器到客户端(通知、实时信息流、仪表盘、AI 回复的逐字输出),SSE 就够了。它秉持同一理念——把成品 HTML 经线路发出去——只是走一条只出不进的普通 HTTP 通道。

它最便宜:基础设施最简单;不为每个客户端保持一个有状态进程,负载均衡与扩展都更轻松。代价是:单向,只有服务器能推,客户端要回传就得另发一次 HTTP 请求;只传文本(UTF-8,不带二进制,而 WebSocket 可以);重度双向场景更吃力——聊天、协同编辑、游戏里那种松散的来回,比一条常开 WebSocket 开销更大。htmx 通过 SSE 扩展提供了精神几乎一致的实现,Datastar 则把 Alpine 风格的反应式统一到 SSE 上。

一句话规则:要双向、低延迟(聊天、协作、游戏)选 WebSockets;只从服务器推送,SSE 更简单也更省。 真正该信的是好的架构,而非时髦的框架或模式。

来源:andros.dev(作者 Andros Fenollosa)