首页
/ Ruby on Rails 升级完全指南:从升级策略到 3.x→8.x 各版本迁移要点

Ruby on Rails 升级完全指南:从升级策略到 3.x→8.x 各版本迁移要点

2026-09-07 10:12:46作者:彭桢灵Jeremy

本篇技术指南源自 Rails 官方升级手册(guides/source/upgrading_ruby_on_rails.md),系统梳理将现有 Rails 应用升级到新版本时所需的完整方法论:先讲测试覆盖、Ruby 版本、逐小版本推进的通用策略,再讲解 bin/rails app:update 更新任务与 config.load_defaults 框架默认值机制,随后按升级目标版本从 Rails 8.2 一路回溯到 Rails 3.1,逐条列出各版本迁移中的破坏性变更、配置项迁移与代码改写示例。阅读并实践本文,你将掌握一次安全、可回滚、可控的 Rails 版本升级全流程,以及查阅各版本发布说明进一步核对细节的能力。

当前仓库中 RAILS_VERSION 显示版本为 8.2.0.alpha,因此本文档站在 Rails 8.2 的高度,覆盖最近一次跨版本升级(8.1 → 8.2)及全部历史迁移路径;个别命令输出、默认值与当前源码(如 railties/lib/rails/generators/rails/app/app_generator.rb)保持一致。


一、升级前的通用建议

在动手升级现有应用之前,先确认你有一个足够充分的升级理由:你需要权衡新功能需求、旧代码获取支持的难度日益增大、以及你可支配的时间与技能等因素。

1.1 测试覆盖率是最佳保障

确保升级后应用仍然正常工作的最佳方式,是在升级开始之前就拥有良好的测试覆盖。如果缺少覆盖应用大部分功能的自动化测试,你就需要花费大量时间手动验证所有改动过的地方。对 Rails 升级而言,这意味着要逐一验证应用中的每项功能。所以务必在开始升级之前就把测试覆盖做到位。

1.2 Ruby 版本要求

Rails 通常紧跟当时最新发布的 Ruby 版本,各主要版本的 Ruby 最低要求如下:

Rails 版本 最低 Ruby 版本
Rails 8.0 / 8.1 Ruby 3.2.0+
Rails 7.2 Ruby 3.1.0+
Rails 7.0 / 7.1 Ruby 2.7.0+
Rails 6 Ruby 2.5.0+
Rails 5 Ruby 2.2.2+

建议分开升级 Ruby 与 Rails:先尽可能升到最新版 Ruby,再升级 Rails。

1.3 升级流程:逐个小版本推进

Rails 版本号遵循 Major.Minor.Patch 形式。Major 与 Minor 版本允许修改公共 API,因此可能引发应用报错;Patch 版本只包含 bug 修复,不会改变任何公共 API。版本切换时最好放慢节奏、一次只升一个小版本,以充分利用弃用(deprecation)警告。

标准流程:

  1. 编写测试并确保全部通过;
  2. 升级到当前版本之后的最新 Patch 版本
  3. 修复测试与已弃用的功能;
  4. 升级到下一个小版本的最新 Patch 版本

重复该过程,直到抵达目标 Rails 版本。

1.4 在版本之间移动

具体操作四步:

  1. 修改 Gemfile 中的 Rails 版本号并执行 bundle update rails
  2. 若使用 jsbundling-rails,同步修改 package.json 中 Rails JavaScript 包的版本,并执行 bin/rails javascript:install
  3. 运行更新任务
  4. 运行你的测试套件。

提示:所有已发布 Rails gem 的版本清单可到 rubygems 的 rails gem 版本页面查询。


二、Update 任务:bin/rails app:update

在更新 Gemfile 中的 Rails 版本后,运行 bin/rails app:update。该命令会在交互式会话中帮助你创建新文件、改动旧文件:

$ bin/rails app:update
       exist  config
    conflict  config/application.rb
Overwrite /myapp/config/application.rb? (enter "h" for help) [Ynaqdh]
       force  config/application.rb
      create  config/initializers/new_framework_defaults_8_0.rb
...

别忘了逐条 review 差异,确认没有意外改动。此过程使用的 diff 与 merge 工具可通过 THOR_DIFFTHOR_MERGE 两个环境变量指定。

从源码实现看,app:update 会以 update 模式复用应用生成器(app_generator.rb):普通 rails new 会删除 new_framework_defaults_* 初始化文件,而 --update 模式会保留/创建目标版本对应的 config/initializers/new_framework_defaults_X_Y.rb;当目标版本不低于当前版本时会移除该文件(remove_new_framework_defaults?),这正是"分步启用新默认值"机制得以成立的原因。


三、配置框架默认值(Framework Defaults)

新版本 Rails 可能拥有与旧版本不同的配置默认值。但在按上述步骤升级后,你的应用仍会沿用上一版 Rails 的配置默认值运行——因为 config/application.rb 里的 config.load_defaults 尚未改变。

为了让你能逐个启用新默认值,Update 任务创建了文件 config/initializers/new_framework_defaults_X_Y.rb(文件名含目标 Rails 版本)。你应通过取消文件内注释的方式逐步启用新配置;这可以跨越多个部署周期渐进完成。一旦应用做好以新默认值运行的准备,就可以删除该文件并把 config.load_defaults 翻转为新版本号。

config.load_defaults 的取值决定了整套框架行为基线:例如 config.load_defaults 7.1 会自动启用 7.1 版缓存序列化格式、7.1 版异常处理语义等(详见后文各版本条目)。


四、从 Rails 8.1 升级到 Rails 8.2

更多变更参见 8.2 release notes

4.1 旧的 Active Record 6.1 marshal 格式已被移除

如果你的应用仍设置 active_record.marshalling_format_version = 6.1(可能源于未调用 config.load_defaults,或使用了 ≤ 6.1 的版本),则必须在升级前显式选择更新的 7.1 marshal 格式,并确保所有缓存已清空或已升级,否则升级后无法读取旧格式缓存。

4.2 enum 的否定作用域现在会包含 nil 值记录

Active Record 中 enum 的否定作用域(如 not_published)现在会把该字段为 nil 的记录一并纳入结果:

class Book < ApplicationRecord
  enum :status, [:proposed, :written, :published]
end

book1 = Book.create!(status: :published)
book2 = Book.create!(status: :written)
book3 = Book.create!(status: nil)

# Before

Book.not_published # => [book2]

# After

Book.not_published # => [book2, book3]

五、从 Rails 8.0 升级到 Rails 8.1

更多变更参见 8.1 release notes

5.1 schema.rb 中的表列现在按字母序排列

Active Record 默认按字母顺序对 schema.rb 中的表列排序,使各机器上的 dump 结果一致、不再随迁移顺序摇摆——大幅减少无意义的 diff 噪音。若需精确保留列顺序,可改用 structure.sql


六、从 Rails 7.2 升级到 Rails 8.0

Rails 8.0 没有额外的专项升级要点条目,仅需阅读 8.0 release notes 核对框架改动。


七、从 Rails 7.1 升级到 Rails 7.2

更多变更参见 7.2 release notes

