Ruby 有个内核方法 autoload,可以很方便的完成复杂的依赖加载,但是这个方法需要指定目录。遵守约定大于配置的 Rails,早已经定义好了 目录规范,此时就显得目录参数有点多余,于是 Rails 就在 ActiveSupport::Autoload 里对它进行了扩展。另外 eager_load 则是 ActiveSupport::Autoload 里进一步通过预加载提升性能的功能。

减少文件路径参数

Ruby 原生的 autoload 方法定义为:

autoload(module, filename) → nil

Rails 的扩展直接将第二个参数 filename 改成了可选参数,如果你没有传入,会通过当前环境和加载 module 的名称生成 filename,逻辑如下:

[name, @_under_path, const_name.to_s].compact.join("::").underscore

大部分都好理解,其中的 @_under_path 是对约定的增强,下面来解释。

特殊情况处理

子模块处理

class/module 的组织会用到文件夹,此时如果文件夹里的类多了,定义也会比较麻烦:

autoload :'Adapters::MySQL'
autoload :'Adapters::PostgreSQL'
autoload :'Adapters::SQLite'

这时候可以利用 autoload_under 进行改进,这个方法会在块作用域下配置上面提到的 @_under_path 值:

autoload_under :adapters do
  autoload :MySQL
  autoload :PostgreSQL
  autoload :SQLite
end

单文件处理

有些类,比如异常类虽然有多个,但是都很简单,不想分太多文件,比如都定义在一个 errors.rb 文件里面:

# errors.rb
class Error < StandardError; end
class ArgumentError < Error; end
class BadRequestError < Error; end

此时需要:

autoload :Error, 'errors'
autoload :ArgumentError, 'errors'
autoload :BadRequestError, 'errors'

这里可以通过 autoload_at 改进:

autoload_at :errors do
  autoload :Error
  autoload :ArgumentError
  autoload :BadRequestError
end

虽然节省不大,但是利用块将同类资源放到了一起,直观很多。

及时加载 eager_load

接下来是重点的 eager_load 功能。我们先思考一下,自动加载方便了我们编码,但有些常量是一定需要的,如果在线上可以提前加载出来这些常量,对性能肯定能提升。那有什么方法可以做到这个呢?答案就是 eager_load 功能。

基本使用

eager_load 功能用起来很简单,把你的库里肯定需要及时加载的模块放到 eager_autoload 块下即可:

# active_lib.rb
module ActiveLib
  autoload :Model

  eager_autoload do
    autoload :Cache
  end
end

上面就会特殊处理这个 Cache 的加载。原理的话,eager_autoload 方法实际上是在块执行期间,设置了 @_eager_autoload 变量,然后 autoload 会对这个变量做特殊处理,将这个变量下加载的常量存储到 @_eagerloaded_constants 数组中。

上面只是存储起来,所有记得要在你的库的最后执行加载,这个 eager_load! 方法就是加载 @_eagerloaded_constants 数组里所有的常量:

ActiveLib.eager_load!

如果你的库不止一个模块使用了 ActiveSupport::Autoload,记得要保证全部加载,通常这么做:

# active_lib/module_a.rb
module ActiveLib
  module ModuleA
    extend ActiveSupport::Autoload
    # ...
  end
end

# active_lib/module_b.rb
module ActiveLib
  module ModuleB
    extend ActiveSupport::Autoload
    # ...
  end
end

# active_lib.rb
module ActiveLib
  extend ActiveSupport::Autoload
  include ActiveLib::ModuleA
  include ActiveLib::ModuleB

  def self.eager_load!
    ActiveLib::ModuleA.eager_load!
    ActiveLib::ModuleB.eager_load!
  end
end

ActiveLib.eager_load!

配合 Rails 使用

如果你的库是为 Rails 应用服务的,例如一个 Rails Engine,那么你还可以“什么时机执行加载”交给 Rails,Rails 有个配置项 eager_load_namespaces 就是处理这个的,这个还能配合 config.eager_load 配置项,能在合适的时机帮你加载:

# active_lib.rb
module ActiveLib
  # ...
end

# 取消主动加载
# ActiveLib.eager_load!

使用 Railtie 让库支持 Rails 环境

# active_lib/railtie.rb
module ActiveLib
  class Railtie < Rails::Railtie
    config.eager_load_namespaces << ActiveLib
  end
end

参考

以上代码基于 Rails 7: