Skip to content
旧时的足迹
Go back

Rails Initialization - Briefing

编辑此页

rails 4-2-stable 源码为例

rails server 命令为例

当执行这个命令时,实际上是先加载了 rails 的 railties/bin/rails

git_path = File.expand_path('../../../.git', __FILE__)

if File.exist?(git_path)
  railties_path = File.expand_path('../../lib', __FILE__)
  $:.unshift(railties_path)
end
require "rails/cli"

前面先引入 railties/lib 文件夹,就不多说了,我们看最后一句它加载了 railties/lib/rails/cli

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'

如代码所示,它先加载 railties/lib/rails/ruby_version_check,然后执行方法 exec_app_rails

EXECUTABLES = ['bin/rails', 'script/rails']
  ...
  exe = find_executable
  exec RUBY, exe, *ARGV

  ...
  # 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 an executable.
  Dir.chdir('..')
  ...
def find_executable
  EXECUTABLES.find { |exe| File.file?(exe) }
end

可以看出 exec_app_rails 回去执行你项目目录下的 bin/rails 指令,如果当前文件夹下没有bin/rails文件,它会到父级目录去搜索,直到找到为止。所以在Rails应用程序目录下的任意位置,都可以执行rails的命令。

#!/usr/bin/env ruby
APP_PATH = File.expand_path('../../config/application', __FILE__)
require_relative '../config/boot'
require 'rails/commands'

它加载了项目目录下的 config/boot

ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)

require 'bundler/setup' # Set up gems listed in the Gemfile.

加载执行 bundler/setup 把Gemfile中的相关Gem都加载到load path中。

接下来回到 bin/rails ,继续加载 rails/commands ,这个文件主要用来解析并执行键入的指令

# ...
require 'rails/commands/commands_tasks'

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

解析命令参数的部分很简单,就不贴代码了。我们来看下执行的部分,首先加载了 rails/commands/commands_tasks 并执行 run_command!(command) 方法。

# rails/commands/commands_tasks

def server
  set_application_directory!
  require_command!("server")

  Rails::Server.new.tap do |server|
    # We need to require application after the server sets environment,
    # otherwise the --environment option given to the server won't propagate.
    require APP_PATH
    Dir.chdir(Rails.application.root)
    server.start
  end
end

def require_command!(command)
  require "rails/commands/#{command}"
end

# Change to the application's path if there is no config.ru file in current directory.
# This allows us to run `rails server` from other directories, but still get
# the main config.ru and properly set the tmp directory.
def set_application_directory!
  Dir.chdir(File.expand_path('../../', APP_PATH)) unless File.exist?(File.expand_path("config.ru"))
end

它会找到项目根目录,然后根据命令去加载 rails/commands/ 目录下的相应文件。 接下来创建了一个 Rails::Server 实例

def initialize(*)
  super
  set_environment
end

初始化时会去执行父类的初始化方法

# rack/lib/rack/server

def initialize(options = nil)
  @ignore_options = []

  if options
    @use_default_options = false
    @options = options
    @app = options[:app] if options[:app]
  else
    argv = defined?(SPEC_ARGV) ? SPEC_ARGV : ARGV
    @use_default_options = true
    @options = parse_options(argv)
  end
end

这里看似没有做什么事情,实际上这里的options设置配置了很多配置选项(如environment、Port、Host、config等等),以便给Rails决定如何运行服务提供支持。

当初始化执行完成之后会去加载 APP_PATH ,还记得之前 bin/rails 文件的内容么,在那里面定义了 APP_PATH 指向项目目录下的config/application文件。

require 不会重复加载已加载过的文件 你可以根据需求对该文件进行配置

require File.expand_path('../boot', __FILE__)

require 'rails/all'

# ...

第一句之前已经加载过了,我们直接来看第二句,它加载了 rails/all

require "rails"

%w(
  active_record
  action_controller
  action_view
  action_mailer
  active_job
  rails/test_unit
  sprockets
).each do |framework|
  begin
    require "#{framework}/railtie"
  rescue LoadError
  end