7.1 所有测试现在都遵循 active_job.queue_adapter 配置

如果你在 config/application.rbconfig/environments/test.rb 中设置了 config.active_job.queue_adapter,在 7.2 之前它不会在所有测试中一致生效——部分测试用你选择的适配器,另一些测试则使用 TestAdapter。Rails 7.2 起,若提供了 queue_adapter 配置,所有测试都会遵循它

这可能导致测试报错:如果你把 queue_adapter 设成了 :test 以外的值,却编写了依赖 TestAdapter 行为的测试,升级后这些测试会失败。若未提供任何配置,则仍继续使用 TestAdapter

7.2 alias_attribute 现在绕过原属性上的自定义方法

Rails 7.2 中,alias_attribute 直接访问底层数据库值,不再调用原属性上的自定义方法(该变更在 7.1 中以弃用警告预告)。

Before(Rails 7.1):

class User < ActiveRecord::Base
  def email
    "custom_#{super}"
  end

  alias_attribute :username, :email
end

user = User.create!(email: "test@example.com")
user.username
# => "custom_test@example.com"

After(Rails 7.2):

user = User.create!(email: "test@example.com")
user.username
# => "test@example.com"  # Raw database value

如果你收到弃用警告 "Since Rails 7.2 #{method_name} will not be calling #{target_name} anymore",应手动定义别名方法:

class User < ActiveRecord::Base
  def email
    "custom_#{super}"
  end

  def username
    email  # This will call the custom email method
  end
end

或者改用 alias_method

class User < ActiveRecord::Base
  def email
    "custom_#{super}"
  end

  alias_method :username, :email
end

八、从 Rails 7.0 升级到 Rails 7.1

更多变更参见 7.1 release notes

8.1 development / test 环境的 secret_key_base 文件改名

development 与 test 环境下 Rails 读取 secret_key_base 的文件由 tmp/development_secret.txt 改名为 tmp/local_secret.txt。直接重命名旧文件为 local_secret.txt 即可继续沿用同一密钥,或把旧密钥复制到新文件。若不处理,应用启动时会在新文件 tmp/local_secret.txt 生成新密钥,使 development/test 环境所有既有 session/cookie 失效,并使由 secret_key_base 派生的其他签名(如 Active Storage / Action Text 附件)一并失效。生产等其他环境不受影响。

8.2 新的 ActiveSupport::Cache 序列化格式

