SPA 写累了?试试 LiveView:服务端管状态,前端不写 JS

前言

先郑重声明一下,这篇文章会带一点主观判断。

因为 LiveView 这个东西,天然就不是那种"参数 A 调成 10 还是 20"的小选择,它更像是在问你:

你到底想不想继续维护两套状态、两套校验、两套调试心智。

我自己前几年写 Web,主路线一直是很典型的 SPA:

  • 前端 React / Vue 管界面和交互
  • 后端提供 REST API 或 GraphQL
  • 登录态、表单状态、列表状态、错误状态,在前端和后端之间来回同步

这套东西当然能跑,而且成熟、工程化也完整。但说句实话,项目一复杂,痛苦也是真痛苦。

最折磨我的不是写组件,也不是调 CSS,而是下面这些"看似正常,实际上非常耗脑子"的活:

  • 一个表单要写前端校验,还要写后端校验
  • 一个按钮点下去,要考虑 loading、失败、回滚、接口报错、数据刷新
  • 一个列表改了某条数据,要么局部更新缓存,要么整页重新拉接口
  • 前端状态和数据库真实状态,经常有一个时间差

后来我再看 LiveView,最大的感受不是"这框架真新",而是:

这玩意儿是在正面解决 SPA 的结构性复杂度。

1. LiveView 到底解决了什么

一句话先说结论:

LiveView 解决的不是"页面怎么更新",而是"状态到底该由谁负责"。

很多人第一次看 LiveView,会把它理解成:

  • 服务端渲染加强版
  • 带 WebSocket 的模板引擎
  • Phoenix 版的"少写点前端"

这么理解不能说全错,但不够到位。

LiveView 最核心的设计,其实是这三个点:

  1. 页面状态主要留在服务端
  2. 浏览器通过 WebSocket 把事件发回服务端
  3. 服务端重新渲染后,只把 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 从一开始就把 mounthandle_eventhandle_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,评论区可以聊聊:你最烦的到底是状态管理、接口同步,还是那套永远写不完的表单逻辑。

相关推荐
Csvn1 小时前
异步错误捕获的六大陷阱:await 裹着 try-catch 就一定稳了吗?
前端
用户059540174461 小时前
向量库静默丢数据踩坑实录:Playwright 端到端测试让我排查了72小时
前端·css
Asize1 小时前
CSS 3D:从布局到立方体
前端
梨子同志1 小时前
React
前端
万少2 小时前
22 点后,我靠这个 AI 工具成了"夜间天才程序员"
前端·后端
狂师2 小时前
比 Playwright 更给力,推荐一个AI Agent的浏览器自动化开源项目!
前端·开源·测试
IT_陈寒2 小时前
React hooks 闭包陷阱把我的状态吃掉了,原来问题出在这里
前端·人工智能·后端
壹方秘境2 小时前
使用ApiCatcher在 iOS 上像修改 hosts 一样自定义域名解析
前端·后端·客户端
柳杉2 小时前
可视化大屏设计器脚手架:从设计到交付的一站式方案
前端·three.js·数据可视化