Phoenix 1.7.0 發佈:內建 Tailwind、驗證路由、LiveView 串流,接下來是什麼

由 Chris McCord 在 2023 年 2 月 24 日發佈


Phoenix 1.7 的最後版本出爐!Phoenix 1.7 包含許多期待已久的全新功能,例如驗證路由、Tailwind 支援、LiveView 驗證產生器、統一 HEEx 範本、最佳化集合的 LiveView 串流,以及更多。這是一個向下相容的版本,有一些棄用函式。大多數人只要變更幾個相依性就能更新。

注意:若要產生新的 1.7 專案,您需要從 hex 安裝 phx.new 產生器

mix archive.install hex phx_new

驗證路由

驗證路由使用基於 sigil (~p) 的編譯時期驗證方法,取代路由輔助工具。

注意:驗證路由使用新的 Elixir 1.14 編譯器功能。Phoenix 仍支援舊版 Elixir,但您需要更新才能享用新的編譯時期驗證功能。

實際上,這表示您以前使用自動產生的函式,例如

  # router
  get "/oauth/callbacks/:id", OAuthCallbackController, :new

  # usage
  MyRouter.Helpers.o_auth_callback_path(conn, :new, "github")
  # => "/oauth/callbacks/github"

  MyRouter.Helpers.o_auth_callback_url(conn, :new, "github")
  # => "https://127.0.0.1:4000/oauth/callbacks/github"

現在可以執行

  # router
  get "/oauth/callbacks/:id", OAuthCallbackController, :new

  # usage
  ~p"/oauth/callbacks/github"
  # => "/oauth/callbacks/github"

  url(~p"/oauth/callbacks/github")
  # => "https://127.0.0.1:4000/oauth/callbacks/github"

這有許多優點。不用再猜測要使用哪個函式,例如 Helpers.oauth_callback_patho_auth_callback_path 等等。您也不再需要處處包含 %Plug.Conn{}%Phoenix.Socket{} 或端點模組,因為在 99% 的情況下您知道應該使用哪個端點組態。

您現在可以一一對應路由中編寫的路由,以及使用 ~p 呼叫路由的方法。您只要將它寫成在 app 中每處都硬編碼字串的樣子,不同之處在於沒有硬編碼字串會帶來的維護問題。我們可以使用 ~p,根據路由中的路由進行編譯時期驗證,輕鬆兼得方便性和維護性。

例如,想像我們拼錯路由

<.link href={~p"/userz/profile"}>Profile</.link>

編譯器會在編譯時期針對您的路由傳遞所有 ~p,並在找不到相符路由時通知您

    warning: no route path for AppWeb.Router matches "/postz/#{post}"
      lib/app_web/live/post_live.ex:100: AppWeb.PostLive.render/1

動態「命名參數」也會像一般字串那樣內插,而不是使用任意函式參數

~p"/posts/#{post.id}"

此外,內插的 ~p 值會透過 Phoenix.Param 協定編碼。例如,應用程式中的 %Post{} 結構可以衍生 Phoenix.Param 協定,產生基於 slug 的路徑,而不是基於 ID 的路徑。這樣您就可以在應用程式中使用 ~p"/posts/#{post}",而不是 ~p"/posts/#{post.slug}"

驗證路由也支援查詢字串,可以是傳統的查詢字串格式

~p"/posts?page=#{page}"

或關鍵字清單或值對應

params = %{page: 1, direction: "asc"}
~p"/posts?#{params}"

與路徑區段類似,查詢字串參數會適當地進行 URL 編碼,並可能直接插補到 ~p 字串中。

一旦你試用新功能,你將無法再回到路由輔助程式了。新的 phx.gen.html|live|json|auth 產生器使用已驗證的路由。

基於元件的 Tailwind 產生器

Phoenix 1.7 預設搭載 TailwindCSS,且不依賴系統中的 nodejs。在我 20 年的網路開發經驗中,TailwindCSS 是我發現用於為介面造型的最佳方式。相較於我曾使用的任何 CSS 系統或架構,其實用工具優先的作法更具可維護性和生產力。其並置的作法也完美地符合功能元件和 LiveView 的架構。

