← 文章 / 编程开发
Hacker News 17小时前 · 2026-08-13 22:34:04 · 0 阅读

HTML over WebSockets:用极简 JavaScript 构建实时 SPA

构建 SPA(单页应用)像是在拼一幅复杂的拼图:前端用 JavaScript 框架绘制界面,后端提供 JSON 接口,两个独立代码库还得通过契约互相理解对方。这套模式早已被业界广泛接受并标准化。但"标准"并不意味着"唯一"。我想向你介绍另一种思路,它并不新颖,却在这些年中逐渐受到关注:HTML over WebSockets

核心思路很简单:服务端不再发送 JSON 让浏览器去拼装 HTML,而是直接把构建好的 HTML 推过来,客户端只负责把内容放到该放的位置。所有渲染逻辑都留在后端,用同一种语言完成,不需要契约,也不需要 API。这种模式被称为 hypermedia(超媒体)或 HTML over the wire(HTML 直传)。至于 HTML 如何传输,决定了通信的延迟双向性,由此衍生出三种变体:

  • 基于 HTTP,每次请求一次响应,代表项目有 htmxUnicorn
  • 基于 SSE,建立一条由服务端到客户端的单向持续通道,代表项目是 Datastar
  • 基于 WebSockets,建立一条永久的双向通道,代表项目有 Phoenix LiveViewDjango LiveView

传输通道的选择至关重要,它直接决定了应用的架构与通信模式。

本文将重点讨论 HTML over WebSockets——这一家族中支持实时双向通信的变体。它让你用极少的 JavaScript、单一语言、无需契约、单一渲染引擎,就能构建一个 SPA。我们将一起了解它是什么、如何运作,以及在什么场景下,相比 HTTP 或 SSE 方案它更值得选用。

起源

Chris McCord 是 Phoenix(Elixir 生态中最流行的框架)的作者,他在 ElixirConf 2019 上展示了一项名为 LiveView 的技术。仅用 15 分钟,他就搭建出了一个实时运行的 Twitter 克隆,无需编写任何用于渲染的 JavaScript,也没用 React、Angular、Vue 等主流前端框架来管理视图。这证明了你可以留在后端,同时保持高效开发和不错的性能。此后,这套方案逐渐流行开来,激发了其他开发者其他语言 实现 HTML-over-WebSockets。你可以回归后端,却不必放弃前端那些好的体验。

它是如何工作的?

虽然乍看之下并非如此,但客户端确实用到了 JavaScript。它的职责不是渲染,而是与服务器建立 WebSocket 通信通道,并把收到的 HTML 放到正确的位置。此外还承担一些辅助工作,比如动画、事件处理等。

McCord 的思路是 不向前端发送 JSON,而是直接发送 HTML,这样前端就无需再做预处理。如此一来,渲染工作及其全部逻辑就被搬到了后端。可是……怎么让服务器主动、即时地把新内容推给我们,而不用我们自己去请求呢?答案很简单:WebSocket。

先回顾一下开篇提到的传统流程:在浏览器中发起 HTTP 请求,服务端处理后返回一个包含所有原始数据的 JSON,接着浏览器解析 JSON,再用渲染引擎拼出对应的 HTML。

sequenceDiagram
    participant C as Browser
    participant S as Server
    C->>S: 1. HTTP request (GET /api/article/2/) and maybe auth
    S->>S: 2. Query the DB
    S->>S: 3. Build a JSON with the article data
    S-->>C: 4. Return JSON
    C->>C: 5. Parse the JSON
    C->>C: 6. Build the HTML with its rendering engine

而在 HTML over WebSockets 模式下,同样的请求走的是一条长连接通道,返回的直接是组装好的 HTML,中间不再有 JSON。而且由于通道始终保持打开,服务器甚至可以 主动 推送变更,无需客户端请求。

使用 WebSockets 的流程大致如下(忽略初次连接和认证,因为它们只在通道打开时发生一次):

sequenceDiagram
    participant C as Browser
    participant S as Server (Back-End)
    C->>S: 1. Sends a text: "I want /article/2/"
    S->>S: 2. Query the DB
    S->>S: 3. Render HTML with its template engine
    S-->>C: 4. Return the assembled HTML/CSS/JS
"..." C->>C: 5. Place the HTML where it belongs

简洁、优雅、快速。客户端只负责把 HTML 放到正确的位置并监听事件,其余一概由服务器处理。因为所有逻辑都跑在后端,所以完全不用操心客户端状态或渲染逻辑。

