Rails 加载过程

Posted on April 7, 2014

rails启动过程

  1. 当前ruby版本的默认gemset下bin中rails

     % which rails
     /Users/zhonghua/.rvm/gems/ruby-2.0.0-p247/bin/rails
    

    该文件中主要

     require 'rubygems'
     version = ">= 0"
    
     if ARGV.first #这段原因不明
       str = ARGV.first
       str = str.dup.force_encoding("BINARY") if str.respond_to? :force_encoding
       if str =~ /\A_(.*)_\z/
         version = $1
         ARGV.shift
       end
     end
    
     #这句话把/Users/zhonghua/.rvm/gems/ruby-2.0.0-p247/gems/railties-4.0.0/bin/rails加入了$: 所以后面可以require 'rails/...'
     gem 'railties', version #TODO
     load Gem.bin_path('railties', 'rails', version) #/Users/zhonghua/.rvm/gems/ruby-2.0.0-p247/gems/railties-4.0.0/bin/rails
    
  2. 步骤一种最后load的railties中bin中rails, 作用主要加载rails/cli

     git_path = File.join(File.expand_path('../../..', __FILE__), '.git')
    
     if File.exists?(git_path) #原因不明
       railties_path = File.expand_path('../../lib', __FILE__)
       $:.unshift(railties_path)
     end
     require "rails/cli"
    
  3. rails/cli 主要执行了Rails::AppRailsLoader.exec_app_rails

     require 'rbconfig' #TODO
     require 'rails/app_rails_loader'
    
     # If we are inside a Rails application this method performs an exec and thus
     # the rest of this script is not run.
     Rails::AppRailsLoader.exec_app_rails
    
     require 'rails/ruby_version_check'
     Signal.trap("INT") { puts; exit(1) }
    
     if ARGV.first == 'plugin'
       ARGV.shift
       require 'rails/commands/plugin_new'
     else
       require 'rails/commands/application'
     end
    
  4. rails/app_rails_loaderRails::AppRailsLoader.exec_app_rails

     def self.exec_app_rails
       original_cwd = Dir.pwd
    
       loop do #主要递归找到rails的根目录
         if exe = find_executable
           contents = File.read(exe)
    
           if contents =~ /(APP|ENGINE)_PATH/ #因为rails项目里地bin/rails 中有APP_PATH,所以流程到这里
             exec RUBY, exe, *ARGV #TODO exec的注释
             break # non reachable, hack to be able to stub exec in the test suite
           elsif exe.end_with?('bin/rails') && contents.include?('This file was generated by Bundler')
             $stderr.puts(BUNDLER_WARNING)
             Object.const_set(:APP_PATH, File.expand_path('config/application', Dir.pwd))
             require File.expand_path('../boot', APP_PATH)
             require 'rails/commands'
             break
           end
         end
    
         # If we exhaust the search there is no executable, this could be a
         # call to generate a new application, so restore the original cwd.
         Dir.chdir(original_cwd) and return if Pathname.new(Dir.pwd).root?
    
         # Otherwise keep moving upwards in search of a executable.
         Dir.chdir('..')
       end
     end
    
  5. 上一步中exec RUBY, exe, *ARGV 将执行rails项目目录下的bin/rails

     APP_PATH = File.expand_path('../../config/application',  __FILE__) #这个是项目的启动文件,之后的命令如果需要启动目录就`require APP_PATH`
     require_relative '../config/boot'
     require 'rails/commands'
    
  6. ../config/boot rails项目的boot文件主要是加载bundle

     ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
     require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
    
  7. 步骤5中require 'rails/commands'

     aliases = {
       "g"  => "generate",
       "d"  => "destroy",
       "c"  => "console",
       "s"  => "server",
       "db" => "dbconsole",
       "r"  => "runner"
     }
    
     command = ARGV.shift # 通过判断ARGV的第一个元素,走不同的when case
     command = aliases[command] || command
    

    对于’generate’, ‘destroy’, ‘console’, ‘server’ 都需要加载当前项目,所以都有require APP_PATH

  8. (rails g, rails d, rails c, rails s)require APP_PATH 加载项目目录下的config/application.rb

     require File.expand_path('../boot', __FILE__) #这里我得到的时false 因为之前加载过了
     require 'rails/all' #这里可以根据需要加载所需gem
    
     # Require the gems listed in Gemfile, including any gems
     # you've limited to :test, :development, or :production.
     Bundler.require(:default, Rails.env) #加载当前环境的gem
     #Rails.env在railtie/lib/rails.rb中
    
     module R4test
       class Application < Rails::Application
         # Settings in config/environments/* take precedence over those specified here.
         # Application configuration should go into files in config/initializers    # -- all .rb files in that directory are automatically loaded.
    
         # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
         # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
         # config.time_zone = 'Central Time (US & Canada)'
    
         # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
         # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
         # config.i18n.default_locale = :de
    
    
        #可以注册回调 config.before_configuration {...}
       end
     end
    
  9. require 'rails/all' 加载railties-4.0.0/lib/rails/all.rb

     require "rails" #这里才加载了railties-4.0.0/lib/rails文件
    
     %w(
       active_record
       action_controller
       action_mailer
       rails/test_unit #如果不用test unit等组件,可以在APP_PATH中改写require 'rails/all'
       sprockets
     ).each do |framework|
       begin
         require "#{framework}/railtie" #每个gem的启动文件都是其lib中gem目录中的railtie, 这个文件在各自加载gem同名文件
       rescue LoadError
       end
     end
    
  10. ...class Application < Rails::Application

    class << self
      def inherited(base)
        raise "You cannot have more than one Rails::Application" if Rails.application
        super
        Rails.application = base.instance
        Rails.application.add_lib_to_load_path! #把项目lib加人load path 所以可以直接require lib中文件
        ActiveSupport.run_load_hooks(:before_configuration, base.instance)
      end
    end
    
  11. ActiveSupport.run_load_hooks(:before_configuration, base.instance) 运行config.before_configuration回调

    这个回调要事先注册,调用config.before_configuration 进行注册

    Rails::Railtie::Configuration#before_configuration 定义了这个方法:

    def before_configuration(&block)
      ActiveSupport.on_load(:before_configuration, yield: true, &block)
    end
    
  12. (rails g, rials d, rails c)Rails.application.require_environment! 执行初始化initializers

    environment = paths["config/environment"].existent.first
    require environment if environment
    

    实际上是加载rails项目中 ` config/environment.rb`

    # Load the Rails application.
    require File.expand_path('../application', __FILE__)
    
    # Initialize the Rails application.
    R4test::Application.initialize!
    
  13. initialize! 最后调用 run_initializers

    return if instance_variable_defined?(:@ran)
    initializers.tsort_each do |initializer|
      initializer.run(*args) if initializer.belongs_to?(group)
    end
    @ran = true
    

