学习 ruby on rails 有一段时间了,也写过一些简单的程序。但对 rails 一直充满神秘感,为什么我们把代码填充到 controller、view、model 里,再执行一下 rails s
,就能得到想要的结果。rails 背后为我们隐藏了多少东西,如果一点都不清楚,这样写代码不是像在搭建空中阁楼吗?心里总觉得不牢靠。感觉要想进一步提高水平,我得看一下源代码,至少可以满足以下心里的好奇心。以前学些程序的时候,比如 C/C++ 什么的,都有一个程序入口点 main 函数。在 rails 里,rails s
貌似是一切开始的地方,于是就从这里开始吧,看看它都干了什么。
在看 railties 源码时,发现 caller_locations
方法可以显示调用栈,于是想用它来跟踪了一下 rails s
的执行过程,虽然它并不能显示所有调用的方法,但是能大致知道程序经过了哪些文件,调用了哪些方法。我在 config/environment.rb
文件的末尾加上 caller_locations.each { |call| puts call.to_s }
一行,然后执行 rails s
,把输出的结果作为模糊的地图开始了 rails s
之旅。
环境说明
ruby 2.4.0, Rails 5.0.2
ruby gems 的位置
cd \
gem environment gemdir rails`/gems`
rails 命令在这里 ~/.rvm/gems/ruby-2.4.0/bin/rails
load Gem.activate_bin_path('railties', 'rails', version)
Gem.activate_bin_path
的主要作用是找到 gem 包下的可执行文件,即 bin 或 exe 目录下的文件。这里找到的是 /.rvm/gems/ruby-2.4.0/gems railties-5.0.2/exe/rails
,这个文件的主要作用是加载 require "rails/cli"
,该文件位于 .rvm/gems/ruby-2.4.0/gems railties-5.0.2/lib/rails/cli.rb
require 'rails/app_loader'
# If we are inside a Rails application this method performs an exec and thus
# the rest of this script is not run.
Rails::AppLoader.exec_app
require 'rails/ruby_version_check'
Signal.trap("INT") { puts; exit(1) }
if ARGV.first == 'plugin'
ARGV.shift
require 'rails/commands/plugin'
else
require 'rails/commands/application'
end
如果已新建了 rails app 并在应用程序目录下,则加载 AppLoader 模块并执行 exec_app 方法启动应用,否则进入新建 app 流程。
Rails::AppLoader.exec_app
方法用来找到应用程序目录下的 bin/rails
文件并执行。该方法通过一个 loop 循环,从当前目录逐级向上查找 bin/rails
,所以即使在应用程序的子目录里也是可以执行 rails 命令的。如果找到,并且文件中设置了 APP_PATH,则执行该文件。
.rvm/gems/ruby-2.4.0/gems railties-5.0.2/lib/rails/app_loader.rb
def exec_app
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
bin/rails
文件将 APP_PATH 设置为 config/application
,然后加载 config/boot
和 rails/commands
。
#!/usr/bin/env ruby
APP_PATH = File.expand_path('../config/application', __dir__)
require_relative '../config/boot'
require 'rails/commands'
config/boot
文件,主要作用是通过 Bundler.setup 将 Gemfile 中的 gem 路径添加到加载路径,以便后续 require。
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
require 'bundler/setup' # Set up gems listed in the Gemfile.
.rvm/gems/ruby-2.4.0/gems/railties-5.0.2/lib/rails/commands.rb
,这里解析 rails 命令参数,根据不同的参数执行不同的任务。
ARGV << '--help' if ARGV.empty?
aliases = {
"g" => "generate",
"d" => "destroy",
"c" => "console",
"s" => "server",
"db" => "dbconsole",
"r" => "runner",
"t" => "test"
}
command = ARGV.shift
command = aliases[command] || command
require 'rails/commands/commands_tasks'
Rails::CommandsTasks.new(ARGV).run_command!(command)
因为这里我们的命令参数是 s (server),所以 run_command!
函数调用了 server 方法
.rvm/gems/ruby-2.4.0/gems/railties-5.0.2/lib/rails/commands/commands_tasks.rb
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
server 方法首先设置应用程序目录,即包含 config.ru 文件的目录。然后加载 Rails::Server
模块。然后加载 APP_PATH,即 config/application.rb
文件,该文件加载了 rails 的全部组件 require "rails/all"
,并定义了我们自己的 rails 应用程序类,如 class Application < Rails::Application
,然后启动服务器。
.rvm/gems/ruby-2.4.0/gems/railties-5.0.2/lib/rails/commands/server.rb
def start
print_boot_information
trap(:INT) { exit }
create_tmp_directories
setup_dev_caching
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
start 方法做了一些准备和处理,直接调用 super 进入父类的 start 方法。Rails::Server 继承自 Rack::Server。
def start &blk
....
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
在这里通过 Rack 分别通过两个模块 Rack::Builder
Rack::Handler
创建 app 和选择服务器。wrapped_app 方法通过调用 app 方法创建应用程序实例,并调用 build_app 方法加载所有中间件。在 default_middleware_by_environment 方法里可以看到默认的中间件。
接着调用 server 方法选择 server,然后调用 server.run 启动服务器。
.rvm/gems/ruby-2.4.0/gems/rack-2.0.1/lib/rack/server.rb
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
由于在创建 Rails::Server 实例时没有传递参数,所以初始化是调用 default_options
方法设置了 options 。其中有一项 optioins[:config] 被设置为 "config.ru",这决定了 app 的创建方式是根据该配置文件创建。
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
具体的创建是通过调用 Rack::Builder.parse_file
方法实现。该方法首先解析 config.ru 文件,然后实例化 Rack::Builder 对象,然后在实例上下文中执行 config.ru 文件中的代码,然后调用 to_app
方法返回 app 对象。
def self.parse_file(config, opts = Server::Options.new)
options = {}
if config =~ /\.ru$/
cfgfile = ::File.read(config)
if cfgfile[/^#\\(.*)/] && opts
options = opts.parse! $1.split(/\s+/)
end
cfgfile.sub!(/^__END__\n.*\Z/m, '')
app = new_from_string cfgfile, config
else
require config
app = Object.const_get(::File.basename(config, '.rb').split('_').map(&:capitalize).join(''))
end
return app, options
end
def self.new_from_string(builder_script, file="(rackup)")
eval "Rack::Builder.new {\n" + builder_script + "\n}.to_app",
TOPLEVEL_BINDING, file, 0
end
def initialize(default_app = nil, &block)
@use, @map, @run, @warmup = [], nil, default_app, nil
instance_eval(&block) if block_given?
end
config.ru 文件中的代码是在 Rack::Builder 的实例上下文中执行的,所以 run Rails.application
实际上是执行 Rack::Builder#run
。它只是简单的把 Rails.application,即我们自己的 Rails 应用程序实例赋值给 Rack::Server
的 @app
。在此之前它先加载了 config/environment.rb
文件初始化应用 Rails.application.initialize!
,而后者又加载了 config/application.rb
,该文件中加载了 rails 的全部组件 require 'rails/all'
和定义了我们自己 Application 类 class Application < Rails::Application
。
def run(app)
@run = app
end
def to_app
app = @map ? generate_map(@run, @map) : @run
fail "missing run or map statement" unless app
app = @use.reverse.inject(app) { |a,e| e[a] }
@warmup.call(app) if @warmup
app
end
以下是 config.ru 文件代码
# This file is used by Rack-based servers to start the application.
require_relative 'config/environment'
# run is a method of Rack::Handler that used for setting up rack server.
run Rails.application
以下是 config/environment.rb 文件代码
# Load the Rails application.
require_relative 'application'
# Initialize the Rails application.
Rails.application.initialize!
到此 Rack app 对象已经建立,Rails.application 实例被保存在 Rack::Server 实例的 @app 里。接着调用 server.run
,并将 app 作为参数传递给它。
server
方法通过 Rack::Handler
选择服务器。由于我们创建 server 实例时没有提供 options,默认的 options[:server] 不存在。所以,通过 Rack::Handler.default
方法进行猜选,先查看 ENV 环境变量里有没有设置,如果没有则按照 ['puma', 'thin', 'webrick']
顺序猜选。通过调用 Rack::Handler.get
方法获取 server 类常量。首先查看@handlers
中是否有匹配的,如没有则尝试通过 try_require('rack/handler', server)
加载。比如对于 puma,该方法的加载路径相当于 rack/handler/puma
。可能你会想这个路径哪里来的,从 RAILS 5 开始,Gemfile 文件里有一行 gem 'puma', '~> 3.0'
,然后启动应用程序时,在 config/boot.rb
文件里已经通过 Bundler.setup 加载了所有依赖的路径。最终 get 方法会返回 Rack::Handler::Puma
这样一个类。
def server
@_server ||= Rack::Handler.get(options[:server])
unless @_server
@_server = Rack::Handler.default
# We already speak FastCGI
@ignore_options = [:File, :Port] if @_server.to_s == 'Rack::Handler::FastCGI'
end
@_server
end
def self.default
# Guess.
if ENV.include?("PHP_FCGI_CHILDREN")
Rack::Handler::FastCGI
elsif ENV.include?(REQUEST_METHOD)
Rack::Handler::CGI
elsif ENV.include?("RACK_HANDLER")
self.get(ENV["RACK_HANDLER"])
else
pick ['puma', 'thin', 'webrick']
end
end
def self.get(server)
return unless server
server = server.to_s
unless @handlers.include? server
load_error = try_require('rack/handler', server)
end
if klass = @handlers[server]
klass.split("::").inject(Object) { |o, x| o.const_get(x) }
else
const_get(server, false)
end
rescue NameError => name_error
raise load_error || name_error
end
在 Rack::Server#start
方法里调用的 server.run wrapped_app, options, &blk
实际上是执行了 Rack::Handler::Puma.run
。该方法在.rvm/gems/ruby-2.4.0/gems/puma-3.7.1/lib/rack/handler/puma.rb
文件里。到这里暂且不用往下挖了,可以想象 Puma 服务器进程启动起来,一切就绪,进入服务器端口监听循环,随时等待接收客户端发来的请求,或者直到收到系统中断信号,关闭服务器,回到最开始的位置。还记得 rails s
吗?这时它正微笑着和你打招呼 “嘿,骚年我以为你迷路了呢?”
小结
从敲下 rails s
命令开始,到服务器启动起来,其实用很短的话就可以概括:rails 命令行解析,创建 Rails::Server 对象,加载 Rails.application,启动服务器监听用户请求。绕了这么一个大圈子,似乎懂了点什么,又好像没懂什么,至少对 Rails
的神秘感减少了几分吧。