7.1 提供一种针对纯字符串值(如视图片段)做了优化的新缓存格式。新应用默认启用 7.1 格式;既有应用可通过 config.load_defaults 7.1,或在 config/application.rb / config/environments/*.rb 中设置 config.active_support.cache_format_version = 7.1 启用。

6.1 或 7.0 格式写入的缓存条目在 7.1 格式下仍可读取。若要对 7.1 升级做滚动部署(尚未升级的服务器也要能读取已升级服务器的缓存),应在第一次部署保持缓存格式不变,后续部署再启用 7.1 格式。

8.3 自动加载路径不再加入 $LOAD_PATH

自 Rails 7.1 起,由自动加载器(autoloader)管理的目录不再被加入 $LOAD_PATH,因此无法再用手动 require 加载其中的文件(本就不该这么写)。这能加快未使用 bootsnap 应用的 require 速度,也能减小使用 bootsnap 时的缓存体积。

如需保留这些路径,可显式开启(但不推荐——自动加载路径中的类与模块应当被自动加载,即直接引用它们):

config.add_autoload_paths_to_load_path = true

注意:lib 目录不受该开关影响,它始终会被加入 $LOAD_PATH

8.4 config.autoload_libconfig.autoload_lib_once

如果应用没有把 lib 加入自动加载(autoload)或自动加载一次(autoload once)路径,可跳过本节。可用以下命令检查:

# Print autoload paths.
$ bin/rails runner 'pp Rails.autoloaders.main.dirs'

# Print autoload once paths.
$ bin/rails runner 'pp Rails.autoloaders.once.dirs'

lib 已在自动加载路径中,config/application.rb 里通常有类似配置:

# Autoload lib, but do not eager load it (maybe overlooked).
config.autoload_paths << config.root.join("lib")

或:

# Autoload and also eager load lib.
config.autoload_paths << config.root.join("lib")
config.eager_load_paths << config.root.join("lib")

或:

# Same, because all eager load paths become autoload paths too.
config.eager_load_paths << config.root.join("lib")

这些写法仍然有效,但推荐替换为更简洁的一行:

config.autoload_lib(ignore: %w(assets tasks))

请把 lib 下所有不含 .rb 文件、或不应被重载/预加载的子目录加入 ignore 列表,例如存在 lib/templateslib/generatorslib/middleware 时:

config.autoload_lib(ignore: %w(assets tasks templates generators middleware))

这一行会使 lib 中(未被忽略的)代码在 config.eager_loadtrue 时(production 模式默认值)被预加载。如果此前 lib 未被加入预加载路径、你仍希望如此,可单独退出:

Rails.autoloaders.main.do_not_eager_load(config.root.join("lib"))

config.autoload_lib_oncelib 位于 config.autoload_once_paths 时对应的等价方法。

8.5 ActiveStorage::BaseController 不再包含 streaming 模块

继承自 ActiveStorage::BaseController、并用 streaming 实现自定义文件服务逻辑的应用控制器,现在必须显式 include ActiveStorage::Streaming 模块。

8.6 MemCacheStoreRedisCacheStore 默认启用连接池

connection_pool gem 已成为 activesupport 的依赖,MemCacheStoreRedisCacheStore 默认使用连接池。若不想使用,可在配置 cache store 时把 :pool 设为 false

config.cache_store = :mem_cache_store, "cache.example.com", { pool: false }

连接池选项的详细说明参见仓库中的 caching_with_rails 指南

8.7 SQLite3Adapter 默认启用 strict strings 模式

strict strings 模式会禁用双引号字符串字面量。SQLite 对双引号字符串的处理较特殊:它先尝试把双引号内容当作标识符名,若不存在再当作字符串字面量——这会让拼写错误在无声无息中溜过(例如可能为一个不存在的列创建索引)。如需关闭该严格模式:

# config/application.rb
config.active_record.sqlite3_adapter_strict_strings_by_default = false

8.8 支持多个 ActionMailer::Preview 路径

config.action_mailer.preview_path 已被弃用,改用 config.action_mailer.preview_paths。向该配置追加路径即可让 Rails 在这些目录中查找 mailer 预览:

config.action_mailer.preview_paths << "#{Rails.root}/lib/mailer_previews"

8.9 config.i18n.raise_on_missing_translations = true 现在对任何缺失翻译都会抛错

之前只在视图或控制器中调用时抛错;现在只要 I18n.t 收到无法识别的 key 就会抛错:

# with config.i18n.raise_on_missing_translations = true

# in a view or controller:
t("missing.key") # raises in 7.0, raises in 7.1
I18n.t("missing.key") # didn't raise in 7.0, raises in 7.1

# anywhere:
I18n.t("missing.key") # didn't raise in 7.0, raises in 7.1

不需要该行为时可设回 false。或者自定义 I18n.exception_handler(参考仓库中的 i18n 指南)。同时 AbstractController::Translation.raise_on_missing_translations 已被移除;若曾依赖该私有 API,应迁移到 config.i18n.raise_on_missing_translations 或自定义异常处理器。

8.10 bin/rails test 现在会运行 test:prepare 任务

通过 bin/rails test 运行测试前会先执行 rake test:prepare。如果你增强过 test:preparetailwindcss-railsjsbundling-railscssbundling-rails 以及许多第三方 gem 都会这样做),这些增强逻辑会在测试前执行。注意:单独运行单文件测试(bin/rails test test/models/user_test.rb)时不会触发 test:prepare

8.11 @rails/ujs 的导入语法改变

Rails 7.1 起不再支持直接从 @rails/ujs 导入某个模块,例如下面的写法会失败:

import { fileInputSelector } from "@rails/ujs"
// ERROR: export 'fileInputSelector' (imported as 'fileInputSelector') was not found in '@rails/ujs' (possible exports: default)

应先整体导入 Rails 对象,再从其身上取模块:

import Rails from "@rails/ujs"
// Alias the method
const fileInputSelector = Rails.fileInputSelector
// Alternatively, reference it from the Rails object where it is used
Rails.fileInputSelector(...)

8.12 Rails.logger 现在返回 ActiveSupport::BroadcastLogger

ActiveSupport::BroadcastLogger 是一个可把日志广播到多个 sink(STDOUT、日志文件……)的新 logger。原先用于广播日志的私有 API(ActiveSupport::Logger.broadcast)已被移除,需要按下述方式改写:

logger = Logger.new("some_file.log")

# Before

Rails.logger.extend(ActiveSupport::Logger.broadcast(logger))

# After

Rails.logger.broadcast_to(logger)

若应用配置了自定义 logger,Rails.logger 会包装并代理所有方法到它之上,无需额外改动。需要访问自定义 logger 实例时可用 broadcasts 方法:

# config/application.rb
config.logger = MyLogger.new

# Anywhere in your application
puts Rails.logger.class #=> BroadcastLogger
puts Rails.logger.broadcasts #=> [MyLogger]

8.13 Active Record Encryption 算法变更

Active Record Encryption 现改用 SHA-256 作为哈希摘要算法。针对旧版本加密的数据,按两种情况处理:

  1. 若你配置了 config.active_support.key_generator_hash_digest_class 为 SHA-1(Rails 7.0 前的默认值),则 Active Record Encryption 也要配置为 SHA-1:

    config.active_record.encryption.hash_digest_class = OpenSSL::Digest::SHA1
    

    如果你所有数据都是非确定性加密的(默认行为,除非 encrypts 传了 deterministic: true),也可改为像场景 2 一样配置 SHA-256,并同时开启下述开关以支持解密旧列:

    config.active_record.encryption.support_sha1_for_non_deterministic_encryption = true
    
  2. config.active_support.key_generator_hash_digest_class 配置为 SHA-256(7.0 新默认值),则 Active Record Encryption 也配置为 SHA-256:

    config.active_record.encryption.hash_digest_class = OpenSSL::Digest::SHA256
    

关于 hash_digest_class 详见配置 Rails 应用指南。此外,新配置项 config.active_record.encryption.support_sha1_for_non_deterministic_encryption 用于修复一个 bug:即便通过上述 hash_digest_class 配置了 SHA-256,部分属性仍被 SHA-1 加密。该开关在 7.1 中默认关闭;如果你在 Rails < 7.1 上加密的数据可能受该 bug 影响,应开启它:

config.active_record.encryption.support_sha1_for_non_deterministic_encryption = true

正在使用加密数据的读者请务必仔细核对上述内容。

8.14 Controller/Integration/System 测试中异常处理方式的变化

config.action_dispatch.show_exceptions 控制 Action Pack 如何响应请求时抛出的异常。7.1 之前它是布尔值:true 表示 rescue 异常并渲染 HTML 错误页(如以 404 Not Found 渲染 public/404.html 而非抛出 ActiveRecord::RecordNotFound),false 表示不 rescue。7.1 将其可取值改为 :all:rescuable:none

  • :all —— 对所有异常渲染 HTML 错误页(等价于原 true);
  • :rescuable —— 仅对 config.action_dispatch.rescue_responses(见配置指南)声明的异常渲染错误页;
  • :none —— 不 rescue 任何异常(等价于原 false)。

Rails 7.1 及之后生成的测试环境会设置 config.action_dispatch.show_exceptions = :rescuable。升级时可改用 :rescuable 体验新行为,或按对应关系替换旧值(:all 替代 true:none 替代 false)。


九、从 Rails 6.1 升级到 Rails 7.0

更多变更参见 7.0 release notes

9.1 button_to 行为改变

从 Rails 7.0 起,若用持久化的 Active Record 对象构建按钮 URL,button_to 会渲染 patch 动词的 form。想保持原行为可显式传 method:

-button_to("Do a POST", [:my_custom_post_action_on_workshop, Workshop.find(1)])
+button_to("Do a POST", [:my_custom_post_action_on_workshop, Workshop.find(1)], method: :post)

或使用路径辅助方法:

-button_to("Do a POST", [:my_custom_post_action_on_workshop, Workshop.find(1)])
+button_to("Do a POST", my_custom_post_action_on_workshop_workshop_path(Workshop.find(1)))

9.2 Spring 需要 ≥ 3.0.0

若使用 Spring,至少升级到 3.0.0,否则会报 undefined method 'mechanism=' for ActiveSupport::Dependencies:Module。同时确保 config/environments/test.rbconfig.cache_classes 设为 false

9.3 Sprockets 成为可选依赖

rails gem 不再依赖 sprockets-rails。仍需使用 Sprockets 的应用要在 Gemfile 中自行添加:

gem "sprockets-rails"

9.4 应用必须以 zeitwerk 模式运行

仍以 classic 模式运行的应用必须切换到 zeitwerk 模式(可参考仓库中的 autoloading_and_reloading_constants 指南)。Rails 7 已删除设置自动加载模式的入口 config.autoloader=;若你之前显式设置为 :zeitwerk,直接删除即可。

ActiveSupport::Dependencies 的私有 API 已被整体删除,包括 hook!unhook!depend_onrequire_or_loadmechanism 等。几个高亮:

  • ActiveSupport::Dependencies.constantize / safe_constantize 改为 String#constantize / String#safe_constantize

    ActiveSupport::Dependencies.constantize("User") # NO LONGER POSSIBLE
    "User".constantize # 👍
    
  • ActiveSupport::Dependencies.mechanism(读写)改为通过 config.cache_classes 控制;

  • 追踪自动加载器活动:ActiveSupport::Dependencies.verbose= 已不可用,在 config/application.rbRails.autoloaders.log! 即可;

  • 辅助内部类如 ActiveSupport::Dependencies::ReferenceBlamable 等也已移除。

9.5 初始化期间的自动加载

自 Rails 6.0 起,应用若在 to_prepare 块之外于初始化阶段自动加载可重载常量,会收到弃用警告("Initialization autoloaded the constant ...")。Rails 7 中该场景会直接抛 NameError,请参照自动加载指南中"应用启动时自动加载"一节修正。once 自动加载器管理的常量允许在初始化期间自动加载并正常使用;为支持这一点,once 自动加载器被更早设置。若应用有自定义 inflect 且需要 once 自动加载器感知,需把 config/initializers/inflections.rb 中的代码移入 config/application.rb 的应用类定义体内:

module MyApp
  class Application < Rails::Application
    # ...

    ActiveSupport::Inflector.inflections(:en) do |inflect|
      inflect.acronym "HTML"
    end
  end
end

9.6 可配置 config.autoload_once_paths

config.autoload_once_paths 现在可在 config/application.rb 应用类定义体内或 config/environments/* 中设置,引擎同样可在引擎类体内或环境配置中配置。设置后该集合被冻结,你可以从这些路径自动加载(尤其是初始化期间),它们由 Rails.autoloaders.once 管理——只自动加载/预加载、不重载。若在环境配置处理完之后才设置并抛 FrozenError,请把代码上移。

9.7 content_type 原样返回 Content-Type 头

ActionDispatch::Request#content_type 现在返回包含 charset 在内的完整头;只想取 MIME 类型用 media_type

Before:

request = ActionDispatch::Request.new("CONTENT_TYPE" => "text/csv; header=present; charset=utf-16", "REQUEST_METHOD" => "GET")
request.content_type #=> "text/csv"

After:

request = ActionDispatch::Request.new("Content-Type" => "text/csv; header=present; charset=utf-16", "REQUEST_METHOD" => "GET")
request.content_type #=> "text/csv; header=present; charset=utf-16"
request.media_type   #=> "text/csv"

9.8 Key generator 摘要类改为 SHA256,需要 cookie rotator

Key generator 的默认摘要类由 SHA1 改为 SHA256,影响 Rails 生成的所有加密消息(包括加密 cookie)。为了能读取旧摘要类生成的消息,需要注册 rotator,否则升级期间用户 session 可能失效。以下为加密与签名 cookie 的 rotator 示例:

# config/initializers/cookie_rotator.rb
Rails.application.config.after_initialize do
  Rails.application.config.action_dispatch.cookies_rotations.tap do |cookies|
    authenticated_encrypted_cookie_salt = Rails.application.config.action_dispatch.authenticated_encrypted_cookie_salt
    signed_cookie_salt = Rails.application.config.action_dispatch.signed_cookie_salt

    secret_key_base = Rails.application.secret_key_base

    key_generator = ActiveSupport::KeyGenerator.new(
      secret_key_base, iterations: 1000, hash_digest_class: OpenSSL::Digest::SHA1
    )
    key_len = ActiveSupport::MessageEncryptor.key_len

    old_encrypted_secret = key_generator.generate_key(authenticated_encrypted_cookie_salt, key_len)
    old_signed_secret = key_generator.generate_key(signed_cookie_salt)

    cookies.rotate :encrypted, old_encrypted_secret
    cookies.rotate :signed, old_signed_secret
  end
end

9.9 ActiveSupport::Digest 摘要类改为 SHA256

默认摘要类由 SHA1 改为 SHA256,会影响 Etag、缓存键等。键的变化会冲击缓存命中率,升级到新哈希时务必留意监控。

9.10 新的 ActiveSupport::Cache 序列化格式(7.0)

7.0 引入更快、更紧凑的序列化格式。启用方式二选一:

# config/application.rb

config.load_defaults 6.1
config.active_support.cache_format_version = 7.0

或直接:

# config/application.rb

config.load_defaults 7.0

注意:Rails 6.1 应用无法读取这种新格式。为平滑升级,首次部署 Rails 7.0 时应保持 config.active_support.cache_format_version = 6.1,待所有 Rails 进程都更新完毕后再切到 7.0。Rails 7.0 能同时读取两种格式,因此升级期间缓存不会被整体失效。

9.11 Active Storage 视频预览图生成方式改变

视频预览图改用 FFmpeg 的场景切换检测(scene change detection)生成更有意义的预览帧(此前取第一帧,视频从黑场淡入时会有问题)。该改动要求 FFmpeg v3.4+

9.12 Active Storage 默认 variant 处理器改为 :vips

新应用默认用 libvips 替代 ImageMagick 做图像变换,可降低生成 variant 的时间与 CPU/内存占用。:mini_magick 并未被弃用,可继续使用。迁移到 libvips 需设置:

Rails.application.config.active_storage.variant_processor = :vips

并把既有图像变换代码改成 image_processing 宏,用 libvips 选项替换 ImageMagick 选项。具体差异如下:

resize 换成 resize_to_limit

- variant(resize: "100x")
+ variant(resize_to_limit: [100, nil])

不改会报 no implicit conversion to float from string

裁剪改用数组

- variant(crop: "1920x1080+0+0")
+ variant(crop: [0, 0, 1920, 1080])

不改会报 unable to call crop: you supplied 2 arguments, but operation needs 5

裁剪值必须落在图像范围内:Vips 比 ImageMagick 严格——

  1. x/y 为负数时不裁剪(如 [-10, -10, 100, 100]);
  2. 位置 + 尺寸超出图像时不裁剪(如 125×125 图像裁剪 [50, 50, 100, 100])。 否则报 extract_area: bad extract area

resize_and_pad 的背景色:Vips 默认黑色,ImageMagick 默认白色,用 background 指定:

- variant(resize_and_pad: [300, 300])
+ variant(resize_and_pad: [300, 300, background: [255]])

移除基于 EXIF 的旋转:Vips 处理 variant 时会按 EXIF 自动旋转。若你存储了用户照片的旋转值再用 ImageMagick 旋转,必须停止:

- variant(format: :jpg, rotate: rotation_value)
+ variant(format: :jpg)

单色图改用 colourspace

- variant(monochrome: true)
+ variant(colourspace: "b-w")

改用 libvips 的压缩选项

JPEG:

- variant(strip: true, quality: 80, interlace: "JPEG", sampling_factor: "4:2:0", colorspace: "sRGB")
+ variant(saver: { strip: true, quality: 80, interlace: true })

PNG:

- variant(strip: true, quality: 75)
+ variant(saver: { strip: true, compression: 9 })

WEBP:

- variant(strip: true, quality: 75, define: { webp: { lossless: false, alpha_quality: 85, thread_level: 1 } })
+ variant(saver: { strip: true, quality: 75, lossless: false, alpha_q: 85, reduction_effort: 6, smart_subsample: true })

GIF:

- variant(layers: "Optimize")
+ variant(saver: { optimize_gif_frames: true, optimize_gif_transparency: true })

部署到生产:Active Storage 会把变换列表编码进图片 URL。如果应用缓存了这些 URL,部署新代码后图片会失效,必须手动清理受影响的缓存键。例如视图里有:

<% @products.each do |product| %>
  <% cache product do %>
    <%= image_tag product.cover_photo.variant(resize: "200x") %>
  <% end %>
<% end %>

可通过 touch 产品或改缓存键来失效:

<% @products.each do |product| %>
  <% cache ["v2", product] do %>
    <%= image_tag product.cover_photo.variant(resize_to_limit: [200, nil]) %>
  <% end %>
<% end %>

9.13 schema dump 中包含 Rails 版本

Rails 7.0 改变了一些列类型的默认值。为避免 6.1 → 7.0 升级的应用用 7.0 新默认值加载当前 schema,schema dump 中会写入框架版本。首次在 7.0 加载 schema 前务必运行 bin/rails app:update,确保 dump 含版本信息:

# This file is auto-generated from the current state of the database. Instead
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
#
# This file is the source Rails uses to define your schema when running `bin/rails
# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
# be faster and is potentially less error prone than running all of your
# migrations from scratch. Old migrations may fail to apply correctly if those
# migrations use external dependencies or application code.
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[6.1].define(version: 2022_01_28_123512) do
  # ...
end

注意:首次用 Rails 7.0 dump schema 时会看到大量变化(含部分列信息),务必审阅并提交新 schema 文件。


十、从 Rails 6.0 升级到 Rails 6.1

更多变更参见 6.1 release notes

10.1 Rails.application.config_for 返回值不再支持 String 键访问

给定配置:

# config/example.yml
development:
  options:
    key: value
Rails.application.config_for(:example).options

过去可对该返回值用 String 键取值;该行为在 6.0 弃用,现在不再可用。仍想用 String 键,可对返回值调用 with_indifferent_access

Rails.application.config_for(:example).with_indifferent_access.dig("options", "key")

10.2 respond_to#any 时响应的 Content-Type

使用 respond_to { |format| format.any } 时,响应头 Content-Type 现在基于块内实际渲染内容而非请求格式:

def my_action
  respond_to do |format|
    format.any { render(json: { foo: "bar" }) }
  end
end
get("my_action.csv")

此前会错误返回 text/csv,现在正确返回 application/json。若依赖旧行为,建议显式声明 action 接受的格式:

format.any(:xml, :json) { render request.format.to_sym => @people }

10.3 halted_callback_hook 增加第二个参数

Active Support 允许重写 halted_callback_hook(回调链被中止时触发)。该方法现在接收第二个参数:被中止的回调名。重写该方法时务必接受两个参数。这是没有先经弃用周期的破坏性变更(出于性能考虑):

class Book < ApplicationRecord
  before_save { throw(:abort) }
  before_create { throw(:abort) }

  def halted_callback_hook(filter, callback_name) # => This method now accepts 2 arguments instead of 1
    Rails.logger.info("Book couldn't be #{callback_name}d")
  end
end

10.4 控制器 helper 类方法改用 String#constantize

概念上,6.1 之前 helper "foo/bar" 等价于:

require_dependency "foo/bar_helper"
module_name = "foo/bar_helper".camelize
module_name.constantize

现在改为:

prefix = "foo/bar".camelize
"#{prefix}Helper".constantize

多数应用向后兼容、无需改动。但若控制器把 helpers_path 指向 $LOAD_PATH 中不属于自动加载路径的目录,该用法不再开箱即用——helper 模块不可自动加载时,应用需在调用 helper 前自行加载它。

10.5 HTTP → HTTPS 重定向改用 308

ActionDispatch::SSL 把非 GET/HEAD 请求从 HTTP 重定向到 HTTPS 时默认状态码改为 308(RFC 7538)。

10.6 Active Storage 现在要求 image_processing

处理 variant 时,必须引入 image_processing gem 而不再直接使用 mini_magick。image_processing 默认在后台用 mini_magick,因此最简单的升级方式是把 mini_magick 换成 image_processing,并移除不再需要的显式 combine_options。为可读性,可把原始 resize 调用改成 image_processing 宏,例如把:

video.preview(resize: "100x100")
video.preview(resize: "100x100>")
video.preview(resize: "100x100^")

分别改成:

video.preview(resize_to_fit: [100, 100])
video.preview(resize_to_limit: [100, 100])
video.preview(resize_to_fill: [100, 100])

10.7 新的 ActiveModel::Error

错误对象现在是新的 ActiveModel::Error 类实例,API 有改动:部分用法会直接报错,另一些会打印弃用警告(供 Rails 7.0 前清理)。


十一、从 Rails 5.2 升级到 Rails 6.0

更多变更参见 6.0 release notes

11.1 Webpacker 非默认启用

Webpacker 是 Rails 6 的默认 JS 编译器,但升级应用不会自动启用它。想用需加入 Gemfile 并安装:

gem "webpacker"
$ bin/rails webpacker:install

11.2 Force SSL

控制器上的 force_ssl 方法被弃用(6.1 移除)。建议改用全局的 config.force_ssl 强制全站 HTTPS;需要豁免部分端点重定向时,用 config.ssl_options 配置。

11.3 签名/加密 cookie 内嵌 purpose 与 expiry 元数据

为增强安全,Rails 把 purpose 与 expiry 元数据嵌入签名/加密 cookie 值,以阻止攻击者复制某个 cookie 的值充当另一个 cookie 使用。嵌入元数据使这些 cookie 与 6.0 之前的 Rails 不兼容。若 cookie 需要被 Rails 5.2 及更早版本读取,或仍在验证 6.0 部署以便回滚,可设置 Rails.application.config.action_dispatch.use_cookies_with_metadata = false

11.4 npm 包全部移入 @rails scope

若之前通过 npm/yarn 加载过相关包,升级到 6.0.0 前必须更新依赖名:

actioncable   → @rails/actioncable
activestorage → @rails/activestorage
rails-ujs     → @rails/ujs

11.5 Action Cable JavaScript API 变化

Action Cable JS 包由 CoffeeScript 转为 ES2015,npm 发行版现在包含源码。可选 API 有破坏性变更:

  • WebSocket 适配器与 logger 适配器的配置从 ActionCable 属性移到 ActionCable.adapters

    -    ActionCable.WebSocket = MyWebSocket
    +    ActionCable.adapters.WebSocket = MyWebSocket
    
    -    ActionCable.logger = myLogger
    +    ActionCable.adapters.logger = myLogger
    
  • ActionCable.startDebugging() / stopDebugging() 被移除,改用属性 ActionCable.logger.enabled

    -    ActionCable.startDebugging()
    +    ActionCable.logger.enabled = true
    
    -    ActionCable.stopDebugging()
    +    ActionCable.logger.enabled = false
    

11.6 ActionDispatch::Response#content_type 原样返回

同样地,响应对象的 content_type 现在包含 charset;只想取 MIME 用 media_type

resp = ActionDispatch::Response.new(200, "Content-Type" => "text/csv; header=present; charset=utf-16")
resp.content_type #=> "text/csv; header=present"   (6.0 前)
resp.content_type #=> "text/csv; header=present; charset=utf-16"  (6.0 起)
resp.media_type   #=> "text/csv"

11.7 新的 config.hosts 设置

出于安全目的新增 config.hosts。它在 development 下默认允许 localhost;用了其他域名需显式放行:

# config/environments/development.rb

config.hosts << "dev.myapp.com"
config.hosts << /[a-z0-9-]+\.myapp\.com/ # Optionally, regexp is allowed as well

其他环境下 config.hosts 默认为空(Rails 不校验 host);也可在生产按需添加做校验。

11.8 自动加载(zeitwerk 时代开启)

Rails 6 默认配置 config.load_defaults 6.0 在 CRuby 上启用 zeitwerk 自动加载模式——自动加载、重载与预加载全部由 Zeitwerk 管理(参考自动加载指南)。使用旧版本默认值的应用可这样开启:

# config/application.rb

config.autoloader = :zeitwerk

公共 API:应用一般无需直接使用 Zeitwerk API,Rails 会依据既有契约(config.autoload_pathsconfig.cache_classes 等)配置好一切。实际 loader 对象可通过 Rails.autoloaders.main 访问(如预加载 STI 类或配置自定义 inflector 时会用到)。

项目结构:正常自动加载的应用结构大体兼容。但 classic 模式由缺失常量名推断文件名(underscore),zeitwerk 由文件名推断常量名(camelize);两者并非总是互逆,尤其涉及首字母缩写时——"FOO".underscore"foo",而 "foo".camelize"Foo" 而非 "FOO"。可用任务检查兼容性:

$ bin/rails zeitwerk:check
Hold on, I am eager loading the application.
All is good!

require_dependency:已知使用场景已全部消除,应全文搜索并删除它们。STI 使用见自动加载指南中的 STI 章节

类/模块定义中的限定名:现在可以在类与模块定义中稳健地使用常量路径:

class Admin::UsersController < ApplicationController
  # ...
end

注意坑:classic 下 class Foo::Bar 体内引用 Wadus 有时能自动加载 Foo::Wadus,这不符合 Ruby 语义,在 zeitwerk 下完全不工作。可改用限定名 Foo::Wadus,或把 Foo 放入嵌套:

module Foo
  class Bar
    Wadus
  end
end

Concerns:可正常从 app/models/concerns 自动加载/预加载。app/models/concerns 作为根目录(属于自动加载路径)被忽略命名空间,因此 app/models/concerns/foo.rb 应定义 Foo 而非 Concerns::Foo。classic 下的 Concerns:: 命名空间只是实现的副作用,使用它的应用需重命名才能在 zeitwerk 下运行。

app 在自动加载路径中:若项目想用 app/api/base.rb 定义 API::Base(classic 下把 app 加入自动加载路径),由于 Rails 会把 app 的所有子目录自动加入自动加载路径,会出现嵌套根目录,该做法不再工作。想保留结构需在 initializer 中删除子目录:

ActiveSupport::Dependencies.autoload_paths.delete("#{Rails.root}/app/api")

自动加载常量与显式命名空间:如果命名空间在文件中定义(如 app/models/hotel.rb 定义 Hotelapp/models/hotel/pricing.rb 定义 Hotel::Pricing),Hotel 常量必须用 class/module 关键字定义:

class Hotel
end

Hotel = Class.newHotel = Struct.new 之类写法不行——子对象 Hotel::Pricing 将找不到。该限制仅针对显式命名空间。

一文件一常量:classic 下同一文件可定义多个顶层常量并全部重载;zeitwerk 下 app/models/foo.rb 同时定义 FooBar 时,Bar 无法自动加载。必须把 Bar 移到自己的文件 bar.rb。内部类不受此限——重载 FooFoo::InnerClass 也会一并重载。

Spring 与 test 环境:test 环境需开启重载(config.cache_classes = false),否则报 reloading is disabled because config.cache_classes is true

Bootsnap:需 ≥ 1.4.2;若运行 Ruby 2.5,由于解释器 bug 需禁用 iseq 缓存(此时至少用 1.4.4)。

config.add_autoload_paths_to_load_path:该配置点默认 true(向后兼容),允许你选择不把自动加载路径加入 $LOAD_PATH。多数应用都应关闭它——你本就不该 require app/models 下的文件,Zeitwerk 内部只使用绝对文件名。关闭后 $LOAD_PATH 查找更快、Bootsnap 无需为这些目录建索引从而省内存(参考配置指南)。

线程安全:classic 模式下常量自动加载非线程安全(Rails 虽有锁让 web 请求线程安全);zeitwerk 模式自动加载线程安全,例如可在 runner 命令的多线程脚本里自动加载。

config.autoload_paths 中的 glob:警惕 config.autoload_paths += Dir["#{config.root}/lib/**/"]。每个元素应代表顶层命名空间(Object),不能嵌套(concerns 目录除外)。修复方式是去掉通配符:

