首页
/ Rails Active Storage 深入解析:Blob/Attachment 模型、文件分发策略与 Direct Uploads 实战

Rails Active Storage 深入解析:Blob/Attachment 模型、文件分发策略与 Direct Uploads 实战

2026-09-05 18:13:49作者:田桥桑Industrious

Active Storage 是 Ruby on Rails 官方提供的文件附件框架:它让你把文件上传到 S3、Google Cloud Storage 等云端服务(也支持本地磁盘服务用于测试和本地部署),并以 Active Record 的方式把文件挂到业务模型上。本篇以仓库中 activestorage/README.md 为骨架,结合 Blob 模型Attachment 模型路由定义引擎配置 的源码,系统讲解其架构设计、安装接入、常用 API、重定向/代理两种文件分发策略,以及客户端直传(Direct Uploads)的完整流程与 JavaScript 事件。

一、与 Rails 内其他附件方案的关键差异

README 指出,Active Storage 与 Rails 生态中其他附件方案的核心区别在于:它通过内置的 ActiveStorage::BlobActiveStorage::Attachment 两个模型(由 Active Record 支撑)实现附件管理,现有应用模型无需添加任何列即可与文件建立关联。附件通过 Attachment 联结模型以多态关联的方式连接到真正的 Blob

源码印证了这一设计:

  • attachment.rb 中定义了两个关键关联:
    • belongs_to :record, polymorphic: true —— 指向任意业务模型(record_type + record_id 多态外键);
    • belongs_to :blob, class_name: "ActiveStorage::Blob", autosave: true —— 指向文件本体记录。
    • 并通过 delegate_missing_to :blob 把所有未定义方法委托给 blob,因此在业务代码中 user.avatar.attach(...) 这类写法实际调用的是 Blob 的能力。
  • blob.rb 的注释明确说明:Blob 存储的是附件元数据(文件名、内容类型等)及其在存储服务中的标识 key,不存储二进制数据本身。Blob 在语义上是不可变的:一个文件对应一个 blob;同一个 blob 可以关联到多个业务模型;如果需要变换(transform)某个 blob,做法是创建新 blob 而不是修改旧 blob(当然之后可以删掉旧版本)。

表结构上,安装迁移 20170806125915_create_active_storage_tables.rb 创建三张表,恰好对应这套模型:

关键字段 说明
active_storage_blobs key(唯一索引)、filenamecontent_typebyte_sizechecksumservice_name 文件元数据;key 是服务端的存储标识
active_storage_attachments name、多态 recordblob,唯一索引 [record_type, record_id, name, blob_id] 业务模型与 blob 的联结;外键指向 blobs 表,保证仍被引用时 blob 无法被删除
active_storage_variant_records blob_id + variation_digest(联合唯一索引) 记录已生成的变体,用于按需惰性生成时避免重复处理

从源码结构看,checksum 是上传完整性校验的关键:上传前 Active Storage 会计算校验和并发送给存储服务验证(见 Blob 的 upload 方法注释),下载时 open 也会校验,不匹配即抛 ActiveStorage::IntegrityError

二、安装

运行以下命令,把 Active Storage 的迁移文件复制到应用中:

bin/rails active_storage:install

README 特别提醒:如果找不到该任务,请确认 config/application.rb 中存在 require "active_storage/engine"

安装后,服务端的配置入口是存储配置文件。从 engine.rbactive_storage.services 初始化器可以看到,Rails 会优先读取 config/storage/#{Rails.env}.yml,否则回退到 config/storage.yml,若两者都不存在则直接抛出 "Couldn't find Active Storage configuration" 异常。配置文件解析出的多套服务(如 localamazonamazon_mirror)会被注册进 ActiveStorage::Service::Registry,再由 config.active_storage.service 指定默认服务——这就是"一个主服务 + 其他服务做镜像"冗余能力的来源(Blob 在上传后可通过 mirror_later 异步同步到镜像服务)。

