You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用Rails Active Storage与Vue.js直接上传文件至S3?

嘿,我之前刚好在Vue项目里对接过Rails Active Storage的S3直传,完全可以脱离Rails的内置form实现,核心就是先从Rails拿到直传的签名信息,然后让Vue直接和S3交互上传,不用经过Rails中转。给你一步步拆解实现流程:

第一步:在Rails后端搭建直传签名接口

Active Storage的直传逻辑是先由后端生成一个带签名的S3上传凭证,所以我们需要写一个API接口来提供这个凭证:

首先创建一个控制器处理签名请求:

# app/controllers/direct_uploads_controller.rb
class DirectUploadsController < ApplicationController
  skip_before_action :verify_authenticity_token, only: [:create] # 非API模式下可保留,需确保前端传CSRF token;API模式可移除
  before_action :authenticate_user! # 可选,需用户认证后上传则添加

  def create
    # 生成预签名的Blob对象,包含S3上传地址和头部信息
    blob = ActiveStorage::Blob.create_before_direct_upload!(**direct_upload_params)
    
    # 返回前端需要的关键信息
    render json: {
      signed_id: blob.signed_id, # 后续关联模型要用的ID
      upload_url: blob.service_url_for_direct_upload, # S3的直传地址
      upload_headers: blob.service_headers_for_direct_upload # S3上传需要的请求头
    }
  end

  private

  def direct_upload_params
    params.require(:blob).permit(:filename, :byte_size, :checksum, :content_type)
  end
end

然后在路由里添加这个接口:

# config/routes.rb
Rails.application.routes.draw do
  # 其他路由...
  post '/direct_uploads', to: 'direct_uploads#create'
end
第二步:Vue前端实现完整上传流程

前端需要完成四个核心步骤:选择文件、请求签名、上传到S3、把签名ID传给Rails关联模型。这里给你一个完整的Vue组件示例:

<template>
  <div class="upload-container">
    <input 
      type="file" 
      @change="handleFileSelect" 
      accept="image/*" <!-- 可选,限制文件类型 -->
    />
    <button 
      @click="startUpload" 
      :disabled="!selectedFile || isUploading"
    >
      {{ isUploading ? '上传中...' : '开始上传' }}
    </button>
    <div v-if="progress > 0" class="progress-bar">
      <div :style="{ width: `${progress}%` }"></div>
      <span>{{ progress }}%</span>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      selectedFile: null,
      isUploading: false,
      progress: 0
    }
  },
  methods: {
    // 处理文件选择
    handleFileSelect(event) {
      this.selectedFile = event.target.files[0];
      this.progress = 0;
    },

    // 生成Active Storage要求的SHA-256校验和(base64编码)
    async generateFileChecksum(file) {
      const arrayBuffer = await file.arrayBuffer();
      const hashBuffer = await crypto.subtle.digest('SHA-256', arrayBuffer);
      const hashArray = Array.from(new Uint8Array(hashBuffer));
      return btoa(String.fromCharCode(...hashArray));
    },

    // 完整上传流程
    async startUpload() {
      if (!this.selectedFile) return;
      this.isUploading = true;

      try {
        // 1. 生成文件校验和,请求Rails后端的签名
        const checksum = await this.generateFileChecksum(this.selectedFile);
        const signatureRes = await fetch('/direct_uploads', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content // 非API模式必须带CSRF token
          },
          body: JSON.stringify({
            blob: {
              filename: this.selectedFile.name,
              byte_size: this.selectedFile.size,
              checksum: checksum,
              content_type: this.selectedFile.type
            }
          })
        });

        if (!signatureRes.ok) throw new Error('获取签名失败');
        const { upload_url, upload_headers, signed_id } = await signatureRes.json();

        // 2. 上传文件到S3,带进度监听
        await this.uploadToS3(upload_url, upload_headers, this.selectedFile);

        // 3. 把signed_id传给Rails,关联到你的模型(示例:创建一个带附件的Post)
        await this.associateWithModel(signed_id);

        alert('上传成功!');
        this.selectedFile = null;
      } catch (err) {
        console.error('上传出错:', err);
        alert('上传失败,请重试');
      } finally {
        this.isUploading = false;
        this.progress = 0;
      }
    },

    // 上传到S3并监听进度
    async uploadToS3(url, headers, file) {
      return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.open('PUT', url);
        
        // 设置S3要求的请求头
        Object.keys(headers).forEach(key => {
          xhr.setRequestHeader(key, headers[key]);
        });

        // 监听上传进度
        xhr.upload.addEventListener('progress', (e) => {
          if (e.lengthComputable) {
            this.progress = Math.round((e.loaded / e.total) * 100);
          }
        });

        xhr.addEventListener('load', () => {
          if (xhr.status >= 200 && xhr.status < 300) {
            resolve();
          } else {
            reject(new Error(`S3上传失败:${xhr.statusText}`));
          }
        });

        xhr.addEventListener('error', () => reject(new Error('网络错误')));
        xhr.send(file);
      });
    },

    // 关联到你的Rails模型(示例:创建一个带附件的Post)
    async associateWithModel(signedId) {
      const res = await fetch('/posts', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
        },
        body: JSON.stringify({
          post: {
            title: '我的新帖子',
            attachment: signedId // 这里把signed_id传给Active Storage的字段
          }
        })
      });

      if (!res.ok) throw new Error('关联模型失败');
    }
  }
}
</script>

<style scoped>
.upload-container {
  margin: 20px;
}

.progress-bar {
  margin-top: 10px;
  height: 20px;
  border: 1px solid #ddd;
  border-radius: 4px;
  position: relative;
}

.progress-bar div {
  height: 100%;
  background-color: #4CAF50;
  border-radius: 3px;
  transition: width 0.3s;
}

.progress-bar span {
  position: absolute;
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  color: #333;
}
</style>
一些关键注意事项
  • CSRF Token:如果你的Rails不是纯API模式,一定要在请求头里带上X-CSRF-Token,可以从页面的<meta name="csrf-token">标签获取。
  • 权限控制:如果需要限制上传权限,记得在DirectUploadsController里添加身份验证(比如devise的authenticate_user!)。
  • 文件校验:前端可以提前做文件大小、类型的校验,避免无效请求。
  • 错误处理:示例里的错误处理比较简单,你可以根据需求优化成更友好的提示。

内容的提问来源于stack exchange,提问作者Tachyons

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.25 07:39:35