查看源代码 上传

LiveView 支持交互式文件上传,并提供上传进度,包括直接上传到服务器和直接上传到云端的 外部上传

内置功能

  • 接受规范 - 定义可接受的文件类型、最大条目数、最大文件大小等。当客户端选择文件时,文件元数据会自动根据规范进行验证。参见 Phoenix.LiveView.allow_upload/3

  • 响应式条目 - 上传会在 socket 中的 @uploads 赋值中进行填充。条目会自动响应进度、错误、取消等。

  • 拖放 - 使用 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 规范自动设置。

当最终用户与文件输入框交互时,模板将进行响应式更新。

上传条目

上传会在 socket 中的 @uploads 赋值中进行填充。每个允许的上传都包含一个条目列表,而不管 allow_upload/3 规范中的 :max_entries 值如何。这些条目结构包含有关上传的所有信息,包括进度、客户端文件信息、错误等。

让我们看一个带注释的示例。

<%!-- 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 元素充当 :avatar 上传的 phx-drop-target。用户可以与文件输入框交互,也可以将文件拖放到元素上来添加新条目。

当文件添加到表单输入框中时,会创建上传条目,每个条目都将在成功完成上传后被消耗掉之前一直存在。

条目验证

验证会根据 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