引擎默认配置同样值得了解(均来自 engine.rb):

  • routes_prefix 默认为 /rails/active_storage
  • resolve_model_to_route 默认为 :rails_storage_redirect(即默认重定向分发,见第四节);
  • service_urls_expire_in 默认 5 分钟——README 中"重定向 HTTP 过期时间为 5 分钟"即来源于此;
  • variant_processor 默认为 :mini_magick,可切换为 :vips:disabled,也接受自定义 Transformer 类。

三、核心用法示例

3.1 单个附件(has_one_attached)

以下示例完整继承自 README,并补充了源码层面的解释:

class User < ApplicationRecord
  # 建立附件与 blob 的关联。当 user 被销毁时,默认会被 purge
  #(模型记录被删除,资源文件也被删除)。
  has_one_attached :avatar
end

# 给 user 挂一个头像
user.avatar.attach(io: File.open("/path/to/face.jpg"), filename: "face.jpg", content_type: "image/jpeg")

# user 有头像吗?
user.avatar.attached? # => true

# 同步销毁头像及实际资源文件
user.avatar.purge

# 通过 Active Job 异步销毁关联模型与实际资源文件
user.avatar.purge_later

user.avatar.attached? # => false

# 生成一个指向应用的持久 URL。访问时会被重定向到真正的服务端点。
# 这层间接让公开 URL 与实际 URL 解耦,例如允许把附件镜像到不同服务以获得高可用。
# 重定向的 HTTP 过期时间为 5 分钟。
url_for(user.avatar)

class AvatarsController < ApplicationController
  def update
    # params[:avatar] 是一个 ActionDispatch::Http::UploadedFile 对象
    Current.user.avatar.attach(params.require(:avatar))
    redirect_to Current.user
  end
end

几点源码级说明:

  • purge / purge_later 的区别见 attachment.rbpurge 在事务中删除 attachment 并 touch 记录,然后调用 blob.purge(销毁记录并调用服务端的 delete);purge_later 则入队 ActiveStorage::PurgeJob。由于删除文件会发起一次到云服务的 HTTP 调用,Blob 的注释明确建议不要在事务或回调中直接 purge,而应使用 purge_later
  • dependent 行为同样来自 attachment:after_destroy_commit :purge_dependent_blob 会读取 has_one_attached/has_many_attached 上声明的 dependent: 选项(:purge:purge_later),因此"模型销毁时默认 purge 附件"是框架级默认行为。
  • 上传流程中,attachment 的 :upload 回调链会依次触发 mirror_blob_later(同步到镜像服务)、analyze_blob_later(入队分析,提取尺寸等元数据)、create_variants(处理命名变体)。analyze 的时机可全局配置 config.active_storage.analyze(默认 :later,即上传后异步分析;:lazily 表示惰性分析)。

3.2 多个附件(has_many_attached)

class Message < ApplicationRecord
  has_many_attached :images
end
<%= form_with model: @message, local: true do |form| %>
  <%= form.text_field :title, placeholder: "Title" %><br>
  <%= form.textarea :content %><br><br>

  <%= form.file_field :images, multiple: true %><br>
  <%= form.submit %>
<% end %>
class MessagesController < ApplicationController
  def index
    # 使用内置的 with_attached_images scope 避免 N+1
    @messages = Message.all.with_attached_images
  end

  def create
    message = Message.create! params.expect(message: [ :title, :content, images: [] ])
    redirect_to message
  end

  def show
    @message = Message.find(params[:id])
  end
end

关于 N+1:with_attached_images 这类 scope 由 Attached::Model 按反射动态生成,一次性预载 attachments 与 blobs;attachment.rb 的头部注释还给出更彻底的预载方式 with_all_variant_records(当开启 ActiveStorage.track_variants 时连变体记录一起预载)。

3.3 图片变体(Variant)

<%# 访问变体 URL 时会惰性变换原始 blob,然后重定向到新的服务位置 %>
<%= image_tag user.avatar.variant(resize_to_limit: [100, 100]) %>