config.autoload_paths << "#{config.root}/lib"

预加载与自动加载一致性:classic 下若 app/models/foo.rb 定义了 Bar,自动加载找不到该文件但预加载能(盲目递归加载文件),可能造成"预加载测试通过、运行时自动加载失败"的隐患;zeitwerk 下两种模式行为一致。

如何在 Rails 6 中继续用 classic 自动加载器

# config/application.rb

config.load_defaults 6.0
config.autoloader = :classic

若在 Rails 6 应用继续使用 classic 加载器,出于线程安全问题,建议在 development 环境中把 web 服务器与后台进程的并发级别设为 1。

11.9 Active Storage 赋值行为变化

Rails 5.2 默认下,对 has_many_attached 声明的附件集合赋值是追加新文件:

class User < ApplicationRecord
  has_many_attached :highlights
end

user.highlights.attach(filename: "funky.jpg")
user.highlights.count # => 1

blob = ActiveStorage::Blob.create_after_upload!(filename: "town.jpg")
user.update!(highlights: [ blob ])

user.highlights.count # => 2
user.highlights.first.filename # => "funky.jpg"
user.highlights.second.filename # => "town.jpg"

Rails 6.0 默认改为替换既有文件(与 Active Record 集合关联赋值行为一致):

user.highlights.attach(filename: "funky.jpg")
user.highlights.count # => 1

