查看源代码 JavaScript 互操作性

为了启用 LiveView 客户端/服务器交互,我们需要实例化一个 LiveSocket。例如

import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"

let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}})
liveSocket.connect()

所有选项都直接传递给 Phoenix.Socket 构造函数,除了以下 LiveView 特定选项

  • bindingPrefix - 用于凤凰绑定(phoenix bindings)的前缀。默认值 "phx-"
  • params - 传递给视图 mount 回调的 connect_params。可以是字面量对象或返回对象的闭包。当提供闭包时,该函数会接收视图的元素。
  • hooks - 对用户定义的 hooks 命名空间的引用,包含用于服务器/客户端互操作的客户端回调。有关详细信息,请参见下面的 客户端 hooks 部分。
  • uploaders - 对用户定义的 uploaders 命名空间的引用,包含用于客户端直接上传到云的客户端回调。有关详细信息,请参见 外部上传指南

调试客户端事件

为了帮助在调试问题时调试客户端,enableDebug()disableDebug() 函数在 LiveSocket JavaScript 实例上公开。调用 enableDebug() 会开启调试日志记录,其中包括 LiveView 生命周期和有效负载事件,这些事件在从客户端到服务器的来回过程中发生。在实践中,您可以将实例公开在 window 上,以便在浏览器的 Web 控制台中快速访问,例如

// app.js
let liveSocket = new LiveSocket(...)
liveSocket.connect()
window.liveSocket = liveSocket

// in the browser's web console
>> liveSocket.enableDebug()

调试状态使用浏览器的内置 sessionStorage,因此它将在浏览器会话持续的时间内保持有效。

模拟延迟

正确处理延迟对于良好的 UX 至关重要。LiveView 的 CSS 加载状态允许客户端在等待服务器响应时提供用户反馈。在开发过程中,localhost 上的几乎零延迟不允许轻松地表示或测试延迟,因此 LiveView 在 JavaScript 客户端中包含了一个延迟模拟器,以确保您的应用程序提供愉快的体验。与上面的 enableDebug() 函数类似,LiveSocket 实例包含 enableLatencySim(milliseconds)disableLatencySim() 函数,这些函数适用于当前浏览器会话。 enableLatencySim 函数接受一个以毫秒为单位的整数,表示到服务器的往返时间。例如

// app.js
let liveSocket = new LiveSocket(...)
liveSocket.connect()
window.liveSocket = liveSocket

// in the browser's web console
>> liveSocket.enableLatencySim(1000)
[Log] latency simulator enabled for the duration of this browser session.
      Call disableLatencySim() to disable

事件监听器

LiveView 向浏览器发出几个事件,并允许开发人员提交自己的事件。

实时导航事件

对于通过 <.link navigate={...}><.link patch={...}> 进行的实时页面导航,它们的服务器端等效项 push_navigatepush_patch,以及通过 phx-submit 提交的表单,JavaScript 事件 "phx:page-loading-start""phx:page-loading-stop" 会在 window 上分发。此外,任何 phx- 事件都可以通过使用 phx-page-loading 对 DOM 元素进行注释来分发页面加载事件。这对于显示主页面加载状态很有用,例如

// app.js
import topbar from "topbar"
window.addEventListener("phx:page-loading-start", info => topbar.show())
window.addEventListener("phx:page-loading-stop", info => topbar.hide())

在回调中,info.detail 将是一个包含 kind 键的对象,其值取决于触发事件

  • "redirect" - 该事件由重定向触发
  • "patch" - 该事件由补丁触发
  • "initial" - 该事件由初始页面加载触发
  • "element" - 该事件由绑定了 phx- 的元素触发,例如 phx-click
  • "error" - 该事件由错误触发,例如视图崩溃或套接字断开连接

对于所有类型的页面加载事件,除了 "element" 之外,所有事件都会在 info 元数据中接收一个额外的 to 键,指向与页面加载相关的 href。

"element" 页面加载事件的情况下,info 将包含一个 "target" 键,其中包含触发页面加载状态的 DOM 元素。

更低级的 phx:navigate 事件也会在浏览器通过 Phoenix 编程方式更改 URL 栏或用户在历史记录中向前或向后导航时触发。 info.detail 将包含以下信息

  • "href" - URL 栏导航到的位置。
  • "patch" - 指示这是补丁导航的布尔标志。
  • "pop" - 指示这是通过 popstate 从用户在历史记录中向前或向后导航进行的导航的布尔标志。

