Phoenix LiveView 实时 Web 框架

FreeGuideOnline 16阅读 2026-07-10

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/2assign/3 将数据绑定到 socket,然后在模板(.heex)中使用 <%= @count %> 访问。模板是纯服务器端渲染的,首次渲染输出完整 HTML。

4. 事件处理

  • 客户端事件:如 phx-clickphx-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-changephx-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 应用,是提升生产力的利器。现在,开始你的实时之旅吧!