blob = ActiveStorage::Blob.create_after_upload!(filename: "town.jpg")
user.update!(highlights: [ blob ])

user.highlights.count # => 1
user.highlights.first.filename # => "town.jpg"

#attach 可用来新增而不移除已有附件:

blob = ActiveStorage::Blob.create_after_upload!(filename: "town.jpg")
user.highlights.attach(blob)

user.highlights.count # => 2

既有应用可通过设置 config.active_storage.replace_on_assign_to_manytrue 选择新行为。旧行为在 Rails 7.0 弃用、7.1 移除。

11.10 自定义异常处理应用

非法的 AcceptContent-Type 请求头现在会抛异常。默认的 config.exceptions_app 会专门处理并补偿该错误;自定义 exceptions 应用也需要处理,否则这类请求会落入回退的 exceptions 应用并返回 500 Internal Server Error


十二、从 Rails 5.1 升级到 Rails 5.2

更多变更参见 5.2 release notes

12.1 Bootsnap

Rails 5.2 在新应用的 Gemfile 中加入 bootsnap,app:update 会在 boot.rb 中配置好它。想使用则加入 Gemfile:

# Reduces boot times through caching; required in config/boot.rb
gem "bootsnap", require: false

否则把 boot.rb 改成不使用 bootsnap。

