跳过正文

第3篇:ActiveSupport 核心扩展

·1852 字·9 分钟
xieweicong
作者
xieweicong

学习目标:掌握 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

关键机制

  1. 依赖跟踪:通过 @_dependencies 实例变量追踪模块依赖
  2. 延迟执行included 块延迟到被混入类时执行
  3. 自动 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 从简单的扩展开始
#

推荐阅读顺序:

  1. String 扩展:activesupport/lib/active_support/core_ext/string/inflections.rb
  2. Numeric 扩展:activesupport/lib/active_support/core_ext/numeric/time.rb
  3. Concern:activesupport/lib/active_support/concern.rb
  4. 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,支持 includedclass_methods

练习 2:自定义 Inflector 规则
#

为中文拼音添加单复数转换规则。

练习 3:使用 Notifications
#

在你的应用中添加自定义事件,统计关键操作的执行时间。

练习 4:实现自定义缓存 Store
#

创建一个将缓存写入数据库的 Cache Store。

13. 本章总结
#

通过本章学习,你应该掌握:

  1. 核心类扩展:String, Array, Hash, Numeric 等的扩展方法
  2. Concern:优雅的模块混入机制
  3. Callbacks:回调系统的实现和使用
  4. Autoload:自动加载机制
  5. Inflector:单复数和命名转换
  6. Notifications:事件发布订阅
  7. Cache:缓存抽象层
  8. Time & TimeZone:时区和时间处理

14. 下一步
#

在下一篇 ActiveRecord 深入解析 中,我们将学习:

  • ActiveRecord::Base 的设计
  • 数据库连接管理
  • 属性系统(Attributes)
  • Schema 和迁移

上一篇Rails 启动流程分析

下一篇ActiveRecord 深入解析

返回学习指南首页