完整流程(包括建立连接和认证)看起来是这样的:

sequenceDiagram
    participant C as Browser
    participant S as Server (Back-End)
    C->>S: 1. Opens WebSocket connection and authenticates
    Note over C,S: A single persistent channel
    C->>S: 2. Sends a text: "I want /article/2/"
    S->>S: 3. Query the DB
    S->>S: 4. Render HTML with its template engine
    S-->>C: 5. Return the assembled HTML/CSS/JS
"..." C->>C: 6. Place the HTML where it belongs Note over S,C: The server can also push
changes without the client asking (broadcast)

此外,凭借其架构本身,它相较其他方案天然具备一些优势。

它有哪些优势?

  • 只有一个渲染引擎,大幅降低了复杂度。
  • 无需单独构建 API:服务器直接生成 HTML 发给客户端,没有中间层。
  • 状态保存在服务器端。它不是无记忆的请求-响应模式:每个连接的客户端都对应一个进程,记录着当前所在位置。这恰恰与 htmx 的无状态设计相反。
  • 直接对接数据库,无需 JSON 或 GraphQL 作为中间层。
  • 真正的实时:客户端无需轮询服务器,即可尽快收到变更。
  • 广播能力:服务器可以一次性向所有连接的客户端推送变更。聊天室、仪表盘、多人游戏等场景的实现都是水到渠成的事。
  • 每次操作的流量和延迟更小:一条持久连接避免了每次交互都重复 TCP 握手和 HTTP 头。并不是说"WebSocket 协议就神奇地更快"(在请求-响应场景下,HTTP/2 和 HTTP/3 已经大幅缩小了这个差距),而是说你跳过了来回握手,直接发送组装好的 HTML
  • 几乎为零的 JavaScript 构建 SPA,无需 React、Angular、Vue 这类重型框架。
  • 合理的 SEO:由于 HTML 在服务端渲染,首屏内容可以被索引。但要注意,爬虫看不到后续通过 WebSocket 推送的更新,所以重要内容必须包含在首次响应里。
  • 更安全,能抵御注入攻击:服务端在把 HTML 送上通道之前就完成了渲染和转义,任何试图塞进 <script> 的内容只会以纯文本的形式传到邻居的屏幕上,而不会作为代码执行。同一种架构让聊天功能变得简单,也让它天然免疫 XSS。

它有哪些缺点?

  • 服务端需要更多资源:它要维持 WebSocket 连接,通常还要在内存中保存每个客户端的状态。水平扩展时,你必须共享这些状态(在 Django 里用 Channels + ASGI 服务器 + Redis 作为 channel layer)。不过,真正的瓶颈只有在大量客户端同时在线时才会显现,而合理的设计可以缓解这一点。我的网站在类似 Raspberry Pi 3 的硬件上运行(同时还跑着其他服务),依然扛住了 600 个读者同时在线的高峰。
  • 延迟:物理延迟较高时,那种"即时"的感觉会打折扣。
  • 离线无法工作。连接一旦断开,网站就停止运作。你必须设计好重连机制和容错逻辑。
  • 起步学习曲线更陡,比直接塞一个 <script> 标签要难:搭一个 WebSocket 服务器并不简单,而且你需要学会使用 LiveView 模式。

当前生态:有哪些框架?

超媒体运动在几乎每种语言中都有实现。看 传输方式这一列:基于 WebSocket 的(LiveView 模式,实时双向)与 HTTP 和 SSE 兄弟并存,后者在不需要双向通道时使用。可以从这里开始:

语言 框架 传输方式 服务端推送? 状态
Elixir Phoenix LiveView WebSocket 成熟(1.x,LiveView 1.0 于 2024 年 12 月发布)
Ruby Hotwire(Turbo + Stimulus) HTTP + WebSocket/SSE(Streams) Turbo 8,支持 morphing
Ruby Live / Lively(socketry) WebSocket 在维护(v0.18,2026),小众:纯 WebSocket + morphdom,非 Rails 生态,以演示为导向
Python / Django Django LiveView WebSocket 活跃(本人项目)
Python / Django Reactor WebSocket 活跃
Python / Django djust WebSocket 新项目,带 Rust VDOM
Python / Django django-unicorn HTTP / AJAX 活跃
Python / Django Tetra AJAX + WebSocket 较新,基于 Alpine.js
C# / .NET Blazor(Interactive Server) WebSocket(SignalR) .NET 9,支持 render modes
PHP / Laravel Livewire 3 + Reverb WebSocket Reverb,Laravel 自家的 WebSocket 服务(2024)
语言无关(JS) htmx HTTP + WS/SSE 扩展 是(扩展) 2.0
语言无关(JS) Datastar SSE 1.0

