本篇博文会从使用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
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