前言
Rails 3中出现了很多新的名词,如Railtie,engine,application等,这些都和Rails的启动过程及gem的开发密切相关,由于好奇,想尝试分析整个Rails 3的启动过程,给自己一个交代。
从我们在工程目录下输入:
rails s
开始。首先看看这个命令中的rails是什么:
$ which rails
/home/tomwang/.rvm/gems/ruby-1.9.3-p194@rails323/bin/rails
这个启动脚本如下:
#!/usr/bin/env ruby
#
# This file was generated by RubyGems.
#
# The application 'railties' is installed as part of a gem, and
# this file is here to facilitate running it.
#
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
gem 'railties', version
load Gem.bin_path('railties', 'rails', version)
这个文件主要做了两件事:
- 导入railties这个gem,这是rails 3中的核心概念,后面还会对它进行更详尽的分析
- load railties这个gem下的另一个rails脚本:/home/tomwang/.rvm/gems/ruby-1.9.3-p194@rails323/gems/railties-3.2.3/bin/rails
#!/usr/bin/env ruby
if File.exists?(File.join(File.expand_path('../../..', __FILE__), '.git'))
railties_path = File.expand_path('../../lib', __FILE__)
$:.unshift(railties_path)
end
require "rails/cli"
这个rails主要是require了"railties-3.2.3/lib/rails/cli.rb "
这个文件,它的内容如下:
require 'rbconfig'
require 'rails/script_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::ScriptRailsLoader.exec_script_rails!
它最后一行执行的函数如下:
module Rails
module ScriptRailsLoader
RUBY = File.join(*RbConfig::CONFIG.values_at("bindir", "ruby_install_name")) + RbConfig::CONFIG["EXEEXT"]
SCRIPT_RAILS = File.join('script', 'rails')
def self.exec_script_rails!
cwd = Dir.pwd
return unless in_rails_application? || in_rails_application_subdirectory?
exec RUBY, SCRIPT_RAILS, *ARGV if in_rails_application? # 终于执行到了我们工程目录下的script/rails
Dir.chdir("..") do
# Recurse in a chdir block: if the search fails we want to be sure
# the application is generated in the original working directory.
exec_script_rails! unless cwd == Dir.pwd
end
rescue SystemCallError
# could not chdir, no problem just return
end
def self.in_rails_application?
File.exists?(SCRIPT_RAILS)
end
def self.in_rails_application_subdirectory?(path = Pathname.new(Dir.pwd))
File.exists?(File.join(path, SCRIPT_RAILS)) || !path.root? && in_rails_application_subdirectory?(path.parent)
end
end
end
由上面注释行可见,最后我们自己工程目录下的script/rails被执行了,它的内容如下:#!/usr/bin/env ruby
# This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
APP_PATH = File.expand_path('../../config/application', __FILE__)
require File.expand_path('../../config/boot', __FILE__)
require 'rails/commands'
它定义APP_PATH,即我们的项目文件:config/application.rb,并require了config/boot,boot文件主要是做了bundle的初始化。然后,rails/commands(railties-3.2.3/lib/rails/commands.rb)文件被load进来,由于我们传入的command参数为s,也就是server,所以我们只展示对应的代码:when 'server'
# Change to the application's path if there is no config.ru file in current dir.
# This allows us to run script/rails server from other directories, but still get
# the main config.ru and properly set the tmp directory.
Dir.chdir(File.expand_path('../../', APP_PATH)) unless File.exists?(File.expand_path("config.ru"))
require 'rails/commands/server'
Rails::Server.new.tap { |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
}
server.start函数如下,它打印出了我们熟悉的控制台启动信息:def start
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 "=> Call with -d to detach" unless options[:daemonize]
trap(:INT) { exit }
puts "=> Ctrl-C to shutdown server" unless options[:daemonize]
#Create required tmp directories if not found
%w(cache pids sessions sockets).each do |dir_to_make|
FileUtils.mkdir_p(Rails.root.join('tmp', dir_to_make))
end
puts 'server start ---'
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
最后的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
# 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
wrapped_app方法最终调用Rack::Server#app:def app
@app ||= begin
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
end
由于我们没有指定:config命令行参数,系统将取默认参数,及工程目录下的:config.ru,这个文件和rack(后面有详细论述)有关。这行代码将导入config.ru,其内容如下:require ::File.expand_path('../config/environment', __FILE__)
run Tao800Fire::Application
它将再导入config/environment.rb文件,在其中,我们的程序将完成初始化:# Load the rails application
require File.expand_path('../application', __FILE__)
# Initialize the rails application
Tao800Fire::Application.initialize!
初始化完成之后,我们回到
Rack::Server#start的最后一行:server.run wrapped_app, options, &blk
由于我们没有指定:server命令行参数,rack为我们选定默认的server:def server
@_server ||= Rack::Handler.get(options[:server]) || Rack::Handler.default(options)
end
def self.default(options = {})
# Guess.
if ENV.include?("PHP_FCGI_CHILDREN")
# We already speak FastCGI
options.delete :File
options.delete :Port
Rack::Handler::FastCGI
elsif ENV.include?("REQUEST_METHOD")
Rack::Handler::CGI
else
begin
Rack::Handler::Thin
rescue LoadError
Rack::Handler::WEBrick
end
end
end
一般来说是WEBrick,除非你安装了其他application server。然后WEBrick启动,打出了如下信息:[2013-03-11 17:30:11] INFO WEBrick 1.3.1
[2013-03-11 17:30:11] INFO ruby 1.9.3 (2012-04-20) [x86_64-linux]
[2013-03-11 17:30:11] INFO WEBrick::HTTPServer#start: pid=28371 port=3000
至此,整个启动过程就完成了,后面的篇章将尝试详细解释其中的一些关键步骤,并介绍其中涉及的一些重要概念。