处理服务器推送的事件

当服务器使用 Phoenix.LiveView.push_event/3 时,事件名称将在浏览器中使用 phx: 前缀分发。例如,想象以下模板,您希望从服务器突出显示现有元素以引起用户的注意

<div id={"item-#{item.id}"} class="item">
  <%= item.title %>
</div>

接下来,服务器可以使用标准的 push_event 发出突出显示。

def handle_info({:item_updated, item}, socket) do
  {:noreply, push_event(socket, "highlight", %{id: "item-#{item.id}"})}
end

最后,窗口事件监听器可以监听该事件,并在元素匹配时有条件地执行突出显示命令。

let liveSocket = new LiveSocket(...)
window.addEventListener("phx:highlight", (e) => {
  let el = document.getElementById(e.detail.id)
  if(el) {
    // logic for highlighting
  }
})

如果您希望,您还可以将此功能与 Phoenix 的 JS 命令集成,在每次触发突出显示时为给定元素执行 JS 命令。首先,更新元素以将 JS 命令嵌入到数据属性中

<div id={"item-#{item.id}"} class="item" data-highlight={JS.transition("highlight")}>
  <%= item.title %>
</div>

现在,在事件监听器中,使用 LiveSocket.execJS 触发新属性中的所有 JS 命令。

let liveSocket = new LiveSocket(...)
window.addEventListener("phx:highlight", (e) => {
  document.querySelectorAll(`[data-highlight]`).forEach(el => {
    if(el.id == e.detail.id){
      liveSocket.execJS(el, el.getAttribute("data-highlight"))
    }
  })
})

通过 phx-hook 使用客户端 hooks

为了在服务器添加、更新或删除元素时处理自定义的客户端 JavaScript,可以使用 phx-hook 提供一个 hooks 对象。 phx-hook 必须指向一个具有以下生命周期回调的对象

  • mounted - 元素已添加到 DOM,并且其服务器 LiveView 已完成安装
  • beforeUpdate - 元素即将在 DOM 中更新。注意:这里的所有调用都必须是同步的,因为操作不能延迟或取消。
  • updated - 元素已由服务器在 DOM 中更新
  • destroyed - 元素已从页面中删除,无论是通过父级更新,还是通过父级完全删除
  • disconnected - 元素的父 LiveView 已断开与服务器的连接
  • reconnected - 元素的父 LiveView 已重新连接到服务器

注意:当在 LiveView 上下文之外使用 hooks 时,mounted 是唯一调用的回调,并且只有 DOM 就绪时页面上的那些元素会被跟踪。对于 DOM 的动态跟踪,例如添加、删除和更新元素,应该使用 LiveView。

上述生命周期回调具有对以下属性的范围内访问权限

  • el - 引用绑定 DOM 节点的属性
  • liveSocket - 对底层 LiveSocket 实例的引用
  • pushEvent(event, payload, (reply, ref) => ...) - 用于从客户端向 LiveView 服务器推送事件的方法
  • pushEventTo(selectorOrTarget, event, payload, (reply, ref) => ...) - 用于从客户端向 LiveView 和 LiveComponent 推送目标事件的方法。它将事件发送到 selectorOrTarget 定义的 LiveComponent 或 LiveView,其中其值可以是查询选择器或实际的 DOM 元素。如果查询选择器返回多个元素,它将向所有元素发送事件,即使所有元素都在同一个 LiveComponent 或 LiveView 中。 pushEventTo 支持传递节点元素(例如 this.el)而不是选择器(例如 "#" + this.el.id)作为第一个参数用于目标。
  • handleEvent(event, (payload) => ...) - 用于处理从服务器推送的事件的方法
  • upload(name, files) - 用于将文件对象列表注入上传器的方法。
  • uploadTo(selectorOrTarget, name, files) - 用于将文件对象列表注入上传器的方法。该 hooks 将文件发送到服务器端由 allow_upload/3 定义的 name 的上传器。分发新的上传会触发一个输入更改事件,该事件将被发送到 selectorOrTarget 定义的 LiveComponent 或 LiveView,其中其值可以是查询选择器或实际的 DOM 元素。如果查询选择器返回多个实时文件输入,则会记录错误。

