如何使用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
相关产品推荐
相关产品推荐

