查看源代码 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_navigate
和 push_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 执行其自己的补丁操作之前,会将 fromEl
和 toEl
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 在推送事件的同时重定向,回调将不会在旧页面的元素上调用。回调将在重定向页面新挂载的钩子元素上调用。