如何在Rails的Active Storage中使用FilePond

FilePond是一个由Rik Schennink编写的漂亮的JavaScript文件上传处理库。

我在各种Ruby on Rails项目中使用过这个库,当我想要流畅的上传体验配合出色的UI视觉效果时。

在本文中,我将介绍如何将FilePond与Rails集成,并在过程中描述和解释我的一些技术选择。本教程也假设你对Active Storage有基本的了解。如果你不熟悉Rails的这部分,请查看官方Rails指南:https://guides.rubyonrails.org/active_storage_overview.html

本教程的演示代码位于https://github.com/Code-With-Rails/filepond-demo。我建议你克隆该仓库并从那里运行演示。

我们要构建什么

要将FilePond与我们的应用集成,有两部分:

  • 首先,我们需要添加FilePond JavaScript库并在文件输入HTML标签上启用它。
  • 其次,我们需要将FilePond与Rails应用集成。具体来说,我们希望Active Storage处理文件上传。

为了完成第一部分,我们将使用原生JavaScript。对于第二部分,我们将利用现有的Active Storage JavaScript库并启用直接上传。为了适应FilePond所需的一些特定服务器端点,我们将创建一个自定义控制器。

FilePond安装

对于本教程,我们将从Rails 7.0.x应用程序开始。这意味着我们将使用importmap-rails来添加JavaScript依赖。

bin/bundle add importmap-rails
bin/rails importmap:install

接下来,我们需要使用以下命令添加FilePond依赖:

bin/rails importmap pin filepond

我们想要预加载FilePond,所以在config/importmap.rb文件中,像这样修改文件:

# 之前
pin 'filepond', to: 'https://ga.jspm.io/npm:filepond@4.30.4/dist/filepond.js'

# 之后
pin 'filepond', to: 'https://ga.jspm.io/npm:filepond@4.30.4/dist/filepond.js', preload: true

FilePond基础

要初始化FilePond,我们首先需要有一个HTML文件输入元素。我们的表单看起来像这样:

<%= form_with model: @user, url: update_avatar_path, method: :post do |f| %>
  <%= f.label :avatar, '更新你的头像' %>
  <!-- 我们将把这个输入转换成pond -->
  <%= f.file_field :avatar, class: 'filepond', direct_upload: true %>
  <%= f.button '更新' %>
<% end %>

相应的JavaScript代码看起来像这样:

// application.js
const input = document.querySelector('.filepond')
FilePond.create(input)

这是让FilePond将简单的文件输入标签转换为小部件所需的最低要求。

针对我们在这里构建的内容,我们将实现以下功能:

  • 配置FilePond使用Active Storage的JavaScript库执行直接上传到云提供商
  • 配置FilePond和我们的应用程序允许仅使用远程URL上传
  • 配置我们的应用程序在用户点击FilePond小部件中的取消(撤销)时清除未附加的文件(blob)

简单的Rails应用程序

我们首先设置数据模型,以便Active Storage可以关联文件附件。

bin/rails g model User name:string
bin/rails db:migrate

user.rb文件中,让我们将模型更新为:

class User < ApplicationRecord
  has_one_attached :avatar do |attachable|
    attachable.variant :thumb, resize_to_limit: [100, 100]
  end
end

集成FilePond

让我们配置FilePond使用Active Storage的直接上传功能:

// app/javascript/application.js
FilePond.setOptions({
  server: {
    process: (fieldName, file, metadata, load, error, progress, abort, transfer, options) => {
      const uploader = new DirectUpload(file, directUploadUrl, {
        directUploadWillStoreFileWithXHR: (request) => {
          request.upload.addEventListener(
            'progress',
            event => progress(event.lengthComputable, event.loaded, event.total)
          )
        }
      })

      uploader.create((errorResponse, blob) => {
        if (errorResponse) {
          error(`出错了:${errorResponse}`)
        } else {
          const hiddenField = document.createElement('input')
          hiddenField.setAttribute('type', 'hidden')
          hiddenField.setAttribute('value', blob.signed_id)
          hiddenField.name = input.name
          document.querySelector('form').appendChild(hiddenField)
          load(blob.signed_id)
        }
      })

      return { abort: () => abort() }
    },
    fetch: {
      url: './filepond/fetch',
      method: 'POST'
    },
    revert: {
      url: './filepond/remove'
    },
    headers: {
      'X-CSRF-Token': document.head.querySelector("[name='csrf-token']").content
    }
  }
})

对于fetch和revert端点,我们需要一个自定义控制器:

# app/controllers/filepond_controller.rb
require 'open-uri'

class FilepondController < ApplicationController
  def fetch
    uri = URI.parse(raw_post)
    url = uri.to_s

    blob = ActiveStorage::Blob.create_and_upload!(
      io: URI.open(uri),
      filename: URI.parse(url).path.parameterize
    )

    if blob.persisted?
      redirect_to rails_service_blob_path(blob.signed_id, blob.filename)
    else
      head :unprocessable_entity
    end
  end

  def remove
    signed_id = raw_post
    blob = ActiveStorage::Blob.find_signed(signed_id)

    if blob
      blob.purge
      head :ok
    else
      head :not_found
    end
  end

  private

  def raw_post
    request.raw_post
  end
end

以及路由:

# config/routes.rb
Rails.application.routes.draw do
  post 'filepond/fetch', to: 'filepond#fetch'
  delete 'filepond/remove', to: 'filepond#remove'
end

结论

FilePond为用户上传提供了出色的用户界面反馈。将它与Rails的Active Storage集成并不困难,但需要一些自定义。

本教程展示了一种集成FilePond的方法,并尽可能应用原生Rails。在下一部分中,我将使用上述实现方法,将所有这些转换为一个gem,这样我们可以重用而无需每次都重新实现。