rails4启动过程

    本篇博文会从使用rails server命令到应用启动完成的代码调用顺序,介绍rails server的启动过程,是对rails guide的一个简略翻译和一些博主的认识,翻译的不好还请各位见谅。看的时候最好找一份rails4的源码一起对照的来看。

    原文地址: http://guides.ruby-china.org/initialization.html

    当我们使用rails c(onsole)或者rails s(erver)时会启动一个rails的应用,rails s其实相当于在当前路径下执行了一段ruby脚本:

version = ">=0"
load Gem.bin_path('railties', 'rails', version)

如果在Rails console里输入上面的命令,你会看到railties/bin/rails被执行了。

在railties/bin/rails源码的最后一行可以看到

require “rails/cli”

require了railties/lib/rails/cli,继续跟进,在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

继续进入railties/lib/rails/app_rails_loader.rb

RUBY = Gem.ruby
EXECUTABLES = ['bin/rails', 'script/rails']
......

class << self
    ......
    def exec_app_rails
      original_cwd = Dir.pwd
      loop do
        if exe = find_executable
          contents = File.read(exe)
     
          if contents =~ /(APP|ENGINE)_PATH/
            exec RUBY, exe, *ARGV
            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 an executable.
        Dir.chdir('..')
      end
    end
       
    def find_executable
      EXECUTABLES.find { |exe| File.file?(exe) }
    end
end

可以通过上面的代码看出Rails::AppRailsLoader.exec_app_rails会在当前目录下找bin/rails或script/rails,找到了就会执行:

 exec Gem.ruby, bin/rails, *ARGV

即相当于

exec ruby bin/rails server

如果当前目录下没有bin/rails或script/rails就会一直递归向上直到找到bin(script)/rails,所以在rails项目的根目录以及子目录下的任何地方都可以使用rails server或rails console命令

bin/rails:

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

APP_PATH常量将会在之后的rails/commands里使用require_relative '../config/boot'是require了config/boot.rb文件,boot.rb负责加载启动Bundler

config/boot.rb

# Set up gems listed in the Gemfile.
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
 
require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])

在标准的的Rails应用中,Gemfile文件申明了所有应用依赖关系,ENV['BUNDLE_GEMFILE']存放Gemfile的文件地址,当Gemfile文件存在,回去require bundle/setup,用于Bundler配置加载Gemfile依赖关系的路径。

一个标准的Rails应用需要依赖几个gem包,如:

  • actionmailer

  • actionpack

  • actionview

  • activemodel

  • activerecord

  • activesupport

  • arel

  • builder

  • bundler

  • erubis

  • i18n

  • mail

  • mime-types

  • polyglot

  • rack

  • rack-cache

  • rack-mount

  • rack-test

  • rails

  • railties

  • rake

  • sqlite3

  • thor

  • treetop

  • tzinfo

require了config/boot.rb后回到bin/rails,最后还会require rails/commands

rails/commands.rb

主要用于扩展rails命令参数的别名:

rails/commands.rb

ARGV << '--help' if ARGV.empty?
 
aliases = {
  "g"  => "generate",
  "d"  => "destroy",
  "c"  => "console",
  "s"  => "server",
  "db" => "dbconsole",
  "r"  => "runner"
}
 
command = ARGV.shift
command = aliases[command] || command
 
require 'rails/commands/commands_tasks'
 
Rails::CommandsTasks.new(ARGV).run_command!(command)

可以看到当不传参数时,实际上与rails --help等效,rails s等效于rails server

rails/commands/command_tasks.rb

当输入了一个错误的rails命令,run_command方法负责抛出一个错误信息。如果命令是有效的,就会调用相同名称的方法

COMMAND_WHITELIST = %(plugin generate destroy console server dbconsole application runner new version help)
 
def run_command!(command)
  command = parse_command(command)
  if COMMAND_WHITELIST.include?(command)
    send(command)
  else
    write_error_message(command)
  end
end

