学习目标:掌握 ActiveSupport 对 Ruby 核心类的扩展和提供的工具类
引言#
ActiveSupport 是 Rails 的基础库,为 Ruby 核心类提供了大量实用扩展。它是所有其他 Rails 组件的基础,理解 ActiveSupport 对于阅读 Rails 源码至关重要。
为什么重要:
- Rails 的其他所有组件都依赖它
- 它展示了 Ruby 元编程的强大能力
- 许多 API 设计模式值得学习
1. ActiveSupport 的组织结构#
activesupport/
├── lib/
│ └── active_support/
│ ├── core_ext/ # Ruby 核心类扩展
│ │ ├── string/ # String 扩展
│ │ ├── array/ # Array 扩展
│ │ ├── hash/ # Hash 扩展
│ │ ├── integer/ # Integer 扩展
│ │ └── ...
│ ├── concern.rb # Concern 模块
│ ├── callbacks.rb # 回调系统
│ ├── autoload.rb # 自动加载
│ ├── cache.rb # 缓存
│ ├── dependencies/ # 依赖管理
│ ├── inflector/ # 单复数转换
│ ├── notifications.rb # 事件通知
│ └── ...
2. Ruby 核心类扩展#
2.1 String 扩展#
ActiveSupport 为 String 添加了大量实用方法:
命名转换#
# activesupport/lib/active_support/core_ext/string/inflections.rb
"user_name".camelize # => "UserName"
"UserName".underscore # => "user_name"
"user_name".dasherize # => "user-name"
"user".pluralize # => "users"
"users".singularize # => "user"
"user_table".tableize # => "user_tables"
"User".constantize # => User (常量)
"user".classify # => "User"
"user".humanize # => "User"
# 外键转换
"User".foreign_key # => "user_id"
"User".foreign_key(false) # => "userid"
源码位置:activesupport/lib/active_support/core_ext/string/inflections.rb
实现原理:
class String
def camelize(first_letter = :upper)
case first_letter
when :upper
ActiveSupport::Inflector.camelize(self, true)
when :lower
ActiveSupport::Inflector.camelize(self, false)
end
end
end
时间相关#
"2 days".to_time # => Time 对象
"10 minutes".from_now # => 10 分钟后
"2 hours".ago # => 2 小时前
其他实用方法#
"hello".inquiry # => ActiveSupport::StringInquirer
"production".inquiry.production? # => true
" hello ".squish # => "hello" (移除多余空格)
"hello".truncate(3) # => "..."
"user/posts".deconstantize # => "user"
"user/posts".demodulize # => "posts"
2.2 Array 扩展#
# 分组
[1, 2, 3, 4, 5].in_groups_of(2)
# => [[1, 2], [3, 4], [5, nil]]
# 提取选项
args = [:a, :b, {c: 3}]
options = args.extract_options!
# options => {c: 3}
# args => [:a, :b]
# 转换为句子
['apple', 'banana', 'orange'].to_sentence
# => "apple, banana, and orange"
# 访问元素
[1, 2, 3, 4, 5].second # => 2
[1, 2, 3, 4, 5].third # => 3
[1, 2, 3, 4, 5].forty_two # => nil
# 拆分
[1, 2, 3, 4, 5, 6].split(3)
# => [[1, 2], [4, 5, 6]]
# 深度扁平化
[[1, [2, 3]], [4, 5]].deep_flatten
# => [1, 2, 3, 4, 5]
源码位置:activesupport/lib/active_support/core_ext/array/
2.3 Hash 扩展#
Hash 的扩展是 Rails 中最常用的:
# 深度合并
h1 = { a: { b: 1 } }
h2 = { a: { c: 2 } }
h1.deep_merge(h2)
# => { a: { b: 1, c: 2 } }
# 符号化键
{ 'a' => 1, 'b' => 2 }.symbolize_keys
# => { a: 1, b: 2 }
# 字符串化键
{ a: 1, b: 2 }.stringify_keys
# => { 'a' => 1, 'b' => 2 }
# 提取子集
{ a: 1, b: 2, c: 3 }.slice(:a, :b)
# => { a: 1, b: 2 }
# 转换为查询字符串
{ a: 1, b: 2 }.to_query
# => "a=1&b=2"
# 深度转换键
{ 'a' => { 'b' => 1 } }.deep_symbolize_keys
# => { a: { b: 1 } }
# 提取值
{ a: 1, b: 2 }.extract!(:a)
# => { a: 1 }
# 原 hash 变为 { b: 2 }
HashWithIndifferentAccess
这是 Rails 中最重要的 Hash 扩展之一:
hash = HashWithIndifferentAccess.new({ 'a' => 1, b: 2 })
hash[:a] # => 1
hash['a'] # => 1
hash[:b] # => 2
hash['b'] # => 2
用途:
params
对象就是HashWithIndifferentAccess
- 允许用字符串或符号访问键
- 在处理 JSON/YAML 时非常有用
源码位置:activesupport/lib/active_support/hash_with_indifferent_access.rb
2.4 Numeric 扩展#
# 时间单位
1.second # => 1
1.minute # => 60
1.hour # => 3600
1.day # => 86400
1.week # => 604800
# 时间计算
2.hours.ago
3.days.from_now
1.week.since(Time.now)
# 字节单位
1.kilobyte # => 1024
1.megabyte # => 1048576
1.gigabyte # => 1073741824
# 格式化
1234567.to_fs(:delimited) # => "1,234,567"
1234.56.to_fs(:currency) # => "$1,234.56"
0.5.to_fs(:percentage) # => "50.000%"
2.5 Object 扩展#
# blank? / present?
"".blank? # => true
nil.blank? # => true
[].blank? # => true
" ".blank? # => true
"hello".present? # => true
# presence - 返回对象本身或 nil
value = "".presence # => nil
value = "hello".presence # => "hello"
# try - 安全调用方法
user.try(:name) # 如果 user 为 nil,返回 nil
user.try(:posts).try(:count)
# tap - 执行代码块并返回自身
User.new.tap { |u| u.name = "John" }
# in? - 检查是否在集合中
"a".in?(['a', 'b', 'c']) # => true
5.in?(1..10) # => true
2.6 Module 扩展#
# delegate - 委托方法
class User
delegate :name, :email, to: :profile
delegate :count, to: :posts, prefix: true # posts_count
end
# mattr_accessor - 模块级别的属性
module MyModule
mattr_accessor :config
end
MyModule.config = { key: 'value' }
# alias_attribute - 别名属性
class User
alias_attribute :login, :email
end
3. Concern - 模块增强#
3.1 为什么需要 Concern?#
传统的模块混入有一些问题:
# 传统方式
module Foo
def self.included(base)
base.extend(ClassMethods)
base.class_eval do
# 类级别代码
end
end
module ClassMethods
# 类方法
end
end
问题:
- 代码冗长
- 模块依赖处理复杂
- 不够优雅
3.2 Concern 的解决方案#
# activesupport/lib/active_support/concern.rb
module Foo
extend ActiveSupport::Concern
included do
# 当被 include 时执行
scope :active, -> { where(active: true) }
end
class_methods do
# 类方法
def hello
"Hello from class"
end
end
# 实例方法
def world
"World from instance"
end
end
class User
include Foo
end
User.hello # => "Hello from class"
User.new.world # => "World from instance"
3.3 Concern 的实现原理#
让我们看看 ActiveSupport::Concern
的源码:
# activesupport/lib/active_support/concern.rb (简化版)
module ActiveSupport
module Concern
# 当模块 extend Concern 时执行
def self.extended(base)
base.instance_variable_set(:@_dependencies, [])
end
# 当模块被 include 时调用
def append_features(base)
if base.instance_variable_defined?(:@_dependencies)
# 如果 base 也是一个 Concern,记录依赖
base.instance_variable_get(:@_dependencies) << self
false
else
# base 是普通类,执行混入
return false if base < self
@_dependencies.each { |dep| base.include(dep) }
super
base.extend const_get(:ClassMethods) if const_defined?(:ClassMethods)
base.class_eval(&@_included_block) if instance_variable_defined?(:@_included_block)
end
end
# 定义 included 块
def included(base = nil, &block)
if base.nil?
@_included_block = block
else
super
end
end
# 定义类方法
def class_methods(&class_methods_module_definition)
mod = const_defined?(:ClassMethods, false) ?
const_get(:ClassMethods) :
const_set(:ClassMethods, Module.new)
mod.module_eval(&class_methods_module_definition)
end
end
end
关键机制:
- 依赖跟踪:通过
@_dependencies
实例变量追踪模块依赖 - 延迟执行:
included
块延迟到被混入类时执行 - 自动 extend:自动将
ClassMethods
模块 extend 到类
3.4 Concern 的依赖处理#
module Foo
extend ActiveSupport::Concern
included do
puts "Foo included"
end
end
module Bar
extend ActiveSupport::Concern
include Foo # Bar 依赖 Foo
included do
puts "Bar included"
end
end
class MyClass
include Bar # 只需 include Bar
end
# 输出:
# Foo included
# Bar included
Concern 自动处理了依赖:你不需要手动 include Foo
。
4. Callbacks - 回调系统#
4.1 回调的使用#
Rails 的回调系统无处不在:
class User < ApplicationRecord
before_save :normalize_email
after_create :send_welcome_email
around_update :log_changes
private
def normalize_email
self.email = email.downcase
end
def send_welcome_email
UserMailer.welcome(self).deliver_later
end
def log_changes
puts "Before update"
yield # 执行实际的更新
puts "After update"
end
end
4.2 回调的实现原理#
# activesupport/lib/active_support/callbacks.rb (简化版)
module ActiveSupport
module Callbacks
def self.included(base)
base.extend ClassMethods
end
module ClassMethods
# 定义回调
def define_callbacks(*names)
names.each do |name|
class_attribute "__#{name}_callbacks"
send("__#{name}_callbacks=", CallbackChain.new)
end
end
# 设置回调
def set_callback(name, kind, *args, &block)
callback = Callback.new(kind, args, block)
send("__#{name}_callbacks").append(callback)
end
# before, after, around 宏
[:before, :after, :around].each do |kind|
define_method(kind) do |*names, &block|
names.each do |name|
set_callback(name, kind, &block)
end
end
end
end
# 运行回调
def run_callbacks(name)
callbacks = self.class.send("__#{name}_callbacks")
callbacks.run(self) do
yield if block_given?
end
end
end
end
使用示例:
class Record
include ActiveSupport::Callbacks
define_callbacks :save
def save
run_callbacks :save do
# 实际的保存逻辑
puts "Saving..."
end
end
end
class User < Record
before_save { puts "Before save" }
after_save { puts "After save" }
end
user = User.new
user.save
# 输出:
# Before save
# Saving...
# After save
4.3 回调链和条件#
class User < ApplicationRecord
before_save :validate_email, if: :email_changed?
before_save :normalize_phone, unless: -> { phone.blank? }
after_save :send_notification, if: [:persisted?, :email_changed?]
# 终止回调链
before_save :check_permission
def check_permission
return false unless has_permission?
# 返回 false 或 throw :abort 都可以终止链
end
end
5. Autoload - 自动加载#
5.1 ActiveSupport::Autoload#
Rails 提供了增强的自动加载功能:
module MyLib
extend ActiveSupport::Autoload
autoload :User
autoload :Post
autoload :Comment
eager_autoload do
autoload :Admin
autoload :Dashboard
end
end
工作原理:
autoload :User
会在首次访问MyLib::User
时加载my_lib/user.rb
eager_autoload
块中的常量会在eager_load!
时立即加载
5.2 Zeitwerk 集成#
Rails 7+ 使用 Zeitwerk 作为默认自动加载器:
# config/application.rb
config.autoload_paths += %W(#{config.root}/lib)
# Rails 会自动加载:
# lib/my_service.rb → MyService
# lib/api/client.rb → Api::Client
# lib/api/v1/users.rb → Api::V1::Users
命名约定:
- 文件名使用
snake_case
- 常量使用
CamelCase
- 目录结构映射到模块嵌套
6. Inflector - 单复数转换#
6.1 单复数规则#
ActiveSupport::Inflector.pluralize("user") # => "users"
ActiveSupport::Inflector.pluralize("person") # => "people"
ActiveSupport::Inflector.singularize("users") # => "user"
ActiveSupport::Inflector.singularize("people") # => "person"
# 自定义规则
ActiveSupport::Inflector.inflections do |inflect|
inflect.irregular 'child', 'children'
inflect.uncountable 'fish'
end
6.2 Inflector 的应用#
Rails 大量使用 Inflector 实现约定:
# 模型名 → 表名
"User".tableize # => "users"
"BlogPost".tableize # => "blog_posts"
# 表名 → 模型名
"users".classify # => "User"
"blog_posts".classify # => "BlogPost"
# 控制器名 → 路径
"UsersController".underscore # => "users_controller"
"Api::V1::UsersController".underscore
# => "api/v1/users_controller"
7. Notifications - 事件系统#
7.1 发布订阅模式#
ActiveSupport::Notifications 实现了发布-订阅模式:
# 订阅事件
ActiveSupport::Notifications.subscribe("user.created") do |name, start, finish, id, payload|
user = payload[:user]
puts "User #{user.name} was created"
end
# 发布事件
ActiveSupport::Notifications.instrument("user.created", user: @user) do
# 实际的创建逻辑
end
7.2 Rails 内置事件#
Rails 发布了大量内置事件:
# SQL 查询
ActiveSupport::Notifications.subscribe("sql.active_record") do |*args|
event = ActiveSupport::Notifications::Event.new(*args)
puts "SQL: #{event.payload[:sql]}"
puts "Duration: #{event.duration}ms"
end
# 缓存操作
ActiveSupport::Notifications.subscribe("cache_read.active_support") do |*args|
event = ActiveSupport::Notifications::Event.new(*args)
puts "Cache read: #{event.payload[:key]}"
end
# 请求处理
ActiveSupport::Notifications.subscribe("process_action.action_controller") do |*args|
event = ActiveSupport::Notifications::Event.new(*args)
puts "Controller: #{event.payload[:controller]}"
puts "Action: #{event.payload[:action]}"
puts "Duration: #{event.duration}ms"
end
7.3 实现原理#
# activesupport/lib/active_support/notifications.rb (简化版)
module ActiveSupport
module Notifications
class << self
def subscribe(pattern, &block)
notifier.subscribe(pattern, &block)
end
def instrument(name, payload = {})
start = Time.now
begin
yield payload if block_given?
ensure
finish = Time.now
publish(name, start, finish, payload)
end
end
def publish(name, start, finish, payload)
notifier.publish(name, start, finish, payload)
end
end
end
end
8. Cache - 缓存系统#
8.1 缓存 Store#
ActiveSupport 提供了多种缓存后端:
# 内存缓存
Rails.cache = ActiveSupport::Cache::MemoryStore.new
# Memcached
Rails.cache = ActiveSupport::Cache::MemCacheStore.new("localhost:11211")
# Redis
Rails.cache = ActiveSupport::Cache::RedisCacheStore.new(url: "redis://localhost:6379/0")
# 文件缓存
Rails.cache = ActiveSupport::Cache::FileStore.new("/tmp/cache")
8.2 缓存 API#
# 读写
Rails.cache.write("key", "value")
Rails.cache.read("key") # => "value"
# fetch - 读取或生成
Rails.cache.fetch("expensive_calculation") do
# 只在缓存未命中时执行
perform_expensive_calculation
end
# 带过期时间
Rails.cache.fetch("key", expires_in: 1.hour) do
"value"
end
# 删除
Rails.cache.delete("key")
# 批量操作
Rails.cache.read_multi("key1", "key2")
Rails.cache.write_multi({"key1" => "value1", "key2" => "value2"})
8.3 缓存键的生成#
# ActiveSupport 提供了智能的缓存键生成
user = User.find(1)
Rails.cache.fetch(user) do
# 生成的键: users/1-20231201120000000000
expensive_user_operation(user)
end
# 键包含:
# - 模型名
# - ID
# - updated_at 时间戳(自动失效)
9. Time & TimeZone#
9.1 时区支持#
# 设置默认时区
Rails.application.config.time_zone = "Beijing"
# 当前时间(用户时区)
Time.current # => ActiveSupport::TimeWithZone
Time.zone.now # => ActiveSupport::TimeWithZone
# vs Ruby 原生时间
Time.now # => Time (服务器时区)
# 时区转换
time = Time.zone.parse("2024-01-01 12:00:00")
time.in_time_zone("Tokyo")
9.2 时间计算#
# 相对时间
2.days.ago
3.hours.from_now
1.week.since(Time.current)
# 时间边界
Time.current.beginning_of_day
Time.current.end_of_month
Time.current.beginning_of_year
# 时间查询
Time.current.monday?
Time.current.weekend?
Time.current.past?
Time.current.future?
10. 其他实用工具#
10.1 Benchmarkable#
User.benchmark("Creating users") do
100.times { User.create(name: "User") }
end
# => Creating users (123.4ms)
10.2 Delegation#
class Profile
attr_accessor :name, :bio
end
class User
attr_accessor :profile
delegate :name, :bio, to: :profile
delegate :size, to: :posts, prefix: true # posts_size
delegate :count, to: :comments, prefix: :total # total_count
end
10.3 Logger#
Rails.logger.debug "Debug message"
Rails.logger.info "Info message"
Rails.logger.warn "Warning message"
Rails.logger.error "Error message"
Rails.logger.fatal "Fatal message"
# 带块
Rails.logger.debug { "Expensive message: #{expensive_operation}" }
11. 源码阅读建议#
11.1 从简单的扩展开始#
推荐阅读顺序:
- String 扩展:activesupport/lib/active_support/core_ext/string/inflections.rb
- Numeric 扩展:activesupport/lib/active_support/core_ext/numeric/time.rb
- Concern:activesupport/lib/active_support/concern.rb
- Callbacks:activesupport/lib/active_support/callbacks.rb
11.2 测试是最好的文档#
# activesupport/test/core_ext/string_ext_test.rb
class StringInflectionsTest < ActiveSupport::TestCase
def test_pluralize
assert_equal "users", "user".pluralize
assert_equal "people", "person".pluralize
end
end
12. 实战练习#
练习 1:实现简化版 Concern#
尝试实现一个简化版的 Concern
,支持 included
和 class_methods
。
练习 2:自定义 Inflector 规则#
为中文拼音添加单复数转换规则。
练习 3:使用 Notifications#
在你的应用中添加自定义事件,统计关键操作的执行时间。
练习 4:实现自定义缓存 Store#
创建一个将缓存写入数据库的 Cache Store。
13. 本章总结#
通过本章学习,你应该掌握:
- 核心类扩展:String, Array, Hash, Numeric 等的扩展方法
- Concern:优雅的模块混入机制
- Callbacks:回调系统的实现和使用
- Autoload:自动加载机制
- Inflector:单复数和命名转换
- Notifications:事件发布订阅
- Cache:缓存抽象层
- Time & TimeZone:时区和时间处理
14. 下一步#
在下一篇 ActiveRecord 深入解析 中,我们将学习:
- ActiveRecord::Base 的设计
- 数据库连接管理
- 属性系统(Attributes)
- Schema 和迁移
上一篇:Rails 启动流程分析
返回:学习指南首页