Tailwind 團隊也很慷慨地為新的專案設計了新的專案登陸頁面、CRUD 頁面和驗證系統頁面,為你的應用程式建置提供一流且完善的起點。

新的 phx.new 專案將包含一個 CoreComponents 模組,其中包含一組 UI 核心元件,例如表格、模態框、表單和資料清單。Phoenix 產生器套件(phx.gen.html|live|json|auth)使用核心元件。這擁有許多絕佳的優點。

首先,你可以自訂核心 UI 元件,以滿足任何你需要、設計和品味。如果你想使用 Bulma 或 Bootstrap,而不是 Tailwind ── 沒問題!只要使用你的架構/特定 UI 的實作取代 core_components.ex 中的功能定義,產生器便會持續提供一個絕佳的起點,無論你是一位初學者,或是有豐富經驗的專家建置客製化產品功能。

實際上,產生器會提供使用你的核心元件的範本,如下所示

<.header>
  New Post
  <:subtitle>Use this form to manage post records in your database.</:subtitle>
</.header>

<.simple_form for={@form} action={~p"/posts"}>
  <input field={@form[:title]} type="text" label="Title" />
  <input field={@form[:views]} type="number" label="Views" />

  <:actions>
    <.button>Save Post</.button>
  </:actions>
</.simple_form>

<.back navigate={~p"/posts"}>Back to posts></.back>

我們很喜歡 Tailwind 團隊為新應用程式設計的內容,但也迫不及待想看到社群釋出他們自己的 core_components.ex 安裝程式替代方案,供各種選擇架構使用。

跨控制器和 LiveView 的統一功能元件

HEEx 提供的功能元件,具有宣示指派和位置,為在 Phoenix 專案中撰寫 HTML 的方式帶來重大的步驟變革。功能元件提供 UI 建構區塊,讓功能封裝起來,並比之前的 Phoenix.View 範本方法有更好的擴充性。你獲得了撰寫動態標記的更自然方式、可重複使用的 UI(呼叫者可以擴充),以及編譯時期功能,讓撰寫基於 HTML 的應用程式成為真正的首要體驗。

Phoenix.View 功能元件帶來撰寫 Phoenix HTML 應用程式的新方式,並設定了新的慣例。此外,使用者會對於如何將以控制器為基礎的 Phoenix.View 功能與應用程式中的 Phoenix.LiveView 功能結合而苦惱。使用者發現自己會在以控制器為基礎的範本中撰寫 render("table", user: user),而他們的 LiveView 則使用新 <.table rows={@users}> 功能。在應用程式中並未有絕佳的方式來分享這些做法。

基於這些原因,Phoenix 團隊統一了 HTML 顯示方式,無論是來自控制器要求或 LiveView。這個改變也讓我們能夠重新檢視慣例,並配合 LiveView 將範本和應用程式程式碼放在一起的做法。

新的應用程式(以及 phx 產生器)移除 Phoenix.View 作為相依性,並偏好新的 Phoenix.Template 相依性,這個相依性使用函式元件作為此架構中所有顯示的基礎。

控制器的樣式維持不變

defmodule AppWeb.UserController do
  use MyAppWeb, :controller

  def index(conn, _params) do
    users = ...
    render(conn, :index, users: users)
  end
end

但是,控制器不再呼叫 AppWeb.UserView.render("index.html", assigns),現在我們會先在檢視模組中尋找 index/1 函式元件,如果找到,則呼叫該元件執行顯示。此外,我們也重新命名轉換的檢視模組,以尋找 AppWeb.UserHTMLAppWeb.UserJSON,等等,以採用針對特定格式檢視範本的做法。這一切都以向後相容的方式執行,並根據 use Phoenix.Controller 的選項進行選擇。

所有 HTML 顯示都是根據函式元件執行,這些元件可以直接在模組中撰寫,或以 Phoenix.Component 提供的新 embed_templates 巨集從外部檔案嵌入。新的應用程式中的 PageHTML 模組看起來如下所示

defmodule AppWeb.PageHTML do
  use AppWeb, :html

  embed_templates "page_html/*"
