檢視原始碼 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 繫結的前置字。預設值 "phx-"
  • params - 要傳遞給檢視 mount 回呼的 connect_params。可能是文字物件或傳回物件的閉包。當提供閉包時,這個函式會接收檢視的元素。
  • hooks – 對使用者定義的 hooks 名稱空間的參照,其中包含伺服器/客戶端交互操作的客戶端回呼。有關詳細資訊,請參閱下方的 客戶端 hooks 區段。
  • uploaders – 對使用者定義的 uploaders 名稱空間的參照,其中包含客戶端側,直接上傳到雲端的客戶端回呼。有關詳細資訊,請參閱 外部上傳指南

偵錯客戶端事件

為了解決問題時幫助客戶端進行偵錯,enableDebug()disableDebug() 函式會顯示在 LiveSocket JavaScript 執行個體上。呼叫 enableDebug() 會開啟偵錯記錄,其中包括當 LiveView 生命週期和酬載事件在客戶端與伺服器之間往返時。在實務上,您可以將執行個體顯示在 window 上,以直接在瀏覽器的網頁控制台存取,例如

// 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 會將多個事件發射到瀏覽器,並允許開發人員提交自己的事件。

Live 導覽事件

對於透過 <.link navigate={...}><.link patch={...}> 進行的即時頁面導覽,其伺服器端對應項 push_navigatepush_patch,以及透過 phx-submit 提交的表單,JavaScript 事件 "phx:page-loading-start""phx:page-loading-stop" 會在視窗上傳遞。此外,任何 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",所有其他事件都會在資訊中繼資料中收到另一個 to 鍵,指向與頁面載入相關的 href。

對於 "element" 頁面載入事件,資訊會包含一個 "target" 鍵,其中包含觸發頁面載入狀態的 DOM 元素。

較低層級的 phx:navigate 事件也會在 Phoenix 或使用者向前或向後導覽時,觸發瀏覽器的網址列出現程式設計化的變更。 info.detail 會包含以下資訊

  • "href" - 網址列導覽到的位置。
  • "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

最後,window 活動偵聽器可以偵聽事件,並在元素符合條件時執行高亮顯示指令

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 來進行使用者端掛鉤

在元素由伺服器新增、更新或移除時,要處理自訂使用者端的 JavaScript,可以使用掛鉤物件透過 phx-hook 來提供。 phx-hook 必須指向具有下列生命週期回呼的物件

  • mounted - 元素已新增至 DOM,且其伺服器 LiveView 的安裝已完成
  • beforeUpdate - 元素將於 DOM 中更新。注意:在此處的任何呼叫都必須為同步,因為操作無法延遲或取消。
  • updated - 元素已由伺服器在 DOM 中更新
  • destroyed - 元素已從頁面移除,可能是由父更新移除,或由父代完全移除
  • disconnected - 元素的父 LiveView 已與伺服器斷線
  • reconnected - 元素的父 LiveView 已重新與伺服器連線

注意:在 LiveView 的內容外使用掛鉤時,mounted 是唯一會被呼叫的回呼,而且只有頁面上在 DOM 完成時存在的元素才會被追蹤。對於元素的動態追蹤,包括在新增、移除和更新時,應使用 LiveView。

以上生命週期回呼可以在範圍內存取以下屬性

  • el - 屬性參考已繫結的 DOM 節點
  • liveSocket - 底層 LiveSocket 實例的參考
  • pushEvent(event, payload, (reply, ref) => ...) - 從使用者端將事件推送到 LiveView 伺服器的函式
  • pushEventTo(selectorOrTarget, event, payload, (reply, ref) => ...) - 從使用者端將目標事件推送到 LiveViews 和 LiveComponents 的函式。它會將事件傳送至在 selectorOrTarget 裡定義的 LiveComponent 或 LiveView 中,其值可以是查詢選擇器或實際的 DOM 元素。如果查詢選擇器傳回一個以上的元素,它會將事件傳送至全部的元素,即使所有元素都在同一個 LiveComponent 或 LiveView 中。 pushEventTo 支援傳遞節點元素(例如 this.el)作為第一個目標參數,而不是選擇器(例如 "#" + this.el.id)。
  • handleEvent(event, (payload) => ...) - 處理從伺服器推動的事件的函式
  • upload(name, files) - 將類似檔案的物件清單注入到上傳器的方法。
  • uploadTo(selectorOrTarget, name, files) - 將類似檔案的物件清單注入到上傳器的方法。此掛鉤會將檔案傳送至上傳器,且 nameallow_upload/3 在伺服器端定義。傳送新上傳檔案會觸發輸入變更事件,並將傳送至 selectorOrTarget 所定義的 LiveComponent 或 LiveView,其值可以是查詢選擇器或實際的 DOM 元素。如果查詢選擇器回傳多個即時檔案輸入,則會記錄錯誤。

例如,用於電話號碼格式化的受控輸入的標記可以用以下方式撰寫

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

然後可以定義掛鉤回呼物件並傳遞至套接字

/**
 * @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 的 DOM 修補作業發生之前,fromEltoEl DOM 節點會傳遞至函式。如此一來,外部程式庫便可重新初始化 DOM 元素或複製必要屬性,同時 LiveView 自行執行修補作業。更新作業無法取消或延後,而回傳值也會被忽略。

例如,下列選項可以用來保證在客戶端端設定的某部份屬性保持完整

...
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- 開頭的屬性在 DOM 被 LiveView 修補時不會被替換。

客戶端與伺服器通訊

掛鉤可以使用 pushEvent 函式將事件推播至 LiveView,並透過 {:reply, map, socket} 回傳值從伺服器接收回應。回應酬載會傳遞至選用的 pushEvent 回應回呼。

可以透過在掛鉤元素上讀取資料屬性或是在伺服器上使用 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 從伺服器推播的事件為全域事件,且會派送給處理該事件的客戶端上所有活動的勾子。如果您需要範疇化事件(例如從目前即時檢視上具有兄弟組件的即時組件推播),這必須透過命名空間來完成

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));
  }
}

注意:萬一一個即時檢視推播事件並呈現內容,handleEvent 回呼會在頁面更新後呼叫。因此,如果即時檢視在推播事件的同時進行重新導向,回呼將不會在舊頁面的元素上呼叫。回呼會在重新導向頁面中新掛載的勾子元素上呼叫。