SSE:廉价的选项

WebSockets 虽然强大,但为每个客户端维持一条双向连接是有成本的。而且很多时候你并不需要它:如果流量主要是服务端到客户端(通知、实时信息流、仪表盘、AI 回复的逐字输出),Server-Sent Events(SSE) 就够了。它的思路是一样的——在链路上发送现成的 HTML——但走的是一条普通的单向 HTTP 连接。

这是最省事的方案:基础设施也最简单。由于不需要为每个客户端维持有状态的进程,负载均衡和横向扩展都更容易。

不过它也有局限:

  • 单向。只有服务端能主动推送。客户端想发送内容时没有对应的通道,必须另外发起一次 HTTP 请求。
  • 只能传文本。只支持 UTF-8,不支持二进制(WebSocket 可以)。
  • 不适合高频双向交互。在聊天、协同编辑或游戏这类场景中,靠 HTTP 来回穿梭比始终保持打开的 WebSocket 要重得多。

htmx 的 SSE 扩展 几乎是同样的实现思路。你用一个属性声明通道,每条事件到达的 HTML 会自动填入位置:

<div hx-ext="sse" sse-connect="/updates" sse-swap="message">
    Real-time content appears here
</div>

底层用的是浏览器自带的 EventSource,自带断线重连,服务端通过 text/event-stream 发送 HTML 片段。和本文是同一套理念,只是换了传输方式。同类思路的还有 Datastar,它在 SSE 之上统一了 Alpine 风格的响应式。

简单判断标准:需要双向、低延迟通信(聊天、协同、游戏),选 WebSocket;只需要服务端主动推送,SSE 更简单、运维成本更低。

写在最后

HTML over WebSockets 并非万能解,这些技术也都没有银弹。传输方式取决于你的问题:需要实时双向交互(聊天、实时面板、协同场景),用 WebSockets;只需要服务端推送,用 SSE;普通的请求-响应就够,用 htmx over HTTP。

每个项目都有自身的背景与约束。如果只能带走一句话,那就是核心思路:传输 HTML 而非 JSON,坚守单一语言,让 API、契约和半数前端代码从你的待办清单上消失。

相信好的架构,而不是流行的框架或模式。

参考资料

  • Phoenix LiveView,官方文档:经典模式,服务器端持有每个客户端的状态,通过 WebSocket 发送差异更新。
  • Phoenix LiveView 1.0 发布,Phoenix 博客:1.0 里程碑(2024 年 12 月),距首次提交已有六年。
  • Hotwire,官网:"HTML Over The Wire" 一词的来源,以及 Turbo 主要基于 HTTP 运行的原因。
  • htmx 文档:基于 HTTP 的超媒体方案,明确采用无状态设计,WebSockets 和 SSE 仅作为扩展提供。
  • Turbo 手册:页面刷新,介绍 Turbo 8 通过morphing 仅更新变化部分,同时保留滚动位置和焦点。
  • idiomorph,被多个方案采用的 DOM morphing 库。
  • Datastar,押注 SSE 而非 WebSockets 的超媒体框架。
  • 使用 server-sent events,MDN:SSE 的工作方式(`EventSource`、自动重连、`Last-Event-ID`、`text/event-stream`)。
  • Laravel Reverb,Laravel 官方出品的 WebSocket 服务器(2024 年):证明 PHP 也能在不依赖第三方扩展的情况下实现实时功能。
  • ASP.NET Core Blazor 渲染模式,Microsoft Learn:Interactive Server 模式基于 SignalR(WebSockets)运行。
License by-nc-nd

本作品采用 署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可。

我在本文中是如何使用 AI 的?

想读那些我从不发表的内容吗?

我会发送那些不适合放进文章里的东西:完整的配置、零散的笔记,以及我当下正在测试的各种内容。不发垃圾邮件,不搞销售套路。

newsletter@andros.dev 发一封邮件,主题写上 SUBSCRIBE,就能加入。

原始来源: Hacker News

评论 (0)