变体机制的要点(结合 variant.rb 与 Blob 注释):

  • 变换参数是 image_processing 支持的任意参数(resize_to_limitresize_to_fill、质量、宽高比等),底层处理器由 config.active_storage.variant_processor 决定,默认 :mini_magick(MiniMagick/ImageMagick),可切换为 :vips(libvips + ruby-vips);engine.rb 中还会在加载失败时给出针对 libvips、ruby-vips、image_processing、mini_magick 的详细安装提示日志。
  • 变体是惰性的:image_tag 渲染出的是 representations 路由的签名 URL,首次访问时才实际执行变换并上传新对象,随后重定向;active_storage_variant_records 表用 variation_digest 记录已生成的变体,避免重复计算。
  • attachment 还支持 preview(面向视频/PDF 等不可变内容类型的预览图,引擎默认注册了 PopplerPDFPreviewer、MuPDFPreviewer、VideoPreviewer)与 representation(统一入口),并支持在模型上声明命名的预定义变体,如 avatar.variant(:thumb)

四、文件分发策略:重定向与代理

Active Storage 支持两种分发方式:重定向(redirecting)与代理(proxying)。

4.1 重定向(默认)

Active Storage 为文件生成稳定的应用 URL,访问时重定向到签名的、短生命周期的服务端点 URL。这样应用服务器无需承担文件数据分发的压力。这是默认策略。

路由层面,config/routes.rbActiveStorage.routes_prefix(默认 /rails/active_storage)下定义了:

get "/blobs/redirect/:signed_id/*filename" => "active_storage/blobs/redirect#show", as: :rails_service_blob
get "/blobs/proxy/:signed_id/*filename"    => "active_storage/blobs/proxy#show", as: :rails_service_blob_proxy

变体(representations)也有对应的 redirect/proxy 两组路由。签名 ID(signed_id)由 ActiveStorage.verifierapp.message_verifier("ActiveStorage"))生成,携带过期时间(默认 urls_expire_in),因此即使 URL 泄漏,服务端点链接也会在到期后失效。

当应用默认配置为代理时,可用 rails_storage_redirect_path / rails_storage_redirect_url 路由帮助方法改为重定向:

<%= image_tag rails_storage_redirect_path(@user.avatar) %>

这两个帮助方法在 routes.rb 中以 direct 路由定义实现:direct :rails_storage_redirect 对 blob 生成 :rails_service_blob 路由,对 variant/preview 等无 signed_id 的对象则生成 :rails_blob_representation 路由,二者都支持 expires_in / expires_at 选项(缺省取 ActiveStorage.urls_expire_in)。

4.2 代理(Proxying)

可选地,文件可以由应用服务器代理:即应用服务器在收到请求后从存储服务下载文件数据并转交给客户端。这适用于把文件经 CDN 分发的场景。

全局默认改为代理:

# config/initializers/active_storage.rb
Rails.application.config.active_storage.resolve_model_to_route = :rails_storage_proxy

对应 engine.rb 中的 ActiveStorage.resolve_model_to_route = app.config.active_storage.resolve_model_to_route || :rails_storage_redirect——这正是为什么默认行为是重定向。

对特定附件显式使用代理,则用 rails_storage_proxy_path / rails_storage_proxy_url

<%= image_tag rails_storage_proxy_path(@user.avatar) %>

实现上,resolve("ActiveStorage::Blob")resolve("ActiveStorage::Attachment")resolve 路由块都会调用 route_for(ActiveStorage.resolve_model_to_route, ...),因此模型 URL 的最终走向完全由 resolve_model_to_route 这一个配置决定,切换成本极低。

五、Direct Uploads:客户端直传

5.1 直传安装步骤

README 给出的完整步骤如下(共四步):

1. 在应用 JS 包中包含 Active Storage JavaScript(或直接引用)。 有四种引入方式:

<%= javascript_include_tag "activestorage" %>

(在应用 HTML 中直接引入,不经 asset pipeline 打包,自动启动 autostart)

# config/importmap.rb
pin "@rails/activestorage", to: "activestorage.esm.js"
<script type="module-shim">
  import * as ActiveStorage from "@rails/activestorage"
  ActiveStorage.start()
</script>

(importmap-rails 方式,ESM 模块,需手动调用 start() 启动)

//= require activestorage

(asset pipeline 方式)

import * as ActiveStorage from "@rails/activestorage"
ActiveStorage.start()

(npm 包方式,需手动启动)