例如,用于控制电话号码格式的输入的标记可以这样写

<input type="text" name="user[phone_number]" id="user-phone-number" phx-hook="PhoneNumber" />

然后可以定义一个 hooks 回调对象并将其传递给套接字

/**
 * @type {Object.<string, import("phoenix_live_view").ViewHook>}
 */
let Hooks = {}
Hooks.PhoneNumber = {
  mounted() {
    this.el.addEventListener("input", e => {
      let match = this.el.value.replace(/\D/g, "").match(/^(\d{3})(\d{3})(\d{4})$/)
      if(match) {
        this.el.value = `${match[1]}-${match[2]}-${match[3]}`
      }
    })
  }
}

let liveSocket = new LiveSocket("/live", Socket, {hooks: Hooks, ...})
...

注意:当使用 phx-hook 时,必须始终设置唯一的 DOM ID。

为了与需要更广泛地访问完整 DOM 管理的客户端库集成,LiveSocket 构造函数接受一个带有 onBeforeElUpdated 回调的 dom 选项。在 LiveView 执行其自己的补丁操作之前,会将 fromEltoEl DOM 节点传递给该函数。这允许外部库在 LiveView 执行其自己的补丁操作时根据需要(重新)初始化 DOM 元素或复制属性。更新操作不能取消或延迟,并且返回值会被忽略。

例如,以下选项可用于确保在客户端设置的某些属性保持不变

...
let liveSocket = new LiveSocket("/live", Socket, {
  params: {_csrf_token: csrfToken},
  hooks: Hooks,
  dom: {
    onBeforeElUpdated(from, to) {
      for (const attr of from.attributes) {
        if (attr.name.startsWith("data-js-")) {
          to.setAttribute(attr.name, attr.value);
        }
      }
    }
  }
}

在上面的示例中,所有以 data-js- 开头的属性在 LiveView 补丁 DOM 时不会被替换。

客户端-服务器通信

hooks 可以通过使用 pushEvent 函数向 LiveView 推送事件,并通过 {:reply, map, socket} 返回值从服务器接收回复。回复有效负载将传递给可选的 pushEvent 响应回调。

服务器可以从 hooks 通信,方法是读取 hooks 元素上的数据属性或在服务器上使用 Phoenix.LiveView.push_event/3 以及在客户端使用 handleEvent

例如,要实现无限滚动,可以将当前页面使用数据属性传递

<div id="infinite-scroll" phx-hook="InfiniteScroll" data-page={@page}>

然后在客户端

/**
 * @type {import("phoenix_live_view").ViewHook}
 */
Hooks.InfiniteScroll = {
  page() { return this.el.dataset.page },
  mounted(){
    this.pending = this.page()
    window.addEventListener("scroll", e => {
      if(this.pending == this.page() && scrollAt() > 90){
        this.pending = this.page() + 1
        this.pushEvent("load-more", {})
      }
    })
  },
  updated(){ this.pending = this.page() }
}

但是,如果需要频繁地向客户端推送数据,则数据属性方法不是一种好方法。要向客户端推送带外事件(例如,渲染图表点),可以执行以下操作

<div id="chart" phx-hook="Chart">
{:noreply, push_event(socket, "points", %{points: new_points})}

然后在客户端

/**
 * @type {import("phoenix_live_view").ViewHook}
 */
Hooks.Chart = {
  mounted(){
    this.handleEvent("points", ({points}) => MyChartLib.addPoints(points))
  }
}

通过 push_event 从服务器推送的事件是全局的,并将分发到正在处理该事件的客户端上的所有活动 hooks。如果您需要对事件进行范围限定(例如,从具有当前实时视图上的兄弟节点的实时组件推送时),则必须通过命名空间进行限定

def update(%{id: id, points: points} = assigns, socket) do
  socket =
    socket
    |> assign(assigns)
    |> push_event("points-#{id}", points)

  {:ok, socket}
end

然后在客户端

Hooks.Chart = {
  mounted(){
    this.handleEvent(`points-${this.el.id}`, (points) => MyChartLib.addPoints(points));
  }
}

注意:如果 LiveView 推送事件并渲染内容,handleEvent 回调将在页面更新后调用。因此,如果 LiveView 在推送事件的同时重定向,回调将不会在旧页面的元素上调用。回调将在重定向页面新挂载的钩子元素上调用。