查看原始碼 上傳

LiveView 支援互動式檔案上傳,進度會顯示在直接至伺服器上傳和用戶端的 直接至雲端上傳 中。

內建功能

  • 接受規格 - 定義可接受的檔案類型、最大條目標數、最大檔案大小,等等。當使用者選擇檔案時,檔案的資料會自動根據這些規格驗證。請見 Phoenix.LiveView.allow_upload/3

  • 回應式條目 - 上傳內容會在 Socket 的 @uploads assign 中,填入條目。條目會自動回應進度、錯誤、取消,等等。

  • 拖放 - 使用 phx-drop-target 屬性來啟用。請見 Phoenix.Component.live_file_input/1

允許上傳

您會在掛載中使用 allow_upload/3 來啟用上傳功能。

@impl Phoenix.LiveView
def mount(_params, _session, socket) do
  {:ok,
   socket
   |> assign(:uploaded_files, [])
   |> allow_upload(:avatar, accept: ~w(.jpg .jpeg), max_entries: 2)}
end

目前先這樣!稍後我們會回到 LiveView 來實作一些與表單和上傳相關的回呼,但大多數與上傳相關的功能都是在範本中進行。

呈現回應式元素

使用 Phoenix.Component.live_file_input/1 元件來呈現檔案輸入,供上傳用

<%!-- lib/my_app_web/live/upload_live.html.heex --%>

<form id="upload-form" phx-submit="save" phx-change="validate">
  <.live_file_input upload={@uploads.avatar} />
  <button type="submit">Upload</button>
</form>

重要:您必須在表單中繫結 phx-submitphx-change

請注意,儘管 live_file_input/1 能讓您設定檔案輸入的其他屬性,但諸如 idacceptmultiple 的屬性會自動設定,依據 allow_upload/3 規格而定。

隨著使用者與檔案輸入互動,範本會接收回應式的更新。

上傳條目

上傳項目會填入套接字中指定於 @uploads 中。每個容許的上傳項目均包含一個輸入項目的清單,不論 :max_entries 值在 allow_upload/3 規格中為何。這些輸入項目的結構包含關於上傳的所有資訊,包括進度、客戶端檔案資訊、錯誤等。

我們來看一個帶註解的範例

<%!-- lib/my_app_web/live/upload_live.html.heex --%>

<%!-- use phx-drop-target with the upload ref to enable file drag and drop --%>
<section phx-drop-target={@uploads.avatar.ref}>

<%!-- render each avatar entry --%>
<%= for entry <- @uploads.avatar.entries do %>
  <article class="upload-entry">

    <figure>
      <.live_img_preview entry={entry} />
      <figcaption><%= entry.client_name %></figcaption>
    </figure>

    <%!-- entry.progress will update automatically for in-flight entries --%>
    <progress value={entry.progress} max="100"> <%= entry.progress %>% </progress>

    <%!-- a regular click event whose handler will invoke Phoenix.LiveView.cancel_upload/3 --%>
    <button type="button" phx-click="cancel-upload" phx-value-ref={entry.ref} aria-label="cancel">&times;</button>

    <%!-- Phoenix.Component.upload_errors/2 returns a list of error atoms --%>
    <%= for err <- upload_errors(@uploads.avatar, entry) do %>
      <p class="alert alert-danger"><%= error_to_string(err) %></p>
    <% end %>

  </article>
<% end %>

<%!-- Phoenix.Component.upload_errors/1 returns a list of error atoms --%>
<%= for err <- upload_errors(@uploads.avatar) do %>
  <p class="alert alert-danger"><%= error_to_string(err) %></p>
<% end %>

</section>

在範例中的 section 元素會作為 phx-drop-target 來進行 :avatar 上傳。使用者可以用檔案輸入互動或將檔案拖放到該元素上以新增新的輸入項目。

當檔案新增到表單輸入中時會建立上傳輸入項目,而且每個輸入項目都存在於完成上傳之前。

輸入項目的驗證

驗證會根據在 allow_upload/3 中指明的任何條件自動進行,然而,正如先前所述,你必須在表單上繫結 phx-change 以便執行驗證。因此你必須至少執行一個最小的回呼

@impl Phoenix.LiveView
def handle_event("validate", _params, socket) do
  {:noreply, socket}
end

allow_upload/3 規格不符的檔案輸入項目會含有錯誤。使用 Phoenix.Component.upload_errors/2 及你自訂的輔助函式來呈現友善的錯誤訊息