end

新的目錄結構看起來會像這樣

lib/app_wb
├── controllers
│   ├── page_controller.ex
│   ├── page_html.ex
│   ├── error_html.ex
│   ├── error_json.ex
│   └── page_html
│       └── home.html.heex
├── live
│   ├── home_live.ex
├── components
│   ├── core_components.ex
│   ├── layouts.ex
│   └── layouts
│       ├── app.html.heex
│       └── root.html.heex
├── endpoint.ex
└── router.ex

現在,你的控制器顯示或 LiveView 顯示都使用相同的函式元件和配置。執行 phx.gen.htmlphx.gen.livephx.gen.auth 時,新產生的範本都會使用你的 components/core_components.ex 定義。

此外,我們將檢視模組並列放置在其控制器檔案旁邊。這帶來了 LiveView 並置帶來的相同好處-高度關聯的檔案放在一起。必須一起變更的檔案現在放在一起,無論是撰寫 LiveView 或控制器功能。

這些變更都是為了改善撰寫 HTML 基礎應用程式的優化方式,但它們也簡化了其他格式的呈現,例如 JSON。例如,基於 JSON 的檢視模組遵循相同的慣例 – 在嘗試 render/2 之前,Phoenix 會在呈現索引範本時先尋找 index/1 函數。這讓我們可以簡化一般的 JSON 呈現,並消除 Phoenix.View.render_one|render_many 等概念。

例如,這是由 phx.gen.json 產生的 JSON 檢視

defmodule AppWeb.PostJSON do
  alias AppWeb.Blog.Post

  @doc """
  Renders a list of posts.
  """
  def index(%{posts: posts}) do
    %{data: for(post <- posts, do: data(post))}
  end

  @doc """
  Renders a single post.
  """
  def show(%{post: post}) do
    %{data: data(post)}
  end

  defp data(%Post{} = post) do
    %{
      id: post.id,
      title: post.title
    }
  end
end

請注意,它如何全部只是常規 Elixir 函數 - 它應該如此!

這些功能透過一種採用新的且進步的方式來撰寫 UI 的方式,為應用程式提供統一的呈現模型,但它們與先前的作法有所不同。大多數大型且已建立的應用程式最適合持續依賴 Phoenix.View

LiveView 串流

LiveView 現在包含用於管理 UI 中的大型集合的串流介面,而無需將集合儲存在伺服器的記憶體中。透過呼叫幾個函數,您可以在 UI 中插入新的項目,動態追加或前置,或重新排列項目,而無需在伺服器上重新載入這些項目。

Phoenix 1.7 中的 phx.gen.live live CRUD 產生器使用串流來管理您的項目清單。這允許資料輸入、更新和刪除,而無需在初始載入後重新擷取項目清單。讓我們看看如何進行。

當您執行 mix phx.gen.live Blog Post posts title views:integer 時,將產生下列 PostLive.Index 模組

defmodule DemoWeb.PostLive.Index do
  use DemoWeb, :live_view

  alias Demo.Blog
  alias Demo.Blog.Post

  @impl true
  def mount(_params, _session, socket) do
    {:ok, stream(socket, :posts, Blog.list_posts())}
  end

  ...
end

請注意,我們有一個新的 stream/3 介面,而不是常規的 assign(socket, :posts, Blog.list_posts())。這會設定具有初始文章集合的串流。然後,在產生的 index.html.heex 範本中,我們會使用串流來呈現文章表格

<.table
  id="posts"
  rows={@streams.posts}
  row_click={fn {_id, post} -> JS.navigate(~p"/posts/#{post}") end}
>
  <:col :let={{_id, post}} label="Title"><%= post.title %></:col>
  <:col :let={{_id, post}} label="Views"><%= post.views %></:col>
  <:action :let={{_id, post}}>
    <div class="sr-only">
      <.link navigate={~p"/posts/#{post}"}>Show</.link>
    </div>
    <.link patch={~p"/posts/#{post}/edit"}>Edit</.link>
  </:action>
  <:action :let={{id, post}}>
    <.link
      phx-click={JS.push("delete", value: %{id: post.id}) |> hide("##{id}")}
      data-confirm="Are you sure?"
    >
      Delete
    </.link>
  </:action>
