如何在客户端而不是表单提交时触发文件上传?

How to trigger the file upload on the client side and not on form submission?

我在此处找到使用 s3 的活动存储示例的工作版本:

https://edgeguides.rubyonrails.org/active_storage_overview.html

现在我希望能够执行文件上传,而不是在我完成填写表单时而是在用户选择要上传的文件后立即执行。 实际上,就我而言,我有一个所见即所得的编辑器,它有一个 on drop 事件触发

var myCodeMirror = CodeMirror.fromTextArea(post_body, {
   lineNumbers: true,
   dragDrop: true
  });

  myCodeMirror.on('drop', function(data, e) {
    var file;
    var files;
    // Check if files were dropped
    files = e.dataTransfer.files;
    if (files.length > 0) {
      e.preventDefault();
      e.stopPropagation();
      file = files[0];
      console.log('File: ' + file.name);
      console.log('File: ' + file.type);
      return false;
    }
  });

那么是不是因为文件丢弃触发了这个事件,所以我要以某种方式将其发送到活动存储,以便它立即开始将文件上传到 S3?

从客户端触发上传

Active Storage 公开了 DirectUpload JavaScript class,您可以使用它直接从客户端触发文件上传。

您可以利用它与第三方插件(例如 Uppy、Dropzone)或您自己的自定义 JS 代码集成。

使用DirectUpload

您需要做的第一件事是确保 AWS S3 设置为处理直接上传。这需要确保您的 CORS 配置设置正确。

接下来,您只需实例化 DirectUpload class 的一个实例,将要上传的文件和上传 URL 传递给它。

import { DirectUpload } from "activestorage"

// your form needs the file_field direct_upload: true, which
// provides data-direct-upload-url
const input = document.querySelector('input[type=file]')
const url = input.dataset.directUploadUrl
const upload = new DirectUpload(file, url)

upload.create((error, blob) => { 
   // handle errors OR persist to the model using 'blob.signed_id'
})

在此处查看完整文档: https://edgeguides.rubyonrails.org/active_storage_overview.html#integrating-with-libraries-or-frameworks

DirectUpload#create 方法启动到 S3 的上传和 returns 出现错误或上传的文件 blob。

假设没有错误,最后一步是将上传的文件持久化到模型中。您可以使用 blob.signed_id 并将其放入页面某处的隐藏字段中,或者使用 AJAX 请求来更新您的模型。

正在上传文件

在上面的例子中,要在 drop 上开始直接上传,只需将上面的代码放入 drop 处理程序中即可。

像这样:

myCodeMirror.on('drop', function(data, e) {
   // Get the file
   var file = e.dataTransfer.files[0];

   // You need a file input somewhere on the page...
   const input = document.querySelector('input[type=file]')
   const url = input.dataset.directUploadUrl

   // Instantiate the DirectUploader object
   const upload = new DirectUpload(file, url)

   // Upload the file
   upload.create((error, blob) => { ... })
});

使用资产管道

如果您只是使用资产管道而不使用 JavaScript 捆绑工具,那么您可以像这样 DirectUpload class 创建实例

const upload = new ActiveStorage.DirectUpload(file, url)

该主题的主要问题是 - 您无法在表单的 java 脚本部分导入数据上传。但是我们可以创建对象 ImmediateUploader 如下:

全局Java脚本部分

upload/uploader.js

import { DirectUpload } from "@rails/activestorage"

export default class Uploader {
  constructor(file, url) {
    this.file = file
    this.url = url
    this.directUpload = new DirectUpload(this.file, this.url, this)
  }
 
  upload() {
    return new Promise((resolve, reject) => {
      this.directUpload.create((error, blob) => {
        if (error) {
          // Handle the error
          reject(error)
        } else {
          // Add an appropriately-named hidden input to the form
          // with a value of blob.signed_id
          resolve(blob)
        }
      })
    })
  }
}

upload/index.js

import Uploader from './uploader.js'

export default {
    upload (file, url) {
        const uploader = new Uploader(file, url)
        return uploader.upload()
    }
}

application.js

window.ImmediateUploader = require('./upload');

表格部分
现在我们可以使用 ImmediateUploader 将选定的文件直接上传到活动存储并在加载后更新图像而无需提交:


<%= simple_form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %>
  <%= f.error_notification %>

  <div class="form-inputs">
    <div class="row">
      <img id="avatar" class="centered-and-cropped" width="100" height="100" style="border-radius:50%" src="<%= url_for(user.photo) %>"> 
      <button type="button" class="btn" onclick="event.preventDefault(); document.getElementById('user_photo').click()">Change avatar</button>
    </div>
    <%= f.file_field :photo, direct_upload: true, class: "hiddenfile" %>  
  </div>
  <div class="form-actions">
    <%= f.button :submit, t(".update"), class: 'btn btn-primary'  %>
  </div>
<% end %>

<% content_for :js do %>
<script>
const input = document.querySelector('input[type=file]')

input.addEventListener('change', (event) => {
  Array.from(input.files).forEach(file => uploadFile(file))
  // clear uploaded files from the input
  input.value = null
})

const uploadFile = (file) => {
  // your form needs the file_field direct_upload: true, which
  //  provides data-direct-upload-url
  const url = input.dataset.directUploadUrl;
  ImmediateUploader.default.upload (file, url)
    .then(blob => {
      // get blob.signed_id and add it to form values to submit form
      const hiddenField = document.createElement('input')
      hiddenField.setAttribute("type", "hidden");
      hiddenField.setAttribute("value", blob.signed_id);
      hiddenField.name = input.name
      document.querySelector('form').appendChild(hiddenField)
      // Update new avatar Immediately
      document.getElementById('avatar').src = '/rails/active_storage/blobs/' + blob.signed_id + '/' + blob.filename;     
      // Update photo in Database
      axios.post('/users/photo', { 'photo': blob.signed_id }).then(response => {});
    });
}</script>
<% end %>

控制器:

class RegistrationController < Devise::RegistrationsController
  def update
    super
    @user = current_user
    @user.avatar = url_for(@user.photo.variant(resize_to_limit: [300, 300]).processed) if @user.photo.attached?
    @user.save
  end
  def updatephoto
    @photo = params[:photo]
    @user = current_user
    @user.photo = @photo
    @user.save

    @user = current_user
    @user.avatar = url_for(@user.photo.variant(resize_to_limit: [300, 300]).processed) if @user.photo.attached?
    @user.save
  end
end