主要命令的启动

‘rails s’

    Dir.chdir(File.expand_path('../../', APP_PATH)) unless File.exists?(File.expand_path("config.ru"))

    require 'rails/commands/server'
    Rails::Server.new.tap do |server|
      require APP_PATH                   # 步骤8
      Dir.chdir(Rails.application.root)
      server.start
    end

server.startrailties-4.0.0/lib/rails/commands/server.rb

    def start
      ..........
        wrapped_app # touch the app so the logger is set up

        console = ActiveSupport::Logger.new($stdout)
        console.formatter = Rails.logger.formatter
        console.level = Rails.logger.level

        Rails.logger.extend(ActiveSupport::Logger.broadcast(console))

      super
      .............
    end

wrapped_apprack-1.5.2/lib/rack/server.rb 提供

    def wrapped_app
      @wrapped_app ||= build_app app
    end

app 创于 app, options = Rack::Builder.parse_file(self.options[:config], opt_parser)

self.options[:config] 是项目中 config.ru 内容

    require ::File.expand_path('../config/environment',  __FILE__) #步骤12
    run Rails.application

‘generate’ ‘destroy’

    require 'rails/generators'

    require APP_PATH
    Rails.application.require_environment!
    Rails.application.load_generators
    require "rails/commands/#{command}"