JavaScript 源码位于 activestorage/app/javascript 目录(以 package.json 声明 @rails/activestorage 包名,经 rollup 构建),activestorage / activestorage.esm 两个资产会被引擎自动加入 asset pipeline 的 precompile 列表(engine.rb 中 active_storage.asset 初始化器,受 config.active_storage.precompile_assets 控制,默认开启)。

2. 用直传 URL 标注文件输入框:

<%= form.file_field :attachments, multiple: true, direct_upload: true %>

3. 为第三方存储服务配置 CORS,允许直传请求。

4. 完成! 上传在表单提交时开始。

5.2 直传的工作原理(源码视角)

从 Blob 模型的方法命名可以看出直传的两阶段协议:

  1. 客户端 JS 拦截表单提交后,先向应用发起 POST /rails/active_storage/direct_uploads(路由名 :rails_direct_uploads,处理者为 direct_uploads_controller.rb),为每个文件携带 filenamebyte_sizechecksumcontent_type 等元数据;
  2. 服务端调用 ActiveStorage::Blob.create_before_direct_upload!(见 blob.rb)创建一条尚无文件对应的 blob 记录——其 key 已由 generate_unique_secure_token 生成(28 位 base36 小写字母,避免大小写敏感问题),并返回 blob 的 signed_id 以及 service_url_for_direct_upload / service_headers_for_direct_upload(含签名上传 URL 与请求头);
  3. 客户端拿到签名 URL 后直接 PUT 文件到云存储(这就是必须配置 CORS 的原因),并通过事件上报进度;
  4. 表单提交完成时,隐藏字段携带 signed_id 到达服务端,Active Record 侧将其与记录关联成 attachment。

这一流程的好处是应用服务器不作为上传的中转站,README 同时指出另一种服务端上传方式 create_and_upload!(需要先有一个可 rewinds 的 io)适合任意后端系统处理文件的场景。

5.3 Direct Upload JavaScript 事件表

README 完整列出的事件(CustomEventevent.detail 为事件数据):

事件名 事件目标 事件数据(event.detail 说明
direct-uploads:start <form> 表单中包含直传字段,且表单被提交。
direct-upload:initialize <input> {id, file} 表单提交后,为每个文件派发一次。
direct-upload:start <input> {id, file} 一次直传即将开始。
direct-upload:before-blob-request <input> {id, file, xhr} 向应用发起直传元数据请求之前。
direct-upload:before-storage-request <input> {id, file, xhr} 向存储服务发起存储请求之前。
direct-upload:progress <input> {id, file, progress} 文件存储请求进展中。
direct-upload:error <input> {id, file, error} 发生错误;除非取消该事件,否则会弹出 alert
direct-upload:end <input> {id, file} 一次直传结束。
direct-uploads:end <form> 所有直传均已结束。

典型用法是监听 direct-upload:progress 更新进度条,监听 direct-upload:error 抑制默认 alert 并展示自定义错误提示。

六、许可与支持

Active Storage 以 MIT 许可证发布(见 MIT-LICENSE)。相关 API 文档可查阅 Rails 官方 API 文档;关于 Active Storage 的完整概念讲解可参考仓库内官方指南 active_storage_overview.md,其测试覆盖(模型、控制器、服务、数据库等)位于 activestorage/test 目录,可作为行为验证的第一手材料。

小结

  • 架构Attachment(多态联结)+ Blob(元数据与服务端 key)的两层模型让业务模型零侵入地获得文件能力;三张表(blobs / attachments / variant_records)支撑元数据、关联与变体去重。
  • 用法has_one_attached / has_many_attached + attach / attached? / purge / purge_later / with_attached_* / variant 构成日常 API;安装只需 bin/rails active_storage:install 并确保 engine 已 require。
  • 分发:默认重定向(签名短 URL + 5 分钟过期),可全局或按附件切换为代理,切换点就是 resolve_model_to_route 一个配置。
  • 直传:JS 库 + direct_upload: true + CORS 三要素;两阶段协议(先建 blob 拿签名 URL,再直传云存储),全程有 9 个 CustomEvent 可供集成。
登录后查看全文
热门项目推荐
相关项目推荐