defp error_to_string(:too_large), do: "Too large"
defp error_to_string(:not_accepted), do: "You have selected an unacceptable file type"

對於會影響所有輸入項目的錯誤訊息,請使用 Phoenix.Component.upload_errors/1 及你自訂的輔助函式來呈現友善的錯誤訊息

defp error_to_string(:too_many_files), do: "You have selected too many files"

取消一個輸入項目

上傳輸入項目也可以取消,無論是透過程式或使用者行為。例如,若要處理上方範本中的點擊事件,你可以執行以下動作

@impl Phoenix.LiveView
def handle_event("cancel-upload", %{"ref" => ref}, socket) do
  {:noreply, cancel_upload(socket, :avatar, ref)}
end

使用已上傳的輸入項目

當終端使用者提交含有 live_file_input/1 的表單時,JavaScript 客戶端會先上傳檔案,然後再呼叫表單的 phx-submit 事件的回呼。

phx-submit 事件的回呼中,你會呼叫 Phoenix.LiveView.consume_uploaded_entries/3 函式來處理完成的上傳,將相關的上傳資料與表單資料一同儲存在伺服器上。

@impl Phoenix.LiveView
def handle_event("save", _params, socket) do
  uploaded_files =
    consume_uploaded_entries(socket, :avatar, fn %{path: path}, _entry ->
      dest = Path.join(Application.app_dir(:my_app, "priv/static/uploads"), Path.basename(path))
      # You will need to create `priv/static/uploads` for `File.cp!/2` to work.
      File.cp!(path, dest)
      {:ok, ~p"/uploads/#{Path.basename(dest)}"}
    end)

  {:noreply, update(socket, :uploaded_files, &(&1 ++ uploaded_files))}
end

:雖然客戶端元資料不可信賴,但檔案大小驗證會在執行直接上傳到伺服器時逐一接收區塊時進行。

此範例直接將檔案寫入硬碟,到 priv 資料夾中。如要在上載中存取資料,例如在 <img /> 標籤中,你須將 uploads 目錄新增到 static_paths/0。在原始的 Phoenix 專案中,此設定會出現在 lib/my_app_web.ex 中。

另一個需要注意的是,在開發過程中,對 priv/static/uploads 所做的變更,會被 live_reload 偵測到。這表示一進行上載並成功後,你的應用程式就會在瀏覽器中重新載入。此設定可在 config/dev.exs 中設定 code_reloader: false 暫時停用。

除了上述以外,此方式在產品環境中也有限制。如果你有執行多個應用程式執行個體,上載的檔案只會儲存在其中一個執行個體中。任何路由到其他電腦的要求最終都會失敗。

因為這些原因,最好將上傳儲存在其他地方,例如資料庫(視大小及內容而定)或獨立的儲存服務。有關實作用戶端到雲端上傳的詳細資訊,請參閱 外部上傳指南

附錄 A:UploadLive

此指南中 LiveView 的一個完整範例

# lib/my_app_web/live/upload_live.ex
defmodule MyAppWeb.UploadLive do
  use MyAppWeb, :live_view

  @impl Phoenix.LiveView
  def mount(_params, _session, socket) do
    {:ok,
    socket
    |> assign(:uploaded_files, [])
    |> allow_upload(:avatar, accept: ~w(.jpg .jpeg), max_entries: 2)}
  end

  @impl Phoenix.LiveView
  def handle_event("validate", _params, socket) do
    {:noreply, socket}
  end

  @impl Phoenix.LiveView
  def handle_event("cancel-upload", %{"ref" => ref}, socket) do
    {:noreply, cancel_upload(socket, :avatar, ref)}
  end

  @impl Phoenix.LiveView
  def handle_event("save", _params, socket) do
    uploaded_files =
      consume_uploaded_entries(socket, :avatar, fn %{path: path}, _entry ->
        dest = Path.join([:code.priv_dir(:my_app), "static", "uploads", Path.basename(path)])
        # You will need to create `priv/static/uploads` for `File.cp!/2` to work.
        File.cp!(path, dest)
        {:ok, ~p"/uploads/#{Path.basename(dest)}"}
      end)

    {:noreply, update(socket, :uploaded_files, &(&1 ++ uploaded_files))}
  end

  defp error_to_string(:too_large), do: "Too large"
  defp error_to_string(:too_many_files), do: "You have selected too many files"
  defp error_to_string(:not_accepted), do: "You have selected an unacceptable file type"
end

若要透過你的應用程式存取你的上載,請務必將 uploads 新增到 MyAppWeb.static_paths/0