12.2 签名/加密 cookie 内嵌过期时间

为增强安全,expiry 信息也嵌入签名/加密 cookie 值,使这些 cookie 与 5.2 之前的 Rails 不兼容。若 cookie 需被 5.1 及更早版本读取,或处于 5.2 部署验证期需支持回滚,可设置 Rails.application.config.action_dispatch.use_authenticated_cookie_encryptionfalse


十三、从 Rails 5.0 升级到 Rails 5.1

更多变更参见 5.1 release notes

13.1 顶层 HashWithIndifferentAccess 软弃用

顶层 HashWithIndifferentAccess 应逐步迁移为 ActiveSupport::HashWithIndifferentAccess。它只是"软弃用"——代码暂时不会坏、也没有警告,但该常量将来会被移除。若 YAML 文档 dump 过这类对象,建议重新 load/dump 一次以确保引用正确常量。

13.2 application.secrets 的键统一为 symbol

config/secrets.yml 的嵌套配置键现在都以 symbol 加载,字符串访问需要修改:

Rails.application.secrets[:smtp_settings]["address"]

改为:

Rails.application.secrets[:smtp_settings][:address]

13.3 render :textrender :nothing 被移除

render :text 不再工作;以 text/plain MIME 渲染文本请用 render :plainrender :nothing 已移除,仅发送响应头用 head 方法,如 head :ok 返回 200 空 body。