其他笔记

  1. Rails.env

     def env
       @_env ||= ActiveSupport::StringInquirer.new(ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development")
     end
    

    关于 ActiveSupport::StringInquirer, 该字符串包装器可以让Rails.env == 'production'使用Rails.env.production?调用

  2. railties/lib/rails/application.rb

     class << self
       #被继承是会调用 在项目中config/application.rb里, 初始化Rails.application = (ProjectName::Application)当前项目的实例
       def inherited(base)
         raise "You cannot have more than one Rails::Application" if Rails.application
         super
         Rails.application = base.instance #base就是项目中config/application.rb中定义的Application
         Rails.application.add_lib_to_load_path!
         ActiveSupport.run_load_hooks(:before_configuration, base.instance)
       end
     end
    
  3. 关于Rails.application

  • Rails.application 是项目::Application类的实例: Rails.application == R4test::Application.instance 为true

    Rails.application.class.ancestors
    => [R4test::Application, Rails::Railtie::Configurable, Rails::Application, Rails::Engine, Rails::Railtie, Rails::Initializable, Object, PP::ObjectMixin, ActiveSupport::Dependencies::Loadable, JSON::Ext::Generator::GeneratorMethods::Object, Kernel, BasicObject]
    
  • Rails.application.config @config ||= Application::Configuration.new(find_root_with_flag("config.ru", Dir.pwd))

  • Rails.application.config.root 指向项目目录

  • default_middleware_stack 定义了默认中间件:

    def default_middleware_stack #:nodoc:
      ActionDispatch::MiddlewareStack.new.tap do |middleware|
        app = self
    
        if rack_cache = load_rack_cache
          require "action_dispatch/http/rack_cache"
          middleware.use ::Rack::Cache, rack_cache
        end
    
        if config.force_ssl
          middleware.use ::ActionDispatch::SSL, config.ssl_options
        end
    
        if config.action_dispatch.x_sendfile_header.present?
          middleware.use ::Rack::Sendfile, config.action_dispatch.x_sendfile_header
        end
    
        if config.serve_static_assets
          middleware.use ::ActionDispatch::Static, paths["public"].first, config.static_cache_control
        end
    
        middleware.use ::Rack::Lock unless allow_concurrency?
        middleware.use ::Rack::Runtime
        middleware.use ::Rack::MethodOverride
        middleware.use ::ActionDispatch::RequestId
    
        # Must come after Rack::MethodOverride to properly log overridden methods
        middleware.use ::Rails::Rack::Logger, config.log_tags
        middleware.use ::ActionDispatch::ShowExceptions, show_exceptions_app
        middleware.use ::ActionDispatch::DebugExceptions, app
        middleware.use ::ActionDispatch::RemoteIp, config.action_dispatch.ip_spoofing_check, config.action_dispatch.trusted_proxies
    
        unless config.cache_classes
          middleware.use ::ActionDispatch::Reloader, lambda { app.reload_dependencies? }
        end
    
        middleware.use ::ActionDispatch::Callbacks
        middleware.use ::ActionDispatch::Cookies
    
        if config.session_store
          if config.force_ssl && !config.session_options.key?(:secure)
            config.session_options[:secure] = true
          end
          middleware.use config.session_store, config.session_options
          middleware.use ::ActionDispatch::Flash
        end
    
        middleware.use ::ActionDispatch::ParamsParser
        middleware.use ::Rack::Head
        middleware.use ::Rack::ConditionalGet
        middleware.use ::Rack::ETag, "no-cache"
      end
    end
    

    Engine中也有同名实例方法,但是是个空的ActionDispatch::MiddlewareStack.new


今天重新追踪了一下Rails 4.1.8 主要文件的加载过程:

  • /home/zhonghua/.rvm/gems/[email protected]/bin/rails

    load Gem.bin_path('railties', 'rails', version)

  • /home/zhonghua/.rvm/gems/[email protected]/gems/railties-4.1.8/bin/rails

    require "rails/cli"

  • /home/zhonghua/.rvm/gems/[email protected]/gems/railties-4.1.8/lib/rails/cli.rb

    Rails::AppRailsLoader.exec_app_rails

  • /home/zhonghua/.rvm/gems/[email protected]/gems/railties-4.1.8/lib/rails/app_rails_loader.rb

    exec RUBY, exe, *ARGV

  • 项目文件 bin/rails

    require_relative '../config/boot'

    require 'rails/commands'

  • 项目文件 config/boot

    加载bundler require 'bundler/setup'

  • /home/zhonghua/.rvm/gems/[email protected]/gems/railties-4.1.8/lib/rails/commands

    require 'rails/commands/commands_tasks'

    Rails::CommandsTasks.new(ARGV).run_command!(command)

  • /home/zhonghua/.rvm/gems/[email protected]/gems/railties-4.1.8/lib/rails/commands/commands_tasks

    require APP_PATH 项目下config/application.rb

    server.start => 项目下 config.ru

  • config/application.rb

  • config,ru

    require ::File.expand_path('../config/environment', __FILE__) run Rails.application

  • 项目下config/environment

    Rails.application.initialize!


疑问

  1. 诸如active_record等gem的源码里,为什么没有gem_spec 等文件