end

从代码中可以看出它一一加载了Rails框架相关的各个内容,每个必要组件都会在这一步被加载

然后我们回到实例化 server 的部分,加载完毕 config/application 之后,将会执行 server.start

# server.rb
def start
  print_boot_information
  trap(:INT) { exit }
  create_tmp_directories
  log_to_stdout if options[:log_stdout]

  super
ensure
  # The '-h' option calls exit before @options is set.
  # If we call 'options' with it unset, we get double help banners.
  puts 'Exiting' unless @options && options[:daemonize]
end

private

def print_boot_information
  url = "#{options[:SSLEnable] ? 'https' : 'http'}://#{options[:Host]}:#{options[:Port]}"
  puts "=> Booting #{ActiveSupport::Inflector.demodulize(server)}"
  puts "=> Rails #{Rails.version} application starting in #{Rails.env} on #{url}"
  puts "=> Run `rails server -h` for more startup options"

  puts "=> Ctrl-C to shutdown server" unless options[:daemonize]
end

执行到这里,将会进行第一次控制台输出,可以看到这里还创建了一个INT中断,然后再创建tmp/cache,tmp/pids, tmp/sessions和tmp/sockets一系列目录最后去执行父类中的start方法,在start方法中我们主要来看这个方法 wrapped_app

# rack/lib/rack/server

def wrapped_app
  @wrapped_app ||= build_app app
end

def build_app(app)
  middleware[options[:environment]].reverse_each do |middleware|
    middleware = middleware.call(self) if middleware.respond_to?(:call)
    next unless middleware
    klass, *args = middleware
    app = klass.new(app, *args)
  end
  app
end

def app
  @app ||= options[:builder] ? build_app_from_string : build_app_and_options_from_config
end

def build_app_and_options_from_config
  if !::File.exist? options[:config]
    abort "configuration #{options[:config]} not found"
  end

  app, options = Rack::Builder.parse_file(self.options[:config], opt_parser)
  @options.merge!(options) { |key, old, new| old }
  app
end

def build_app_from_string
  Rack::Builder.new_from_string(self.options[:builder])
end

可以看到app的创建会先去取options[:config],options[:config]指向项目下的config.ru文件,我们先来看一下这个文件的样子

# config.ru

# This file is used by Rack-based servers to start the application.

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

它加载了项目目录下的 config/environment

# Load the Rails application.
require File.expand_path('../application', __FILE__)

# Initialize the Rails application.
Rails.application.initialize!

可以看到这里也会加载项目的 config/application

接下来将会执行 Rails.application.initialize! 这个方法会对所有的组件进行初始化

require 不会重复加载已加载过的文件

def initialize!(group=:default) #:nodoc:
  raise "Application has been already initialized." if @initialized
  run_initializers(group, self)
  @initialized = true
  self
end

它会调起 railties/lib/rails/initializable.rb 的 run_initializers方法

# railties/lib/rails/initializable.rb

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

def initializers
  @initializers ||= self.class.initializers_for(self)
end

module ClassMethods
  def initializers
    @initializers ||= Collection.new
  end

  def initializers_chain
    initializers = Collection.new
    ancestors.reverse_each do |klass|
      next unless klass.respond_to?(:initializers)
      initializers = initializers + klass.initializers
    end
    initializers
  end

  def initializers_for(binding)
    Collection.new(initializers_chain.map { |i| i.bind(binding) })
  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

在方法 initializers_chain 它去遍历所有祖先链并把拿到它们的initializers方法,之后依次执行。这些执行完成后回到 rack/server.rb 的 build_app 方法,这个方法将会依照环境去加载rack所有的middleware, 最后一步 server.run 将会根据你的server类型去启动相应的服务。

到此,整个启动流程就结束了。


编辑此页
Share this post on:

Previous Post
Quick-Sort
Next Post
Greedy-And-Annealing