Phoenix LiveView 实时 Web 框架
Phoenix LiveView 实时 Web 框架教程
什么是 Phoenix LiveView?
LiveView 是 Phoenix 框架的一个核心功能,它允许开发者在 不需要编写任何 JavaScript 的情况下构建富交互的实时单页应用。通过在服务器端保持一个持久化连接(WebSocket),LiveView 将页面的更新以最小的数据形式推送给客户端,整个过程由 Elixir 优雅地管理。
传统 SPA 与 LiveView 的对比
| 特性 | 传统 SPA (React/Vue) | Phoenix LiveView |
|---|---|---|
| 主要语言 | JavaScript | Elixir |
| 状态管理 | 客户端状态 + API 同步 | 服务端状态,即单一真相源 |
| 实时更新 | 需手动实现(WebSocket + 订阅) | 内置,通过 PubSub 和进程模型 |
| 首次渲染 | 空白页面等待 JS 执行 | 服务端渲染,即时可见 |
| 页面交互 | 客户端 JS 处理 DOM 更新 | 服务端处理事件,推送 DOM 差异 |
环境准备与项目创建
必备工具
- Elixir 1.14 或更高版本
- Erlang/OTP 25+
- Phoenix 1.7+(LiveView 已默认集成)
- 一个代码编辑器(如 VS Code)
- 基础 Elixir 语法知识
创建你的第一个 LiveView 项目
在终端执行:
mix phx.new my_live_app --no-ecto --no-mailer --no-dashboard
--no-ecto跳过数据库(本教程不涉及数据库)--no-mailer--no-dashboard保持项目简洁
进入项目目录并安装依赖:
cd my_live_app
mix deps.get
修改 config/dev.exs,确保 LiveView 的签名盐已配置(Phoenix 1.7 自动配置可以忽略):
config :my_live_app, MyLiveAppWeb.Endpoint,
live_view: [signing_salt: "your_salt"]
然后启动服务器:mix phx.server,访问 http://localhost:4000 即可看到默认页面。
LiveView 的核心概念
1. 状态即是进程
每个 LiveView 实例都是一个 Elixir 进程。用户在浏览器打开页面时,服务端会创建一个对应的 LiveView 进程,该进程维护了当前页面的所有状态。当进程终止(如用户关闭标签页),状态随之销毁。这种模型天然避免了状态同步问题。
2. 挂载点(mount/3)
mount/3 是 LiveView 的入口函数,在首次连接时被调用。它接收三个参数:params(路由参数)、session(会话信息)和 socket。你必须返回 {:ok, socket} 或 {:ok, socket, redirect: "/other"} 等。
def mount(_params, _session, socket) do
{:ok, assign(socket, count: 0)}
end
3. 赋值(assigns)与模板
通过 assign/2 或 assign/3 将数据绑定到 socket,然后在模板(.heex)中使用 <%= @count %> 访问。模板是纯服务器端渲染的,首次渲染输出完整 HTML。
4. 事件处理
- 客户端事件:如
phx-click、phx-submit,会发送事件到服务端的handle_event/3函数。 - 内部消息:通过
send(self(), msg)发送消息,在handle_info/2中处理。
构建一个实时计数器
创建 LiveView 模块
新建文件 lib/my_live_app_web/live/counter_live.ex:
defmodule MyLiveAppWeb.CounterLive do
use MyLiveAppWeb, :live_view
def mount(_params, _session, socket) do
{:ok, assign(socket, count: 0)}
end
def render(assigns) do
~H"""
<div>
<h1>计数器:<%= @count %></h1>
<button phx-click="increment">+1</button>
<button phx-click="decrement">-1</button>
</div>
"""
end
def handle_event("increment", _params, socket) do
{:noreply, update(socket, :count, &(&1 + 1))}
end
def handle_event("decrement", _params, socket) do
{:noreply, update(socket, :count, &(&1 - 1))}
end
end
配置路由
编辑 lib/my_live_app_web/router.ex,在 scope "/", MyLiveAppWeb 中添加 live 路由:
live "/counter", CounterLive
启动服务器,访问 /counter,点击按钮即可看到数字实时变化,整个过程不刷新页面,也没有一行 JavaScript。
表单与数据绑定
LiveView 提供了 phx-change 和 phx-submit 来处理表单交互,并且内置表单组件 Phoenix.Component.form/1(或旧版的 Phoenix.LiveView.Helpers.form/1)。
示例:简单的用户问候表单
新建 lib/my_live_app_web/live/greeter_live.ex:
defmodule MyLiveAppWeb.GreeterLive do
use MyLiveAppWeb, :live_view
def mount(_params, _session, socket) do
{:ok, assign(socket, name: "", greeting: "")}
end
def render(assigns) do
~H"""
<form phx-submit="greet">
<input type="text" name="name" placeholder="输入你的名字"
value={@name}
phx-keyup="name_input" />
<button type="submit">问候</button>
</form>
<p><%= @greeting %></p>
"""
end
def handle_event("name_input", %{"value" => name}, socket) do
{:noreply, assign(socket, name: name)}
end
def handle_event("greet", %{"name" => name}, socket) do
greeting = "你好,#{name}!"
{:noreply, assign(socket, greeting: greeting, name: name)}
end
end
路由添加 live "/greeter", GreeterLive。现在输入框中键入内容时,通过 phx-keyup 实时更新 @name,提交后显示问候语。注意这里用的是纯 HTML 表单,没有使用 Phoenix 的 form 组件,适合简单场景。
实时多用户功能:利用 PubSub
LiveView 天然集成了 Phoenix PubSub,可以轻松实现跨用户、跨页面的实时广播。
聊天室示例(单房间简化版)
创建 lib/my_live_app_web/live/chat_live.ex:
defmodule MyLiveAppWeb.ChatLive do
use MyLiveAppWeb, :live_view
@topic "chat:lobby"
def mount(_params, _session, socket) do
if connected?(socket) do
Phoenix.PubSub.subscribe(MyLiveApp.PubSub, @topic)
end
{:ok, assign(socket, messages: [], new_message: "")}
end
def render(assigns) do
~H"""
<div>
<h2>公共聊天室</h2>
<div id="messages" phx-update="stream">
<ul>
<%= for msg <- @messages do %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<form phx-submit="send_message">
<input type="text" name="message" placeholder="输入消息..." value={@new_message}
phx-keyup="typing" />
<button type="submit">发送</button>
</form>
</div>
"""
end
def handle_event("typing", %{"value" => text}, socket) do
{:noreply, assign(socket, new_message: text)}
end
def handle_event("send_message", %{"message" => msg}, socket) do
full_msg = "用户:#{msg}"
Phoenix.PubSub.broadcast(MyLiveApp.PubSub, @topic, {:new_message, full_msg})
{:noreply, assign(socket, new_message: "")}
end
def handle_info({:new_message, msg}, socket) do
{:noreply, assign(socket, messages: [msg | @messages])}
end
end
connected?(socket)确保仅在 WebSocket 连接建立后才订阅 PubSub,避免 mount 阶段重复订阅。phx-update="stream"是优化渲染的技巧,当列表大量更新时减少 DOM 操作。- 多个用户打开此页面,即可实时看到各自发送的消息。
路由添加 live "/chat", ChatLive。
生命周期回调
除了 mount/3,LiveView 还有其他重要回调:
handle_params/3
在挂载后或 URL 参数变化时调用。适合根据查询参数更新状态。
def handle_params(%{"tab" => tab}, _uri, socket) do
{:noreply, assign(socket, tab: tab)}
end
handle_event/3
处理客户端事件(点击、表单提交、按键等)。
handle_info/2
处理来自其他进程的 Erlang 消息(如 PubSub 广播、定时器消息)。
定时器示例
在 mount 中设置定时器:
def mount(_params, _session, socket) do
if connected?(socket), do: :timer.send_interval(1000, self(), :tick)
{:ok, assign(socket, time: Time.utc_now())}
end
def handle_info(:tick, socket) do
{:noreply, assign(socket, time: Time.utc_now())}
end
然后在模板中显示 @time。
组件化与函数组件
LiveView 推崇使用无状态的函数组件来构建可复用的 UI 片段。Phoenix 1.7 中,函数组件定义在模块 MyLiveAppWeb.Components 或任意你引用的模块中。
示例:一个简单的卡片组件
defmodule MyLiveAppWeb.Components do
use Phoenix.Component
def card(assigns) do
~H"""
<div class="card">
<h3><%= @title %></h3>
<p><%= @body %></p>
</div>
"""
end
end
在模板中使用:
<Components.card title="LiveView" body="构建实时应用如此简单" />
与 JavaScript 的互操作
尽管 LiveView 的目标是减少 JS 依赖,但有时仍需使用现成的 JS 库或自定义脚本。
Hook 机制
通过声明 phx-hook 属性,可以挂载一个 JavaScript 对象到元素上。
在 assets/js/app.js 中定义 hook:
let Hooks = {}
Hooks.MapRenderer = {
mounted() {
// 使用 Leaflet 或 Google Maps 初始化地图
console.log("地图元素已挂载", this.el)
},
updated() {
// 数据更新时调用
}
}
let liveSocket = new LiveSocket("/live", Socket, { hooks: Hooks, ... })
在模板中:
<div id="map" phx-hook="MapRenderer" />
部署注意事项
LiveView 应用依赖持久的 WebSocket 连接,部署时需要注意:
- 反向代理配置:确保 Nginx 或 Caddy 正确代理 WebSocket 升级。Nginx 示例:
location /live { proxy_pass http://your_phx_server; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } - 负载均衡:由于每个 LiveView 进程是有状态的,需要使用支持 Phoenix Channel 的负载均衡策略,如使用
Phoenix.PubSub的节点间广播,或配置 IP 哈希(sticky sessions)。 - 分布式 PubSub:多节点部署时,将 PubSub 适配器从
Phoenix.PubSub.PG2切换到Phoenix.PubSub.Redis或使用pg组来实现跨节点通信。
总结与最佳实践
- 尽量保持 LiveView 简单:将业务逻辑放在 Context 模块中,LiveView 只负责 UI 状态。
- 谨慎使用 assigns:大量赋值会增加内存和网络开销,必要时使用
stream指令分页加载。 - 利用
connected?/1:区分初始服务端渲染和后续的 WebSocket 更新,避免不必要的资源消耗。 - 错误处理:使用
try/rescue或在handle_event中返回{:noreply, put_flash(socket, :error, "出错了")}来展示友好提示。 - 性能监控:使用 Phoenix LiveDashboard 查看 LiveView 的渲染时间、内存占用。
LiveView 让开发者能够以极简的 Elixir 代码构建出高度交互的实时 Web 应用,是提升生产力的利器。现在,开始你的实时之旅吧!