<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>htmx &#8211; mylogs.cn</title>
	<atom:link href="https://mylogs.cn/tag/htmx/feed/" rel="self" type="application/rss+xml" />
	<link>https://mylogs.cn</link>
	<description>发现、记录、分享</description>
	<lastBuildDate>Wed, 12 Aug 2026 17:46:38 +0000</lastBuildDate>
	<language>zh-Hans</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	
	<item>
		<title>WebSockets 直传 HTML：近乎零 JS 的实时网页实践</title>
		<link>https://mylogs.cn/html-over-websockets-real-time-spa/</link>
		
		<dc:creator><![CDATA[steve, zhang]]></dc:creator>
		<pubDate>Wed, 12 Aug 2026 17:46:24 +0000</pubDate>
				<category><![CDATA[科技]]></category>
		<category><![CDATA[htmx]]></category>
		<category><![CDATA[Phoenix LiveView]]></category>
		<category><![CDATA[SSE]]></category>
		<category><![CDATA[WebSockets]]></category>
		<category><![CDATA[前端架构]]></category>
		<category><![CDATA[实时网页]]></category>
		<category><![CDATA[超媒体 HTML over the wire]]></category>
		<guid isPermaLink="false">https://mylogs.cn/html-over-websockets-real-time-spa/</guid>

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



<figure data-wp-context="{&quot;imageId&quot;:&quot;6a7cdc6e79e48&quot;}" data-wp-interactive="core/image" data-wp-key="6a7cdc6e79e48" class="wp-block-image size-large aligncenter wp-lightbox-container"><img fetchpriority="high" decoding="async" width="1200" height="799" data-wp-class--hide="state.isContentHidden" data-wp-class--show="state.isContentVisible" data-wp-init="callbacks.setButtonStyles" data-wp-on--click="actions.showLightbox" data-wp-on--load="callbacks.setButtonStyles" data-wp-on--pointerdown="actions.preloadImage" data-wp-on--pointerenter="actions.preloadImageWithDelay" data-wp-on--pointerleave="actions.cancelPreload" data-wp-on-window--resize="callbacks.setButtonStyles" src="https://mylogs.cn/wp-content/uploads/2026/08/c1596819dfd6b0945f95652ab5e0d590_header.webp" alt="WebSockets 直传 HTML：近乎零 JS 的实时网页实践" class="wp-image-4615" style="max-width:100%;height:auto;" srcset="https://mylogs.cn/wp-content/uploads/2026/08/c1596819dfd6b0945f95652ab5e0d590_header.webp 1200w, https://mylogs.cn/wp-content/uploads/2026/08/c1596819dfd6b0945f95652ab5e0d590_header-300x200.webp 300w, https://mylogs.cn/wp-content/uploads/2026/08/c1596819dfd6b0945f95652ab5e0d590_header-1024x682.webp 1024w, https://mylogs.cn/wp-content/uploads/2026/08/c1596819dfd6b0945f95652ab5e0d590_header-768x511.webp 768w" sizes="(max-width: 1200px) 100vw, 1200px" /><button
			class="lightbox-trigger"
			type="button"
			aria-haspopup="dialog"
			data-wp-bind--aria-label="state.thisImage.triggerButtonAriaLabel"
			data-wp-init="callbacks.initTriggerButton"
			data-wp-on--click="actions.showLightbox"
			data-wp-style--right="state.thisImage.buttonRight"
			data-wp-style--top="state.thisImage.buttonTop"
		>
			<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="none" viewBox="0 0 12 12">
				<path fill="#fff" d="M2 0a2 2 0 0 0-2 2v2h1.5V2a.5.5 0 0 1 .5-.5h2V0H2Zm2 10.5H2a.5.5 0 0 1-.5-.5V8H0v2a2 2 0 0 0 2 2h2v-1.5ZM8 12v-1.5h2a.5.5 0 0 0 .5-.5V8H12v2a2 2 0 0 1-2 2H8Zm2-12a2 2 0 0 1 2 2v2h-1.5V2a.5.5 0 0 0-.5-.5H8V0h2Z" />
			</svg>
		</button><figcaption class="wp-element-caption">图片来源：andros.dev</figcaption></figure>





<h2 class="wp-block-heading">三种传输方式</h2>



<p class="wp-block-paragraph">关键不在于 HTML 怎么「旅行」，而在于它决定了通信的延迟和双向能力。这个家族有三种变体。</p>



<ul class="wp-block-list"><li>走 <strong>HTTP</strong>：一问一答，代表是 htmx 与 Unicorn。</li><li>走 <strong>SSE</strong>（服务器推送事件）：在服务器到客户端之间打开一条单向持续通道，代表是 Datastar。</li><li>走 <strong>WebSockets</strong>：一条永久的双向通道，代表是 Phoenix LiveView 与 Django LiveView。</li></ul>



<p class="wp-block-paragraph">本文聚焦 WebSockets 这一支——家族里实时、双向的那一种，能让人用近乎零 JavaScript、单一语言、无契约、单一渲染引擎造出一个 SPA。</p>



<h2 class="wp-block-heading">它从哪来</h2>



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



<h2 class="wp-block-heading">它怎么运转</h2>



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



<p class="wp-block-paragraph">传统流程是这样的：浏览器发出 HTTP 请求，服务器查出数据、拼出 JSON 返回；浏览器再解析 JSON、用自己的渲染引擎把 HTML 造出来。</p>



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



<h2 class="wp-block-heading">它的优势</h2>



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



<h2 class="wp-block-heading">它的代价</h2>



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



<h2 class="wp-block-heading">框架全景</h2>



<p class="wp-block-paragraph">超媒体运动几乎已覆盖每种语言，按传输方式可分为常开双向的 WebSockets 一脉，与 HTTP、SSE 这两支「用不上双向通道时」的平替。</p>



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



<h2 class="wp-block-heading">SSE：更便宜的单向平替</h2>



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



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



<p class="wp-block-paragraph">一句话规则：<strong>要双向、低延迟（聊天、协作、游戏）选 WebSockets；只从服务器推送，SSE 更简单也更省。</strong> 真正该信的是好的架构，而非时髦的框架或模式。</p>



<p class="has-small-font-size wp-block-paragraph">来源：andros.dev（作者 Andros Fenollosa）</p>

]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>
