檢視原始碼 元件和 HEEx

需求:本指南希望您已完成 入門指南 並且讓 Phoenix 應用程式 成功執行

需求:本指南希望您已完成 請求生命週期指南

Phoenix 端點管線會接收請求,將其路由到控制器,並呼叫檢視模組來呈述範本。控制器中的檢視介面很簡單,由控制器呼叫連線變數的檢視函式,而函式的工作是回傳 HEEx 範本。我們呼叫任何接受 變數 參數並回傳 HEEx 範本的函式為函式元件。函式元件的定義是透過 Phoenix.Component 模組來達成。

函式元件是您在 Phoenix 中執行任何類型的標記語言範本呈述的基本構成模組。它們是標準 MVC 控制器為基礎的應用程式、LiveView 應用程式、配置範本以及您會在其他範本中使用的較小 UI 定義的共用抽象。

在本章節中,我們會回顧元件在先前章節中的使用方式並找出新的使用案例。

函式元件

在請求生命週期的章節最後,我們在 lib/hello_web/controllers/hello_html/show.html.heex 中建立了一個範本,讓我們來開啟它

<section>
  <h2>Hello World, from <%= @messenger %>!</h2>
</section>

這個範本是嵌入於 HelloHTML 中,位置位於 lib/hello_web/controllers/hello_html.ex

defmodule HelloWeb.HelloHTML do
  use HelloWeb, :html

  embed_templates "hello_html/*"
end

這很簡單,只有兩行,use HelloWeb, :html。這行呼叫 HelloWeb 中定義的 html/0 函式,用於設定我們的函式元件和範本的基本匯入和組態。

我們在模組中做出的匯入和別名也都會在我們的範本中生效。這是因為範本會有效地編譯成在其各自模組中的函式。例如,如果您在模組中定義函式,您就能直接從範本呼叫它。讓我們看看這如何實作。

想像我們想要對 show.html.heex 進行重構,以將 <h2>Hello World, from <%= @messenger %>!</h2> 的呈述移到它自己的函式中。我們可以將它移到 HelloHTML 中的函式元件中,我們就這樣做

defmodule HelloWeb.HelloHTML do
  use HelloWeb, :html

  embed_templates "hello_html/*"

  attr :messenger, :string, required: true

  def greet(assigns) do
    ~H"""
    <h2>Hello World, from <%= @messenger %>!</h2>
    """
  end
end

我們透過Phoenix.Component提供的attr/3巨集宣告我們接受的屬性,然後定義greet/1函數,回傳HEEx範本。

接下來我們需要更新show.html.heex

<section>
  <.greet messenger={@messenger} />
</section>

當我們重新載入https://127.0.0.1:4000/hello/Frank時,我們應該會看到和先前相同的內容。

由於範本是嵌入在HelloHTML模組裡,因此我們能夠直接呼叫檢視函數,如<.greet messenger="..." />

如果組件定義在其他地方,我們也可以輸入<HelloWeb.HelloHTML.greet messenger="..." />

透過宣告屬性為必要的,如果我們呼叫<.greet />組件而沒有傳遞屬性時,Phoenix會在編譯時發出警告。如果某個屬性是可選的,你可以指定:default選項加上一個值

attr :messenger, :string, default: nil

雖然這是一個簡單的範例,它展示了Phoenix中函數組件扮演的不同角色

  • 函數組件可以定義為接收assigns作為引數的函數,並呼叫~H標記,正如我們在greet/1中所做的一樣

  • 函數組件可以從範本檔案嵌入,這就是我們載入show.html.heexHelloWeb.HelloHTML的方式

  • 函數組件可以宣告預期的屬性,這些屬性會在編譯時驗證

  • 函數組件可以從控制器直接渲染

  • 函數組件可以從其他函數組件直接渲染,正如我們從show.html.heex呼叫<.greet messenger={@messenger} />一樣

而還有更多。在我們深入探討之前,讓我們先徹底了解HEEx範本語言背後的表示能力。

HEEx

函數組件和範本檔案是由HEEx範本語言驅動的,其中HEEx代表「HTML+EEx」。EEx是一個Elixir函式庫,它使用<%= expression %>來執行Elixir表達式,並將其結果插入到範本中。這通常用於顯示我們透過@捷徑所設定的assigns。在你的控制器中,如果你呼叫

  render(conn, :show, username: "joe")

接下來你就可以在範本中存取上述的使用者名稱,方式為<%= @username %>。除了顯示assigns和函數外,我們幾乎可以使用任何Elixir表達式。例如,為了要有條件判斷

<%= if some_condition? do %>
  <p>Some condition is true for user: <%= @username %></p>
<% else %>
  <p>Some condition is false for user: <%= @username %></p>
<% end %>

甚至是迴圈

<table>
  <tr>
    <th>Number</th>
    <th>Power</th>
  </tr>
  <%= for number <- 1..10 do %>
    <tr>
      <td><%= number %></td>
      <td><%= number * number %></td>
    </tr>
  <% end %>
</table>

你注意到上面 <%= %><% %> 的用法差異嗎?所有輸出內容至範本的表達式必須使用等號 (=)。如果未包含等號,程式碼仍會執行,但不會產生任何輸出結果。

HEEx 也具備我們接下來將學習的實用 HTML 延伸功能。

HTML 延伸功能

