Rails::Railtieを流し読み
Rails::Railtieを流し読み
動機
request_storeを読んでいる途中で、結局途中でrailtiesにブチあたり、この辺ちゃんと読んだことないのが気持ち悪いな、という気持ちでコードリーディングを始めてみました。
version
2.3.0
ドキュメントを読んでみる
個人的に重要だと思った点をピックアップしてみます。
core of the Rails framework and provides several hooks to extend Rails and/or modify the initialization process.
rails frameworkのコアになっており、初期化プロセスを拡張するための仕組みを提供している。
Every major component of Rails (Action Mailer, Action Controller, Active Record, etc.) implements a railtie. Each of them is responsible for their own initialization.
主要なcomponentはrailtieを実装している。なるほど、Gemを読んでいく上ではこの辺の仕組みをわかってないとしんどそうです。
To add an initialization step to the Rails boot process from your railtie, just define the initialization code with the initializer macro: If specified, the block can also receive the application object, in case you need to access some application-specific configuration, like middleware:
自作のrailtieをRails の初期化プロセスに組み込むには、initialization codeを書くだけです、と。blockはapplication objectを受け取ることができて、application固有の設定にアクセスできます。
request_storeのソースコードをみてみると、こうなっております。
module RequestStore class Railtie < ::Rails::Railtie initializer "request_store.insert_middleware" do |app| if ActionDispatch.const_defined? :RequestId app.config.middleware.insert_after ActionDispatch::RequestId, RequestStore::Middleware else app.config.middleware.insert_after Rack::MethodOverride, RequestStore::Middleware end if ActiveSupport.const_defined?(:Reloader) && ActiveSupport::Reloader.respond_to?(:to_complete) ActiveSupport::Reloader.to_complete do RequestStore.clear! end elsif ActionDispatch.const_defined?(:Reloader) && ActionDispatch::Reloader.respond_to?(:to_cleanup) ActionDispatch::Reloader.to_cleanup do RequestStore.clear! end end end end end
bulletはこんな感じです。
if defined? Rails::Railtie class BulletRailtie < Rails::Railtie initializer 'bullet.configure_rails_initialization' do |app| app.middleware.use Bullet::Rack end end end
ざっくり眺める
Rails::Railtieはabstract class
直接インスタンス化はできないことがわかります。
rails/railtie.rb at master · rails/rails · GitHub
module Rails class Railtie # ... def initialize #:nodoc: if self.class.abstract_railtie? raise "#{self.class.name} is abstract, you cannot instantiate it directly." end end end end
主要なメソッドは、rake_tasks
, console
, runner
, generators
かな。。
ここで格納されたblockが、後に #each_registered_block(type, &block)
の中で使用されるようです。
module Rails class Railtie # ... def rake_tasks(&blk) register_block_for(:rake_tasks, &blk) end def console(&blk) register_block_for(:load_console, &blk) end def runner(&blk) register_block_for(:runner, &blk) end def generators(&blk) register_block_for(:generators, &blk) end private # receives an instance variable identifier, set the variable value if is # blank and append given block to value, which will be used later in # `#each_registered_block(type, &block)` def register_block_for(type, &blk) var_name = "@#{type}" blocks = instance_variable_defined?(var_name) ? instance_variable_get(var_name) : instance_variable_set(var_name, []) blocks << blk if blk blocks end
処理の流れを追ってみる
エントリポイント
lib/rails/initializable.rb
initializerメソッドはRails::Initializableモジュールに定義されており、ここでInitializerインスタンスが initializersに格納されています。
module Rails module Initializable module ClassMethods ... def initializers @initializers ||= Collection.new end .. def initializer(name, opts = {}, &blk) raise ArgumentError, "A block must be passed when defining an initializer" unless blk opts[:after] ||= initializers.last.name unless initializers.empty? || initializers.find { |i| i.name == opts[:before] } initializers << Initializer.new(name, nil, opts, &blk) end end end end
lib/rails/initializable.rb
Rails::Applications#initialize!が呼ばれると、 initializersはRails::Initializable#run_initializersの中でrunされていきます。 tsortが何なのかは、Ruby's TSort explained を読めばわかりそうなのですが、いったんここでは先に進みます。
config/environment.rb
# Load the Rails application. require_relative 'application' # Initialize the Rails application. Rails.application.initialize!
lib/rails/application.rb
module Rails class Application < Engine ... # Initialize the application passing the given group. By default, the # group is :default def initialize!(group = :default) #:nodoc: raise "Application has been already initialized." if @initialized run_initializers(group, self) @initialized = true self end end end
lib/rails/initializable.rb
module Rails module Initializable ... def run_initializers(group = :default, *args) return if instance_variable_defined?(:@ran) initializers.tsort_each do |initializer| initializer.run(*args) if initializer.belongs_to?(group) end @ran = true end ...
initializersはRails::Application#initializersとRails::Initializable#initializers があるが、モジュールのメソッドよりクラスで定義されているメソッドが優先され、Rails::Application#initializers が実行されます(実際に動かして確認しました)。
lib/rails/application.rb
module Rails class Application < Engine ... def initializers #:nodoc: Bootstrap.initializers_for(self) + railties_initializers(super) + Finisher.initializers_for(self) end end end
Bootstrap
initializersは下記の通り、applicationの準備をするものが返されます。
[:load_environment_hook, :load_active_support, :set_eager_load, :initialize_logger, :initialize_cache, :initialize_dependency_mechanism, :bootstrap_hook, :set_secrets_root]
railties
initializersは Rails::Application 自身に定義されたものが返されます。
(byebug) initializers.map(&:name)
[:set_load_path, :set_autoload_paths, :add_routing_paths, :add_locales, :add_view_paths, :load_environment_config, :prepend_helpers_path, :load_config_initializers, :engines_blank_point, :append_assets_path]
#railties_initializers
の中身を少し掘ってみていきます。#ordered_railties
は何をしているのでしょうか?
def railties_initializers(current) #:nodoc: initializers = [] ordered_railties.reverse.flatten.each do |r| if r == self initializers += current else initializers += r.initializers end end initializers end # Returns the ordered railties for this application considering railties_order. def ordered_railties #:nodoc: @ordered_railties ||= begin order = config.railties_order.map do |railtie| if railtie == :main_app self elsif railtie.respond_to?(:instance) railtie.instance else railtie end end all = (railties - order) all.push(self) unless (all + order).include?(self) order.push(:all) unless order.include?(:all) index = order.index(:all) order[index] = all order end end
lib/rails/initializable.rb
ここで、定義されたブロックが実行されてますね。
class Initializer def run(*args) @context.instance_exec(*args, &block) end end
次に読みたい
wicked_pdf, request_store, bullet, wkhtmltopdf, sidekiq, active_support, rbenv, webrick, draper, jbuilder, rails/active_job, rails/spring, capistrano, omniauth, committee etc..
参考
The Rails Initialization Process — Ruby on Rails Guides
Configure your gem the Rails way with Railtie
ブログに技術書の内容を丸写しする問題点と、オリジナルなコンテンツを書くためのアイデア - give IT a try