首页
/ Rails 8.2 Action View 变更日志精读:从 ERB 配置迁移到 datalist 表单助手的新特性全景

Rails 8.2 Action View 变更日志精读:从 ERB 配置迁移到 datalist 表单助手的新特性全景

2026-09-05 12:43:31作者:宣聪麟

本篇基于 Action View 组件的 actionview/CHANGELOG.md(当前仓库版本见 RAILS_VERSION,为 8.2.0.alpha 开发期)逐条解读本轮更新,覆盖国际化翻译作用域、表单标签助手修复、datalist 表单元素、ERB 选项配置迁移、render 缓存增强等主题,并结合仓库源码验证每条变更的实际实现位置。读完后你可以准确掌握 Action View 在本轮迭代中的行为变化清单、升级注意事项(哪些是破坏性变更、哪些只是修复),以及如何在视图层正确使用新特性。

一、变更总览

本轮 Action View 的更新可以归纳为五类:

类别 主要条目
新特性 datalist_tag / f.datalisttranslatescope: 相对路径、render_in 透传选项与块、cached: 集合缓存的 key:/expires_in:
破坏性/迁移 ERB 选项配置迁移到 ActionView::Basesafe_join 不再回退 $, 全局变量、DependencyTracker 初始化后注册的弃用
行为修复 file_field_tagaccept: 数组、无界 Range 支持、color_field 显式 value:tag 助手块内容合并、current_page? 支持 QUERY
导航助手重构 current_page?button_tolink_to 抽取到 ActionView::Helpers::NavigationHelper
测试/维护 ActionView::TestCase#render 重置 renderedViewReloader#deactivate 清理文件监听回调

二、国际化:translatescope: 支持当前模板相对路径

变更日志第一条指出:translate(及其别名 t)的 scope: 选项现在支持以点号开头、相对于当前模板解析,行为与 key 参数对齐:

# 在 posts/index.html.erb 中
translate("bar", scope: ".foo")   # 等价于 translate("posts.index.foo.bar")

仓库源码可以印证这一实现。translation_helper.rb 中新增了 scope_option_by_partial 方法:当 scope 是字符串或符号且以 . 开头时,将其与 scope_prefix 拼接。scope_prefix 基于当前模板的 @virtual_path 生成,把路径分隔符替换为点号(posts/indexposts.index),并按模板路径做了按 partial 的缓存(@_scope_key_by_partial_cache)。同时源码保留了与 key 参数一致的边界处理:当模板路径不可用时抛出 Cannot use scope: #{scope.inspect} shortcut because path is not available,符号类型的 scope 会保留符号类型返回(resolved.to_sym)。

这个变更的实践意义在于:在列表/详情页中引用同模块下的翻译键时不再需要手写完整前缀,例如 posts/index 模板里写 t("title", scope: ".posts") 即可定位到 posts.posts.title,减少重复字面量。

三、表单标签助手:一批输出正确性修复

3.1 file_field_tagaccept: 数组改为逗号连接

此前传入 accept: ["image/png", "image/gif"] 会渲染出 accept="image/png image/gif",而 HTML 规范中 accept 属性是逗号分隔列表;现在渲染为 accept="image/png,image/gif",与对象形式的 file_field 保持一致。

实现位于 form_tag_helper.rb

def file_field_tag(name, options = {})
  if options[:accept].is_a?(Array)
    options = options.merge(accept: options[:accept].join(","))
  end
  ...

方法入口处对 Array 类型的 accept 做一次 join(","),文档注释也同步补充了该示例。

3.2 number_field_tag / range_field_tag 支持无界 Range

此前传入无终止(endless)范围 18.. 或无起始(beginless)范围 ..10 作为 :in/:within 选项会抛 RangeError;现在 endless 范围只渲染 min 不渲染 max,beginless 范围只渲染 max 不渲染 min,与 number_field/range_field 行为一致。

源码中的关键改动在 form_tag_helper.rb#L956-L963

def number_field_tag(name, value = nil, options = {})
  options = options.stringify_keys
  options["type"] ||= "number"
  if range = options.delete("in") || options.delete("within")
    options.update("min" => range.begin, "max" => (range.max if range.end))
  end
  text_field_tag(name, value, options)
end

"max" => (range.max if range.end) 这一行正是 endless 支持的核心:range.endnil 时不设置 maxrange_field_tagform_tag_helper.rb#L981-L983)只是将 type: :range 合并后复用 number_field_tag,因此两者同步获得该能力。

3.3 color_field 尊重显式 value:(含 nil

此前 value: nil 会被忽略,字段仍从模型已存的颜色取值;现在显式提供 value: 即被尊重,只有完全未传 value: 时才回退到模型存储值。这一修复让"允许用户清空颜色"之类的场景无需额外绕路。

3.4 search_field 修复 autosave: true 引发的 NameError

变更日志明确记录了 search_field 传入 autosave: true 时抛 NameError 的问题已修复,属于典型的布尔选项误被当作变量名解析一类的模板层缺陷。

四、Tag 助手与输出安全:safe_jointag 块合并、哈希属性

4.1 safe_join 不再回退 $, 全局变量(破坏性变更)

变更日志说明:此前 safe_join 在未传分隔符时会使用 Ruby 的 $, 全局变量分隔元素;Ruby 已弃用 $, 十余年且该行为从未写入文档,现在默认分隔符改为 nil(不添加分隔符)。

当前实现见 output_safety_helper.rb

def safe_join(array, sep = nil)
  sep = ERB::Util.unwrapped_html_escape(sep)
  array.flatten.map! { |i| ERB::Util.unwrapped_html_escape(i) }.join(sep).html_safe
end

升级提示:如果你的应用依赖了 safe_join 默认用 $, 分隔的行为,请显式传入分隔符,否则元素之间将不再有任何分隔。

4.2 tag 参数内容与块内容改为合并而非覆盖

此前 tag.div("Hello ") { "World" } 只返回 <div>World</div>(块覆盖参数);现在返回 <div>Hello World</div>,参数内容作为前缀与块内容拼接。

4.3 跳过空属性名,避免生成非法 HTML

Tag 助手现在会跳过值为空的属性名,防止渲染出形如 name="" 之类的非法属性。

4.4 Hash/关键字选项渲染为连字符化(dasherized)HTML 属性

这是一个面向现代前端库(如 Hyperscript 风格属性)的便利特性:

tag.button "POST to /clicked", hx: { post: "/clicked", swap: :outerHTML, data: { json: true } }
# => <button hx-post="/clicked" hx-swap="outerHTML" hx-data="{&quot;json&quot;:true}">POST to /clicked</button>

嵌套 Hash 会逐层展开为 hx-posthx-swap 等属性,内部 Hash(如 data)则序列化为 JSON 字符串并转义后填入属性值。这省去了手工拼接 hx-* 属性的冗长写法。

五、datalist 表单元素:datalist_tagf.datalist

本轮新增了两层 datalist 支持。

5.1 datalist_tag(FormTagHelper)

datalist_tag('countries_datalist',
  ['Argentina',
   ['Brazil', { class: 'brazilian_option' }],
   ['Chile', 'CL', { disabled: true }]],
  { class: 'sa-countries-sample' })
# => <datalist id="countries_datalist" class="sa-countries-sample">
#      <option value="Argentina">Argentina</option>
#      <option value="Brazil" class="brazilian_option">Brazil</option>
#      <option value="CL" disabled="disabled">Chile</option>
#    </datalist>

源码实现非常薄(form_tag_helper.rb#L998-L1001):

def datalist_tag(id, option_tags = nil, html_options = {})
  option_tags ||= ""
  content_tag("datalist", options_for_select(option_tags), { "id" => id }.update(html_options.stringify_keys))
end

option_tags 参数直接复用 options_for_select 的容器格式,因此 ["Brazil", { class: ... }]["Chile", "CL", { disabled: true }] 等选项写法与下拉框完全一致;未传选项时 option_tags 归一为空字符串,避免 nil 进入 options_for_select

5.2 f.datalist(FormBuilder)

<%= form_with model: @post do |f| %>
  <%# 用同一派生 id 将输入框连接到 datalist: %>
  <%= f.text_field :country, list: f.field_id(:country, :datalist) %>
  <%= f.datalist :country, ["Argentina", "Brazil", "Chile"] %>
<% end %>

产出:

<input list="post_country_datalist" type="text" name="post[country]" id="post_country" />
<datalist id="post_country_datalist">
  <option value="Argentina">Argentina</option>
  <option value="Brazil">Brazil</option>
  <option value="Chile">Chile</option>
</datalist>

FormBuilder 侧的实现(form_options_helper.rb#L954-L955)就是对 datalist_tag 的转发,并用 field_id(method, "datalist") 派生 post_country_datalist 形式的 id;f.field_id(:country, :datalist) 生成同一 id,从而让 list 属性与 datalist 自动对齐,无需手写字符串。

六、导航助手:current_page? 支持 QUERY,且整体抽取为独立模块

6.1 current_page? 匹配 HTTP QUERY 请求

变更日志引入 RFC 10008 定义的 QUERY 方法:current_page? 现在可以通过 method: :query 匹配 QUERY 请求;而默认 method: :get 有意不匹配 QUERY,保持向后兼容。

current_page?('/search', method: :query)

签名见 navigation_helper.rb#L94

def current_page?(options = nil, check_parameters: false, method: :get, **options_as_kwargs)

method: 关键字参数默认值仍为 :get,因此既有行为不变,需要匹配 QUERY 的场景显式传入即可。

6.2 抽取 ActionView::Helpers::NavigationHelper

current_page?button_tolink_to 三个方法被提取到独立的 ActionView::Helpers::NavigationHelper 模块。从源码结构看,此举便于导航类助手单独被引入、测试与演进,对调用方是透明重构——控制器与视图无需任何改动。

七、日期助手:relative_time_in_words 接受 DateNumeric

此前向 relative_time_in_words 传入 DateNumeric 会抛 ArgumentError,而语义相近的 distance_of_time_in_words 却接受这些类型。本轮修复后两者行为对齐,直接返回时间距离字符串,省去了调用侧的 Time.zone.at(...) 之类的预处理。

八、破坏性迁移:ERB 选项配置迁移到 ActionView::Base

这是本轮最需要关注的迁移项。变更日志声明:配置 ERB 选项现在应在 ActionView::Base 类上进行,ActionView::Template::Handlers::ERB 类成为私有 API

此前直接在 ERB handler 上配置的 escape_ignore_list 等选项,现在应改为:

ActionView::Base.erb_trim_mode = nil
ActionView::Base.erb_implementation = ERB
ActionView::Base.escape_ignore_list = ["text/csv"]
ActionView::Base.strip_trailing_newlines = false

仓库源码证实了这一迁移方式:base.rb#L210 将四个 setter 委托给 ERB handler:

delegate :erb_trim_mode=, :erb_implementation=, :escape_ignore_list=, :strip_trailing_newlines=, to: "ActionView::Template::Handlers::ERB"

而 handler 侧(erb.rb)保留了带默认值的 class_attributeerb_trim_mode 默认 "-"escape_ignore_list 默认 ["text/plain"]strip_trailing_newlines 默认 false。渲染时(erb.rb#L82-L86)实际读取的是类级配置:strip_trailing_newlines 决定是否 chompescape_ignore_list.include?(template.type) 决定是否转义。此外,railtie 中仍有针对 escape_ignore_list 的 Ractor 共享化处理(railtie.rb#L70)。

升级建议:

  • ActionView::Template::Handlers::ERB.escape_ignore_list = [...] 一类调用改为 ActionView::Base 上的同名配置,或统一收敛到 config.action_view railtie 配置;
  • 直接依赖 ERB handler 内部行为的插件代码需要审计,因为该类已声明为私有 API。

九、DependencyTracker 弃用:应用初始化后注册 tracker

变更日志宣布弃用在应用初始化之后调用 ActionView::DependencyTracker.register_tracker,下个 Rails 版本将抛 FrozenError。仓库源码中已能确认对应的弃用提示(dependency_tracker.rb 内注册路径包含 "Registering a dependency tracker after application initialization is deprecated." 弃用消息)。

正确的做法是在应用初始化期间注册,并用 ActiveSupport.on_load(:action_view) 等待 Action View 加载完成:

ActiveSupport.on_load(:action_view) do
  ActionView::DependencyTracker.register_tracker(MyTracker.new)
end

这保证了自定义预编译依赖追踪器在视图模板编译体系冻结前完成注册。

十、渲染增强:render_in 透传、cached: 集合缓存、collection 块

10.1 render 将选项与块透传给自定义 #render_in

自定义 renderable 对象的 render_in 现在能收到渲染选项与块:

class Greeting
  def render_in(view_context, **)
    if block_given?
      view_context.render(html: yield)
    else
      view_context.render(inline: <<~ERB.strip, **)
        Hello <%= local_assigns[:name] || "World" %>
      ERB
    end
  end
end

render(Greeting.new)                                        # => "Hello, World"
render(Greeting.new, name: "Local")                         # => "Hello, Local"
render(renderable: Greeting.new, locals: { name: "Local" }) # => "Hello, Local"
render(Greeting.new) { "Hello, Block" }                     # => "Hello, Block"

这意味着 renderable 协议更完整:locals: 可以穿透到 local_assignsrenderable: + locals: 的组合写法也被支持,块内容则直接交由对象自行决定如何使用。

10.2 cached: 配合 collection: 支持 key:expires_in:

render 在使用 collection: 时,cached: 选项下新增 key:expires_in: 两个子选项,让集合渲染的每个片段可以自定义缓存键与过期时间,替代默认的模板派生策略。

10.3 集合渲染支持块

render 集合渲染现在接受块,块会对集合中每个渲染元素执行一次,可用于逐元素包装、标注或收集数据。

10.4 视图重载相关优化

  • ViewReloader#deactivate 现在会移除 file_system_resolver_hooks 回调,避免 fork 出的进程在清除 reloader 后每次 prepend_view_path 都触发文件系统扫描;
  • View watcher 的构建被推迟到视图路径真正注册时进行,减少启动期无用开销。

十一、测试与其他修复

  • ActionView::TestCase#render 重置 rendered:此前 memoization 引入后 rendered 不再随 render 调用重置,与文档描述不符,现已恢复约定行为(actionview/lib/action_view/test_case.rb)。
  • FormBuilder#to_partial_path 修复:名字不以 Builder 结尾的子类此前返回 nil,现在能正确推导 partial 路径。
  • collection_radio_buttons / collection_check_boxes 标签修复:当集合值为 nil 时,label 的 for 属性与 input 的 id 不一致的问题已修复,避免表单控件的无障碍关联断裂。

十二、升级检查清单

结合上述变更,升级到该版本时对视图层的检查建议如下:

  1. 搜索 $, 与裸调用 safe_join:确认无依赖默认 $, 分隔的代码;
  2. 搜索 ActionView::Template::Handlers::ERB. 的赋值调用:迁移到 ActionView::Baseconfig.action_view
  3. 检查 register_tracker 调用时机:移入 ActiveSupport.on_load(:action_view) 块中;
  4. 表单输出 diff:涉及 file_field_tag 数组 accept、无界 Range 数字输入、color_field 空值的应用,输出会如 3.1~3.3 节所述变化,属于修复性变更,回归测试应更新断言;
  5. current_page? 高亮逻辑:如果你新增了 QUERY 请求端点并希望导航高亮,显式传 method: :query

以上条目均对应 actionview/CHANGELOG.md 中的具体条目,涉及的核心实现分布在 translation_helper.rbform_tag_helper.rbform_options_helper.rboutput_safety_helper.rbnavigation_helper.rbbase.rberb.rb,可按文件路径进一步深入阅读源码与 actionview/test 下的对应测试用例。

登录后查看全文
热门项目推荐
相关项目推荐