13.4 redirect_to :back 被移除

redirect_to :back 在 5.0 弃用、5.1 彻底移除。替代为 redirect_back,务必提供 fallback_location(用于 HTTP_REFERER 缺失时):

redirect_back(fallback_location: root_path)

十四、从 Rails 4.2 升级到 Rails 5.0

更多变更参见 5.0 release notes

14.1 要求 Ruby 2.2.2+

Rails 5.0 起仅支持 Ruby 2.2.2+,升级前先确保 Ruby 版本达标。

14.2 模型默认继承 ApplicationRecord

Rails 5.0 起所有模型默认继承自 ApplicationRecord(如同控制器继承 ApplicationController),为应用提供统一配置模型行为的位置。需在 app/models/ 创建 application_record.rb

class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
end

并让所有模型改继承它。

14.3 通过 throw(:abort) 中止回调链

4.2 中 Active Record / Active Model 的 before 回调返回 false 会中止整个回调链;5.0 起返回 false 不再有该副作用,必须显式 throw(:abort)。4.2 → 5.0 升级期间返回 false 仍中止链但会收到弃用警告。准备好后可加入配置消除警告:

config/application.rb
ActiveSupport.halt_callback_chains_on_return_false = false

注意该选项不影响 Active Support 自身的回调(它们从不因返回值中止链)。

14.4 Active Job 默认继承 ApplicationJob

与模型类似,需在 app/jobs/ 创建 application_job.rb

class ApplicationJob < ActiveJob::Base
end

并让所有 job 继承它。

14.5 控制器测试

assignsassert_template 被抽到 rails-controller-testing gem,需在 Gemfile 添加后才能继续使用(RSpec 用户还需按 gem 文档做额外配置)。上传文件:测试中使用的 ActionDispatch::Http::UploadedFile 应替换为 Rack::Test::UploadedFile

14.6 生产环境启动后禁用自动加载

生产环境默认在 boot 后禁用自动加载。预加载是启动流程的一部分:顶层常量正常、无需 require;运行期才执行的深层常量(如方法体内)也没问题,因为定义它们的文件在 boot 时已被预加载。极少数需要在生产运行期自动加载的应用可设置 Rails.application.config.enable_dependency_loading = true