</.table>

這看起來與舊範本非常相似,但我們將 @stream.posts 傳遞給我們的表格,而不是存取赤裸的 @posts 指定。使用串流時,我們也會收到串流的 DOM ID,連同項目一起。

回到伺服器,我們可以看到在表格中插入新項目是多麼簡單。當我們產生的 FormComponent 透過表單更新或儲存文章時,我們會將訊息包傳送給關於新的或已更新文章的父級 PostLive.Index LiveView

PostLive.FormComponent:

defmodule DemoWeb.PostLive.FormComponent do
  ...
  defp save_post(socket, :new, post_params) do
    case Blog.create_post(post_params) do
      {:ok, post} ->
        notify_parent({:saved, post})

        {:noreply,
         socket
         |> put_flash(:info, "Post created successfully")
         |> push_patch(to: socket.assigns.patch)}

      {:error, %Ecto.Changeset{} = changeset} ->
        {:noreply, assign_form(socket, changeset)}
    end
  end

  defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
end

然後我們在 PostLive.Index handle_info 子句中選取訊息

@impl true
def handle_info({DemoWeb.PostLive.FormComponent, {:saved, post}}, socket) do
  {:noreply, stream_insert(socket, :posts, post)}
end

所以,此表單告訴我們它儲存了一篇文章,而我們只要在串流中將文章串流插入即可。就是這樣!如果使用者介面中已經存在文章,就會在原處進行更新。否則,它會預設附加到容器中。您還可以透過stream_insert(socket, :posts, post, at: 0)進行前置新增,或將任何索引傳遞給:at以進行任意項目插入或重新排序。

串流是我們前往 LiveView 1.0 的最後一塊磚石,我很高興我們能順利抵達。

新的表單欄位資料結構

我們都很熟悉 Phoenix.HTML 表單的基本碼元 <.form for={@changeset}>,其中表單會接收實作Phoenix.HTML.FormData 協定的資料結構並傳回 %Phoenix.HTML.Form{}。我們的做法遇到一個問題是,表單資料結構無法追蹤個別表單欄位的變更。這讓 LiveView 中的最佳化變為不可能,因為每次個別變更都必須重新渲染並重新傳送表單。透過引入 Phoenix.HTML.FormData.to_formPhoenix.Component.to_form,我們現在針對個別欄位變更建立了 %Phoenix.HTML.FormField{} 資料結構。

新的 phx.gen.live 產生器和您的 core_components.ex 利用了這些新功能。

Phoenix 和 LiveView 的下一步是什麼

Phoenix 產生器利用了 LiveView 的最新功能,而且未來會持續擴充。隨著串流集合成為預設,我們可以朝向 Live CRUD 產生器(phx.gen.live)的現成進階功能邁進。例如,我們計畫為資源推出現成的同步使用者介面。已產生的 Phoenix 表單功能會持續演進,並加入新的to_form 介面。

對於 LiveView,to_form 讓我們能夠傳送最佳化表單的基礎。現在,個別變更只會產生一個最佳化差異。

在這個最佳化工作後,LiveView 1.0 的主要功能剩餘擴充表單 API,能更好支援動態表單輸入、精靈樣式表單和委派表單輸入給子級 Live 組件。

備用 Web 伺服器支援

多虧 Mat Trudel 的努力,我們現在有基礎能在 Plug 和 Phoenix 中提供一流的 Web 伺服器支援,讓使用者可以在 Phenix 中換用其他 Web 伺服器,例如 Bandit,同時享受 WebSockets、Channels 和 LiveView 等所有功能。如果您有興趣使用純 Elixir HTTP 伺服器或在您自己的 Phoenix 專案中試試 Bandit,請密切關注 Bandit 專案!

下一步

和往常一樣,分步升級指南 可以將您現有的 1.6.x 應用程式升級到 1.7。

您可以在此找到完整的變更紀錄。

如果您遇到問題,請在 Elixir slack 或論壇中找到我們。

編寫程式愉快!

-Chris