除了允許透過 <%= %> 內插 Elixir 表達式外,.heex 範本還具備具 HTML 感知能力的延伸功能。例如,讓我們看看在其中內插含有 "<" 或 ">" 的值會發生什麼情況,這會導致 HTML 注入

<%= "<b>Bold?</b>" %>

當你渲染範本時,你會看到字面上的 <b> 出現在頁面上。這表示使用者無法在頁面上注入 HTML 內容。如果你想要允許他們這麼做,你可以呼叫 raw,但必須非常謹慎

<%= raw "<b>Bold?</b>" %>

HEEx 範本的另一個超能力是驗證 HTML 和屬性的精簡內插語法。你可以撰寫

<div title="My div" class={@class}>
  <p>Hello <%= @username %></p>
</div>

請注意,你可以簡單地使用 key={value}。HEEx 會自動處理特殊值(例如 false),以移除屬性或類別清單。

若要內插一組動態屬性至關鍵字清單或對映,請執行

<div title="My div" {@many_attributes}>
  <p>Hello <%= @username %></p>
</div>

另外,請嘗試移除關閉 </div>,或將其重新命名為 </div-typo>。HEEx 範本將會告知你錯誤。

HEEx 也支援透過特殊屬性 :if:for 的精簡語法表示 iffor 表達式。例如,使用此方式替代以下範例

<%= if @some_condition do %>
  <div>...</div>
<% end %>

你可以撰寫

<div :if={@some_condition}>...</div>

同樣地,對於推論式,可以撰寫為

<ul>
  <li :for={item <- @items}><%= item.name %></li>
</ul>

配置

配置僅是函式組件。它們是在模組中定義的,就像其他所有函式組件範本一樣。在新生成的應用程式中,這是 lib/hello_web/components/layouts.ex。你還會找到 layouts 資料夾,其中包含兩個由 Phoenix 生成的內建配置。預設的根配置稱為 root.html.heex,這是預設會將所有範本渲染至其中的配置。第二個是應用程式配置,稱為 app.html.heex,會在根配置中渲染,並包含我們的內容。

你可能會想知道,從渲染檢視產生的字串是如何進入配置中。這是一個好問題!如果我們檢視 lib/hello_web/components/layouts/root.html.heex,在 <body> 的結尾附近會看到下面這段。

<%= @inner_content %>

換句話說,渲染你的頁面後,結果會放入 @inner_content 中。

Phoenix 提供了各式各樣的便利方式來控制要呈現哪種版面。例如,Phoenix.Controller 模組提供了 put_root_layout/2 函式供我們切換根版面。此函式的第一個參數為 conn,第二個參數則為格式及其版面的關鍵字清單。你可以將它設為 false 來停用版面。

你可以編輯 lib/hello_web/controllers/hello_controller.exHelloControllerindex 動作,如下所示。

def index(conn, _params) do
  conn
  |> put_root_layout(html: false)
  |> render(:index)
end

重新載入 https://127.0.0.1:4000/hello 後,我們應該會看到一個截然不同的頁面,這個頁面完全沒有標題或 CSS 造型。

如要自訂應用程式版面,我們會呼叫一個名為 put_layout/2 類似函式。實際上,我們來建立另一個版面並將索引範本呈現在其中。舉例來說,我們可以為應用程式的管理員區段製作一個不同的版面,其中沒有標誌影像。為執行這項操作,請將現有的 app.html.heex 複製到同一個目錄 lib/hello_web/components/layouts 中的新檔案 admin.html.heex。接著移除新檔案中 <header>...</header> 標籤內的全部內容(或變更為你想要的內容)。

現在,在 lib/hello_web/controllers/hello_controller.ex 控制器中的 index 動作中加入下列內容

def index(conn, _params) do
  conn
  |> put_layout(html: :admin)
  |> render(:index)
end

載入頁面時,我們應該會呈現沒有標頭(或你編寫的自訂標頭)的管理員版面。

在這個階段,你可能會好奇,Phoenix 為什麼要有兩個版面?

首先,這讓我們能夠靈活運用。實際上,我們不太可能有多個根版面,因為它們通常只包含 HTML 標頭。這讓我們可以將心力放在不同的應用程式版面,以及它們之間變更的部分。其次,Phoenix 附帶了一個名為 LiveView 的功能,讓我們能夠藉由伺服器呈現的 HTML 建立豐富且即時的使用者體驗。LiveView 能夠動態變更頁面內容,但它只會變更應用程式版面,絕不會變更根版面。查看LiveView 文件以進一步了解。

核心組件

在一個新的 Phoenix 應用程式中,你還可以在 components 資料夾中找到一個 core_components.ex 模組。這個模組是定義函式組件的範例,可重複用於整個應用程式中。藉由這個做法,當應用程式持續演進時,我們的組件也能持續保持一致的樣貌。

如果您查看設在 lib/hello_web.ex 中的 HelloWebdef html,您會看到 CoreComponents 已透過 use HelloWeb, :html 自動匯入至所有 HTML 檢視。這也是 CoreComponents 自身在最上方執行 use Phoenix.Component 而不是 use HelloWeb, :html 的原因:後者的作法會造成死結,因為我們會試圖將 CoreComponents 匯入自身。

由於產生器代碼假設那些元件可用來快速建立架構應用程式,所以 CoreComponents 在 Phoenix 產生器代碼中也會扮演重要的角色。如果您想要進一步了解所有這些區塊,您可以