14.7 其他移除与替换

  • XML 序列化ActiveModel::Serializers::Xml 抽到 activemodel-serializers-xml gem,需自行添加;
  • legacy mysql 适配器被移除:改用 mysql2
  • debugger 不支持(Ruby 2.2),改用 byebug
  • bin/rails 运行任务与测试bin/rails testbin/rails dev:cache,在应用根目录运行 bin/rails 查看命令列表;
  • ActionController::Parameters 不再继承 HashWithIndifferentAccessparams 返回对象而非 hash;用 map 等方法前先 permit 再转 hash:params.permit([:proceed_to, :return_to]).to_h
  • protect_from_forgery 默认 prepend: false:按调用位置插入回调链;想始终最先执行用 protect_from_forgery prepend: true
  • 默认模板 handler 变为 RAW:无 handler 扩展名的文件不再走 ERB,需给文件加正确扩展名;
  • 模板依赖支持通配符:三条 Template Dependency 注释可合并为一条,如 recordings/threads/events/*
  • content_tag_for/div_for 移除:改用 content_tag 或加 record_tag_helper gem;
  • protected_attributesactiverecord-deprecated_finders gem 不再被支持
  • 测试顺序默认随机ActiveSupport::TestCase 默认 :random;设回 :sortedconfig.active_support.test_order = :sorted
  • ActionController::Live 变成 Concern:在自定义模块中 include ActionController::Live 需同时 extend ActiveSupport::Concern,否则生产环境会出问题。

14.8 新的框架默认值

  • belongs_to 默认必填:关联缺失默认触发校验错误,可用 optional: true 按关联关闭。既有应用需在 initializer 开启:

    config.active_record.belongs_to_required_by_default = true
    

    可逐模型覆盖:

    class Book < ApplicationRecord
      self.belongs_to_required_by_default = false
      belongs_to(:author)
    end
    
    class Car < ApplicationRecord
      self.belongs_to_required_by_default = true
      belongs_to(:pilot)
    end
    
  • Per-form CSRF tokens:每个表单拥有针对该表单 action/method 的独立 CSRF token,缓解 JS 创建表单的代码注入攻击:

    config.action_controller.per_form_csrf_tokens = true
    
  • Origin 检查:校验 HTTP Origin 头作为额外 CSRF 防线:

    config.action_controller.forgery_protection_origin_check = true
    
  • Action Mailer 队列名:默认 mailers,可全局改:

    config.action_mailer.deliver_later_queue_name = :new_queue_name
    
  • Mailer 视图片段缓存:用 config.action_mailer.perform_caching 控制:

    config.action_mailer.perform_caching = true
    
  • db:structure:dump 输出控制config.active_record.dump_schemas = :all(或 :schema_search_path);

  • HSTS + 子域config.ssl_options = { hsts: { subdomains: true } }

  • 保留接收者时区(Ruby 2.4):ActiveSupport.to_time_preserves_timezone = false

14.9 JSON/JSONB 序列化变化

Rails 5.0 起,JSON/JSONB 列若被赋为 String,Active Record 不再把它转成 Hash,而是原样返回字符串。这也影响 db/schema.rb 中相关列的 :default。建议不要给 JSON 列赋字符串,而应传 Hash(会被自动序列化为 JSON)。


十五、从 Rails 4.1 升级到 Rails 4.2

15.1 Web Console

在 Gemfile 的 :development 组添加 gem "web-console", "~> 2.0"bundle install(升级时不会自动包含)。安装后可在任意视图放 <%= console %>,dev 环境的错误页也会提供 console。

15.2 Responders

respond_with 与类级 respond_to 被抽到 responders gem,需在 Gemfile 添加 gem "responders", "~> 2.0"。实例级 respond_to 不受影响。

15.3 事务回调中的错误处理

after_rollback / after_commit 中的错误过去被抑制、仅打印日志;新版本会像其他回调一样正常传播。定义这类回调会收到弃用警告,可这样开启新行为:

config.active_record.raise_in_transactional_callbacks = true

15.4 测试用例顺序

Rails 5.0 起默认随机执行测试。4.2 引入 active_support.test_order 显式指定(:sorted 锁定现状或 :random 选择未来行为),未指定会发弃用警告:

# config/environments/test.rb
Rails.application.configure do
  config.active_support.test_order = :sorted # or `:random` if you prefer
end

15.5 序列化属性赋 nil

使用自定义 coder(如 serialize :metadata, JSON)时,赋 nil 会以 NULL 存入数据库,而不再把 nil 传入 coder(JSON coder 下不再是 "null")。

15.6 生产日志级别

Rails 5 计划把生产日志级别从 :info 改为 :debug。保持现状可在 production.rb 显式设置 config.log_level = :info

15.7 Rails 模板中的 after_bundle

模板若在 Bundler 运行前就把全部文件加入版本控制,会漏掉生成的 binstub。用 after_bundle 包裹 git 操作:

# template.rb
generate(:scaffold, "person name:string")
route "root to: 'people#index'"
rake("db:migrate")

after_bundle do
  git :init
  git add: "."
  git commit: %Q{ -m 'Initial commit' }
end

15.8 Rails HTML Sanitizer 与 DOM Testing

sanitizesanitize_cssstrip_tagsstrip_links 改用 Rails HTML Sanitizer(内部使用 Loofah,Loofah 再用 Nokogiri),sanitize 现在可接收 Loofah::Scrubber 实现强力 scrub,并新增 PermitScrubberTargetScrubber。需要旧实现可加 gem "rails-deprecated_sanitizer"assert_tagTagAssertions 被弃用,改用 rails-dom-testing gem 的 assert_select

15.9 Masked Authenticity Tokens

为缓解 SSL 攻击,form_authenticity_token 现在每次请求都会加掩码;token 校验通过"去掩码后解密"完成。依赖静态 session CSRF token 校验非 Rails 表单的策略需相应调整。

15.10 Action Mailer 的惰性求值

过去调用 mailer 类方法会立即执行对应实例方法;引入 Active Job 与 #deliver_later 后,实例方法的调用被推迟到 deliver_nowdeliver_later 时:

class Notifier < ActionMailer::Base
  def notify(user)
    puts "Called"
    mail(to: user.email)
  end
end
mail = Notifier.notify(user) # Notifier#notify is not yet called at this point
mail = mail.deliver_now           # Prints "Called"

多数应用无感知。若依赖旧的同步代理行为,应把非 mailer 方法定义为 mailer 类的类方法。

15.11 外键支持

迁移 DSL 支持外键定义。若在使用 Foreigner gem,可考虑移除,但 Rails 的外键支持是 Foreigner 的子集。迁移步骤:移除 Gemfile 中的 foreigner → bundle installbin/rake db:schema:dump → 确认 db/schema.rb 含全部外键定义及必要选项。


十六、从 Rails 4.0 升级到 Rails 4.1

16.1 远程 <script> 标签的 CSRF 防护

CSRF 防护现在也覆盖返回 JS 响应的 GET 请求,防止第三方站点用 <script> 远程引用你的 JS 提取敏感数据。因此 get :index, format: :js 这类功能/集成测试会触发 CSRF 防护,应改用 xhr :get, :index, format: :js 显式测试 XHR。注意:你自己的 <script> 标签默认也被视为跨域并拦截,确需从 <script> 加载 JS 时必须显式跳过这些 action 的 CSRF 防护。

16.2 Spring

Gemfile 加 gem "spring", group: :developmentbundle installbundle exec spring binstub。注意用户自定义 rake 任务默认在 development 环境运行。

16.3 config/secrets.yml

迁移到 secrets.yml 约定:创建 config/secrets.yml

development:
  secret_key_base:

test:
  secret_key_base:

production:
  secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>

secret_token.rb initializer 中的既有 secret_key_base 设置生产环境变量(或直接复制到 production 段);删除 secret_token.rb;用 rake secret 为 development/test 生成新密钥;重启服务器。

16.4 测试助手与其他

  • ActiveRecord::Migration.check_pending! 调用可移除(require "rails/test_help" 时会自动检查,保留也无害);

  • Cookies 序列化:4.1 之前用 Marshal。切到 JSON 用 config.action_dispatch.cookies_serializer = :hybrid(透明迁移既有 cookie)。注意 JSON 下 Date/Time 序列化为字符串、Hash 键会字符串化,建议 cookie 只存简单数据;

  • Flash 结构变化:键统一规范化为字符串。flash["string"]flash[:symbol] 都能读写,但遍历 flash.keys 恒得字符串键,比较 key 时用字符串;

  • JSON 处理:MultiJSON 已移除(用 obj.to_json + JSON.parse;切勿用 JSON.load 反序列化任意 Ruby 对象)。Rails 4.1 把自身编码器与 JSON gem 隔离(JSON.generate 不再触发 as_json 等 Rails 特性)。新编码器移除了循环结构检测、encode_json 钩子与 BigDecimal 数字化选项(需要可加 activesupport-json_encoder gem)。Time/DateTime/TimeWithZone#as_json 默认带毫秒精度,保留旧行为用 ActiveSupport::JSON::Encoding.time_precision = 0

  • 内联回调块内使用 returnbefore_save { return false } 会抛 LocalJumpError,应改成 before_save { false } 或定义具名方法;

  • Fixture 中定义的方法:4.1 起每个 fixture 的 ERB 在独立上下文求值,跨 fixture 共享的辅助方法应定义在 ActiveRecord::FixtureSet.context_class 上:

    module FixtureFileHelpers
      def file_sha(path)
        OpenSSL::Digest::SHA256.hexdigest(File.read(Rails.root.join("test/fixtures", path)))
      end
    end
    
    ActiveRecord::FixtureSet.context_class.include FixtureFileHelpers
    
  • I18n 强制可用 localeenforce_available_locales 默认 true(安全措施,防止用户输入被当作 locale),确需关闭用 config.i18n.enforce_available_locales = false

  • Relation 上的 mutator 方法Relation 不再有 map!delete_if 等变异方法,先 to_a 再用;

  • Default Scope 变更:default_scope 不再被同字段链式条件覆盖,而是像其他 scope 一样合并:

    class User < ActiveRecord::Base
      default_scope { where state: "pending" }
      scope :active, -> { where state: "active" }
      scope :inactive, -> { where state: "inactive" }
    end
    
    User.active
    # 之前: WHERE "users"."state" = 'active'
    # 之后: WHERE "users"."state" = 'pending' AND "
登录后查看全文
热门项目推荐
相关项目推荐