def write_error_message(command)
  puts "Error: Command '#{command}' not recognized"
  if %x{rake #{command} --dry-run 2>&1 } && $?.success?
    puts "Did you mean: `$ rake #{command}` ?\n\n"
  end
  write_help_message 
  exit(1)
end
          
def parse_command(command) 
  case command 
  when '--version', '-v' 
    'version' 
  when '--help', '-h' 
     'help'
  else
     command 
  end
end

根据服务器命令,Rails会进一步运行以下代码,rails server将会调用server方法

def set_application_directory!
  Dir.chdir(File.expand_path('../../', APP_PATH)) unless File.exist?(File.expand_path("config.ru"))
end

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

 如上代码首先如果当前目录config.ru不存在,会将目录切换到rails的根目录(APP_PATH是之前bin/rails文件中获得),之后会require "rails/commands/server",然后创建Rails::Server的对象,调用tap方法启动server

rails/commands/server.rb

require 'fileutils'
require 'optparse'
require 'action_dispatch'
require 'rails'
 
module Rails
  class Server < ::Rack::Server
    ......
    def initaialize(*)
        super
        set_environment
    end
    ......
    def set_environment
        ENV["RAILS_ENV"] ||= options[:environment]
    end
    ......
end

fileutils和optparse是Ruby的标准库,提供文件炒作相关和解析元素的帮助方法。

action_dispatch,为actionpack/lib/action_dispatch.rb文件,ActionDispatch是Rails框架的路由组件,它增加了像路由,会话,一般中间件的功能。

 Rails::Server.new实际上就是创建一个Rack::Server的实例再为ENV["RAILS_ENV"]赋值。

Rack::Server负责为全部的以Rack为基础的引用提供通用的server接口

Rack:lib/rack/server.rb

def initialize(options = nil)
  @options = options
  @app = options[:app] if options && options[:app]
end

实际上,options参数是nil的,这个构造方法中什么都不会发生

再来看set_environment方法,在方法中并没有找到options这个局部变量,说明这是一个方法调用,其实他的方法定义在Rack::Server中,如下:

def options
  @options ||= parse_options(ARGV)
end

然后parse_options是像这样定义的:

def parse_options(args)
  options = default_options
 
  # Don't evaluate CGI ISINDEX parameters.
  # http://www.meb.uni-bonn.de/docs/cgi/cl.html
  args.clear if ENV.include?("REQUEST_METHOD")
 
  options.merge! opt_parser.parse!(args)
  options[:config] = ::File.expand_path(options[:config])
  ENV["RACK_ENV"] = options[:environment]
  options
end

default_options:

def default_options
  environment  = ENV['RACK_ENV'] || 'development'
  default_host = environment == 'development' ? 'localhost' : '0.0.0.0'
 
  {
    :environment => environment,
    :pid         => nil,
    :Port        => 9292,
    :Host        => default_host,
    :AccessLog   => [],
    :config      => "config.ru"
  }
end

现在ENV中还没有REQUEST_METHOD键,所以先跳过args.clear,下面一行options merge了定义在Rack::Server中方法返回的对象的parse!方法的返回值

opt_parser:

def opt_parser
  Options.new
end

Options这个类定义在Rack::Server中,但是在Rails::Server中被重写,重写后的parse!方法:

def parse!(args)
  args, options = args.dup, {}
 
  opt_parser = OptionParser.new do |opts|
    opts.banner = "Usage: rails server [mongrel, thin, etc] [options]"
    opts.on("-p", "--port=port", Integer,
            "Runs Rails on the specified port.", "Default: 3000") { |v| options[:Port] = v }
  ...

这个方法将为那些Rails可以使用来确定服务应该怎么运行的参数设置好键。到这里initialize结束,我们跳到rails/server中require APP_PATH的地方

当require APP_PATH被执行,config/application.rb将会被加载。这个文件在你的与应用中用于更具你的需要只有的给变配置

Rails::Server#start

当config/application被加载后,server.start被调用,这个方法定义如下:

def start
  print_boot_information
  trap(:INT) { exit }
  create_tmp_directories
  log_to_stdout if options[:log_stdout]
 
  super
  ...
end
 
private
 
  def print_boot_information
    ...
    puts "=> Run `rails server -h` for more startup options"
    ...
    puts "=> Ctrl-C to shutdown server" unless options[:daemonize]
  end
 
  def create_tmp_directories
    %w(cache pids sessions sockets).each do |dir_to_make|
      FileUtils.mkdir_p(File.join(Rails.root, 'tmp', dir_to_make))
    end
  end
 
  def log_to_stdout
    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))
  end

print_boot_information这里将会是Rails启动以来第一次答应信息,start方法为INT信号创建了一个trap,所以只要你按住CTRL-C,将会退出进程。通过后面的create_tmp_directories方法,将会创建tmp/cache,tmp/session和tmp/sockets目录。之后会调用wrapped_app方法,用于在创建和分配一个ActiveSupport::Logger实例之前。

super方法将会调用Rack::Server.start这个方法,如下:

def start &blk
  if options[:warn]
    $-w = true
  end
 
  if includes = options[:include]
    $LOAD_PATH.unshift(*includes)
  end
 
  if library = options[:require]
    require library
  end
 
  if options[:debug]
    $DEBUG = true
    require 'pp'
    p options[:server]
    pp wrapped_app
    pp app
  end
 
  check_pid! if options[:pid]
 
  # Touch the wrapped app, so that the config.ru is loaded before
  # daemonization (i.e. before chdir, etc).
  wrapped_app
 
  daemonize_app if options[:daemonize]
 
  write_pid if options[:pid]
 
  trap(:INT) do
    if server.respond_to?(:shutdown)
      server.shutdown
    else
      exit
    end
  end
 
  server.run wrapped_app, options, &blk
end

Rails应用有趣的部分就是在最后一行,server.run。这里我们再次遇到了wrapped_app方法。

wrapped_app:

def wrapped_app
    @wrpped_app ||= build_app app
end

def app
  @app ||= options[:builder] ? build_app_from_string : build_app_and_options_from_config
end
...
private
  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)
    self.options.merge! options
    app
  end
 
  def build_app_from_string
    Rack::Builder.new_from_string(self.options[:builder])
  end

option[: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 <%= app_const %>

Rack::Builder.parse_file方法在这里获取到config.ru文件的内容然后解析他,用了下面的代码:

app = new_from_string cfgfile, config
 
...
 
def self.new_from_string(builder_script, file="(rackup)")
  eval "Rack::Builder.new {\n" + builder_script + "\n}.to_app",
    TOPLEVEL_BINDING, file, 0
end

Rack::Builder的初始化方法会接受一个代码块同时在Rack::Builder的一个实例中执行它。这就是大多数Rails发生的初始化过程,当执行config.ru的代码时首先会require config/environment.rb

config/environment.rb

这个文件被config.ru(rails server)和Passenger require的共同文件。

这个文件最开始require 了config/application.rb

config/application.rb

这个文件require了config/boot.rb


你可能感兴趣的:(Ruby,Rails,启动过程)