前言
先郑重声明一下,这篇文章会带一点主观判断。
因为 LiveView 这个东西,天然就不是那种"参数 A 调成 10 还是 20"的小选择,它更像是在问你:
你到底想不想继续维护两套状态、两套校验、两套调试心智。
我自己前几年写 Web,主路线一直是很典型的 SPA:
- 前端 React / Vue 管界面和交互
- 后端提供 REST API 或 GraphQL
- 登录态、表单状态、列表状态、错误状态,在前端和后端之间来回同步
这套东西当然能跑,而且成熟、工程化也完整。但说句实话,项目一复杂,痛苦也是真痛苦。
最折磨我的不是写组件,也不是调 CSS,而是下面这些"看似正常,实际上非常耗脑子"的活:
- 一个表单要写前端校验,还要写后端校验
- 一个按钮点下去,要考虑 loading、失败、回滚、接口报错、数据刷新
- 一个列表改了某条数据,要么局部更新缓存,要么整页重新拉接口
- 前端状态和数据库真实状态,经常有一个时间差
后来我再看 LiveView,最大的感受不是"这框架真新",而是:
这玩意儿是在正面解决 SPA 的结构性复杂度。
1. LiveView 到底解决了什么
一句话先说结论:
LiveView 解决的不是"页面怎么更新",而是"状态到底该由谁负责"。
很多人第一次看 LiveView,会把它理解成:
- 服务端渲染加强版
- 带 WebSocket 的模板引擎
- Phoenix 版的"少写点前端"
这么理解不能说全错,但不够到位。
LiveView 最核心的设计,其实是这三个点:
- 页面状态主要留在服务端
- 浏览器通过 WebSocket 把事件发回服务端
- 服务端重新渲染后,只把 DOM diff 推给客户端
也就是说,浏览器不是主要状态中心,它更像一个"事件输入层 + 渲染承载层"。
举个例子,最经典的计数器:
elixir
defmodule DemoWeb.CounterLive do
use DemoWeb, :live_view
def mount(_params, _session, socket) do
{:ok, assign(socket, count: 0)}
end
def handle_event("inc", _params, socket) do
{:noreply, update(socket, :count, &(&1 + 1))}
end
def render(assigns) do
~H"""
<div>
<h1>Count: <%= @count %></h1>
<button phx-click="inc">+1</button>
</div>
"""
end
end
如果你从 React 视角看,这段代码最反直觉的地方是:
- 没有前端
useState - 没有
fetch - 没有 API 路由
- 没有前端事件处理函数去调后端
按钮点击以后,浏览器只是把 "inc" 这个事件发到服务端,服务端改 socket.assigns,然后把更新后的 diff 推回去。
这就是 LiveView 的出发点:
状态不在前后端之间来回搬运,状态就待在服务端。
2. 它和 SPA 最大的区别,不是"少写 JS",而是少维护一套世界
很多文章喜欢把 LiveView 的卖点写成"前端不写 JS"。这句话有传播力,但我觉得它会把重点带偏。
因为真正值钱的不是少写几行 JavaScript,而是:
你少维护了一整套前端状态系统。
2.1 SPA 的问题,不是技术不行,而是同步成本太高
SPA 最强的地方是客户端足够灵活,交互细腻,生态巨大。
但它的代价也一直很明确:
- 客户端有一份状态
- 服务端有一份状态
- 两边要靠 API 契约持续保持一致
你做一个最普通的"编辑资料"功能,前端通常要操心这些东西:
ts
const [form, setForm] = useState(initialForm)
const [errors, setErrors] = useState({})
const [saving, setSaving] = useState(false)
async function onSubmit() {
setSaving(true)
const res = await updateProfile(form)
if (!res.ok) {
setErrors(res.errors)
setSaving(false)
return
}
toast.success("保存成功")
setSaving(false)
}
这还只是最理想的情况。真实项目里很快就会追加:
- 接口超时怎么办
- 用户连续点击怎么办
- 后端返回字段和前端表单字段不完全一致怎么办
- 乐观更新失败怎么回滚
这些都不是 React 的锅,也不是 Vue 的锅,而是 SPA 架构天然要承担的同步成本。
2.2 LiveView 的思路:校验、状态、UI 反馈放回同一个地方
再看 LiveView 的表单思路,你会发现它很"暴力":
- 表单状态在服务端
- 校验在服务端
- 错误消息在服务端
- 提交后的界面反馈,还是服务端决定
比如一个实时校验的表单,它不是"前端先验证一遍,提交时后端再验证一遍",而是直接把 phx-change 事件发回去,让服务端用 changeset 做统一校验。
这套模式最爽的地方,是业务规则终于不用写两份了。
我第一次认真体会到这一点,是在处理"前端显示合法,后端拒绝保存"的问题时。以前我会本能地去查:
- 是前端校验漏了?
- 是接口 DTO 变了?
- 是后端规则升级了没同步?
LiveView 里这类问题会少很多,因为验证源头天然更集中。
3. 我为什么说 LiveView 不是"又一个框架"
因为它跟 React、Vue、Svelte 这类东西,讨论维度根本不完全一样。
React 这类框架主要在优化:
- 客户端组件组织
- 客户端状态管理
- 客户端渲染性能
而 LiveView 在优化的是:
- 服务端状态主导的交互模型
- Web 应用的整体复杂度
- 实时场景下的数据一致性
说白了,React 在问"前端这套怎么做更好",LiveView 在问"这套东西是不是一定要放前端做"。
这个问题,味道就完全不一样了。
3.1 它更像"把 Web 做回服务器主导"
我现在越来越觉得,LiveView 的价值不是倒退,而是一次有技术前提支撑的"回摆"。
以前传统 SSR 最大的问题是什么?
- 页面切换重
- 交互不够细
- 实时能力弱
现在有了长连接、DOM diff、现代浏览器和更强的服务端并发模型之后,LiveView 重新把"服务器主导页面状态"这件事做活了。
所以它不是简单地回到 JSP / PHP 模板时代。
它更像是:
保留服务器统一状态的优点,同时借 WebSocket 拿回一部分 SPA 的交互体验。
这才是它真正有意思的地方。
4. 跟几种常见方案放在一起看,差异会更明显
4.1 LiveView vs React/Vue + REST API
这是最核心的一组对比。
React/Vue 这套组合的核心是:
- 客户端负责状态和视图
- 服务端提供数据和业务接口
LiveView 的核心是:
- 服务端负责状态和业务
- 客户端主要负责事件上报和界面承载
两边最大的差异,不是语法,不是组件,而是状态中心的位置。
如果你团队前端能力很强、交互特别重、离线需求多,那 SPA 依然很合理。
但如果你做的是:
- 后台系统
- 管理平台
- 表单密集型业务
- 多用户实时协作
那 LiveView 真有可能让复杂度降一个量级。
4.2 LiveView vs Hotwire
我个人觉得,这两个方向很像,都是在反思"是不是所有交互都要交给大前端框架"。
但区别也明显:
- Hotwire 更像是"HTML over the wire",强调用服务端返回的 HTML 片段更新页面
- LiveView 更强调服务端长期持有状态,由进程持续管理交互
我的主观判断是:
Hotwire 更像轻量回归,LiveView 更像完整范式。
为什么这么说?因为 LiveView 从一开始就把 mount、handle_event、handle_info 这一整套服务端事件循环做成了一等公民。它不是补丁式增强,而是明确告诉你:
页面本身就是一个运行在服务端进程里的交互单元。
这个心智一旦建立起来,后面做实时页面会非常顺。
4.3 LiveView vs Blazor
Blazor Server 和 LiveView 在大方向上其实挺像,都是把 UI 状态更多地放回服务端。
但我个人更看重 LiveView 的一点是:它和 Elixir / BEAM 的并发模型咬得非常紧。
每个 LiveView 都是一个进程,这件事在 Elixir 世界里非常自然。你做消息传递、订阅广播、定时刷新、故障隔离,脑回路是顺的。
换句话说,LiveView 不是"框架强行发明了一种模型",而是"框架顺着 BEAM 最擅长的事情往前推了一步"。
这也是为什么我觉得它不只是 Phoenix 的一个插件,而是 Phoenix 在 BEAM 上长出来的一种原生能力。
4.4 LiveView vs Phoenix Channels
这个也很容易被搞混。
很多人会觉得:既然 Phoenix 本来就有 Channels,那我直接 Channels + 自己写前端,不也能做实时?
能,当然能。
但问题是,Channels 解决的是通信通道 ,LiveView 解决的是页面交互模型。
Channels 更像是底层能力:
- 我能订阅
- 我能广播
- 我能收消息
LiveView 更往上一层:
- 页面怎么初始化
- 事件怎么处理
- 状态怎么保存
- UI 怎么更新
所以我自己的理解一直是:
Channels 是零件,LiveView 是整车。
5. 我踩过或者差点踩进去的几个坑
既然这是一篇系列开篇,我不想只说优点,坑也得先摆出来。
5.1 第一个坑:别把它当成"服务端版 React"
这是最容易犯的错误。
如果你脑子里一直想着:
- 这里相当于前端组件
- 那里相当于前端 state
- 这个事件相当于前端 handler
那你一开始会学得很别扭。
因为 LiveView 的关键不在于"它像不像 React",而在于:
它本质上是一个长期存活的服务端进程在驱动页面。
你如果不接受这个前提,后面看 handle_info/2、PubSub、connected?/1 的时候,会一直觉得绕。
5.2 第二个坑:什么都想实时,最后可能把服务端打爆
LiveView 很容易让人上头。
尤其第一次看到 phx-change 每次输入都能校验,phx-click 一点就改状态,实时感很强,真有一种"这不比前后端分离省事多了"的爽感。
但冷静一点。
每一次交互,背后都不是白送的。
我自己一开始就差点在这里翻车。
当时我脑子很热,想把一个筛选很多、列表也很长的页面,全塞进一个 LiveView 里:
- 输入关键字就实时查
- 点筛选条件就实时刷
- 右侧详情面板跟着实时变
结果就是,功能确实很快能做出来,但页面一复杂,马上会遇到几个现实问题:
- 输入太频繁,事件发得很密
- assigns 变胖以后,diff 成本也上来
- 你以为自己省掉了前端状态,实际上只是把压力集中到了一个服务端进程里
后来我对这件事的理解就变了:
LiveView 不是不能做重交互,而是你必须先想清楚,哪些交互值得走服务端回路,哪些要节流,哪些应该拆组件,哪些根本不该做成"每次输入都立刻响应"。
如果你的页面里:
- 大量输入频繁触发事件
- assign 塞得很重
- 列表很大还一直全量重渲染
那服务端压力会很快上来。
我对 LiveView 的一个基本判断是:
它不是不要性能意识,而是把性能问题从前端搬到了服务端进程和 diff 策略上。
所以别误会成"写少了 JS,就自动高性能"。
5.3 第三个坑:它不是所有项目的银弹
这个我一定要提前讲。
LiveView 非常适合:
- 业务后台
- 实时面板
- 聊天、协作、通知流
- 表单和工作流密集的系统
但如果你做的是下面这些场景,就得非常谨慎:
- 重度离线应用
- 客户端本地计算很多
- 动画和画布交互特别重
- 对前端独立迭代要求极高的大团队协作
这种时候,SPA 依然更对路。
所以我的态度一直很明确:
LiveView 很强,但它强在"把不必要的前端复杂度砍掉",不是强在"统一吃掉所有前端场景"。
6. 我为什么觉得它特别适合被 SPA 折磨过的人
因为你只有真正在项目里被下面这些东西来回磨过,才会意识到 LiveView 的价值不是"新鲜",而是"减负":
- 接口字段改了,前后端一起追
- 一个交互链路跨前端、API、数据库三层调试
- 表单校验两边维护,逻辑总有一天飘掉
- 实时页面要自己补一堆 websocket 订阅和状态同步
LiveView 最吸引我的,不是它"酷",而是它非常直接:
能不分两套,就别分两套。
这句话看起来朴素,但其实很有杀伤力。
因为很多 Web 项目的复杂度,并不是业务本身复杂,而是架构把简单问题拆成了两套系统来做。
LiveView 的价值,就是把一部分被过度拆开的东西重新收回来。
7. 这一套范式,最适合怎么理解
如果让我用一句最不绕的话总结,我会这么说:
SPA 是把浏览器当应用主场,LiveView 是把服务器当应用主场。
浏览器当然还重要,但它不再是最主要的状态中心。
只要你接受这个前提,很多设计就都顺了:
- 为什么
assigns那么重要 - 为什么
handle_event是主路径 - 为什么 PubSub 能自然接进来
- 为什么聊天室、协作面板这类场景会特别顺手
反过来,如果你不接受这个前提,只把它理解成"少写 JS 的 Phoenix 页面",那你会低估它,也会用不好它。
总结
这篇文章我只想讲清一件事:
LiveView 不只是又一个框架,它是在重新定义 Web 应用里"状态该放哪、交互该由谁主导"。
它不一定适合所有团队,也不可能替代所有 SPA。但如果你已经被前后端状态同步、表单双份校验、实时交互接线这些问题折腾得有点麻,LiveView 非常值得认真看。
对我来说,它最有价值的一点不是"前端不写 JS",而是:
很多本来被拆成两层、三层才能完成的事情,现在可以回到一个统一的服务端交互模型里。
如果你也写过一段时间 SPA,评论区可以聊聊:你最烦的到底是状态管理、接口同步,还是那套永远写不完的表单逻辑。