學(xué)習(xí) ruby on rails 有一段時間了呜达,也寫過一些簡單的程序捺球。但對 rails 一直充滿神秘感,為什么我們把代碼填充到 controller班巩、view金吗、model 里,再執(zhí)行一下 rails s
趣竣,就能得到想要的結(jié)果摇庙。rails 背后為我們隱藏了多少東西,如果一點都不清楚遥缕,這樣寫代碼不是像在搭建空中閣樓嗎卫袒?心里總覺得不牢靠。感覺要想進(jìn)一步提高水平单匣,我得看一下源代碼夕凝,至少可以滿足以下心里的好奇心。以前學(xué)些程序的時候户秤,比如 C/C++ 什么的码秉,都有一個程序入口點 main 函數(shù)。在 rails 里鸡号,rails s
貌似是一切開始的地方转砖,于是就從這里開始吧,看看它都干了什么鲸伴。
在看 railties 源碼時府蔗,發(fā)現(xiàn) caller_locations
方法可以顯示調(diào)用棧晋控,于是想用它來跟蹤了一下 rails s
的執(zhí)行過程,雖然它并不能顯示所有調(diào)用的方法姓赤,但是能大致知道程序經(jīng)過了哪些文件赡译,調(diào)用了哪些方法。我在 config/environment.rb
文件的末尾加上 caller_locations.each { |call| puts call.to_s }
一行不铆,然后執(zhí)行 rails s
蝌焚,把輸出的結(jié)果作為模糊的地圖開始了 rails s
之旅。
環(huán)境說明
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 包下的可執(zhí)行文件综看,即 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 并在應(yīng)用程序目錄下舞吭,則加載 AppLoader 模塊并執(zhí)行 exec_app 方法啟動應(yīng)用泡垃,否則進(jìn)入新建 app 流程。
Rails::AppLoader.exec_app
方法用來找到應(yīng)用程序目錄下的 bin/rails
文件并執(zhí)行羡鸥。該方法通過一個 loop 循環(huán)蔑穴,從當(dāng)前目錄逐級向上查找 bin/rails
,所以即使在應(yīng)用程序的子目錄里也是可以執(zhí)行 rails 命令的惧浴。如果找到存和,并且文件中設(shè)置了 APP_PATH,則執(zhí)行該文件衷旅。
.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 設(shè)置為 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 路徑添加到加載路徑茄袖,以便后續(xù) 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 命令參數(shù)宪祥,根據(jù)不同的參數(shù)執(zhí)行不同的任務(wù)。
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)
因為這里我們的命令參數(shù)是 s (server)家乘,所以 run_command!
函數(shù)調(diào)用了 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 方法首先設(shè)置應(yīng)用程序目錄蝗羊,即包含 config.ru 文件的目錄。然后加載 Rails::Server
模塊仁锯。然后加載 APP_PATH耀找,即 config/application.rb
文件,該文件加載了 rails 的全部組件 require "rails/all"
业崖,并定義了我們自己的 rails 應(yīng)用程序類涯呻,如 class Application < Rails::Application
凉驻,然后啟動服務(wù)器。
.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 方法做了一些準(zhǔn)備和處理复罐,直接調(diào)用 super 進(jìn)入父類的 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
創(chuàng)建 app 和選擇服務(wù)器效诅。wrapped_app 方法通過調(diào)用 app 方法創(chuàng)建應(yīng)用程序?qū)嵗凸觯⒄{(diào)用 build_app 方法加載所有中間件。在 default_middleware_by_environment 方法里可以看到默認(rèn)的中間件乱投。
接著調(diào)用 server 方法選擇 server咽笼,然后調(diào)用 server.run 啟動服務(wù)器。
.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
由于在創(chuàng)建 Rails::Server 實例時沒有傳遞參數(shù)戚炫,所以初始化是調(diào)用 default_options
方法設(shè)置了 options 剑刑。其中有一項 optioins[:config] 被設(shè)置為 "config.ru",這決定了 app 的創(chuàng)建方式是根據(jù)該配置文件創(chuàng)建双肤。
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
具體的創(chuàng)建是通過調(diào)用 Rack::Builder.parse_file
方法實現(xiàn)施掏。該方法首先解析 config.ru 文件,然后實例化 Rack::Builder 對象茅糜,然后在實例上下文中執(zhí)行 config.ru 文件中的代碼七芭,然后調(diào)用 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 的實例上下文中執(zhí)行的蔑赘,所以 run Rails.application
實際上是執(zhí)行 Rack::Builder#run
狸驳。它只是簡單的把 Rails.application,即我們自己的 Rails 應(yīng)用程序?qū)嵗x值給 Rack::Server
的 @app
缩赛。在此之前它先加載了 config/environment.rb
文件初始化應(yīng)用 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 對象已經(jīng)建立究西,Rails.application 實例被保存在 Rack::Server 實例的 @app 里。接著調(diào)用 server.run
物喷,并將 app 作為參數(shù)傳遞給它卤材。
server
方法通過 Rack::Handler
選擇服務(wù)器。由于我們創(chuàng)建 server 實例時沒有提供 options峦失,默認(rèn)的 options[:server] 不存在扇丛。所以,通過 Rack::Handler.default
方法進(jìn)行猜選尉辑,先查看 ENV 環(huán)境變量里有沒有設(shè)置帆精,如果沒有則按照 ['puma', 'thin', 'webrick']
順序猜選。通過調(diào)用 Rack::Handler.get
方法獲取 server 類常量。首先查看@handlers
中是否有匹配的卓练,如沒有則嘗試通過 try_require('rack/handler', server)
加載隘蝎。比如對于 puma,該方法的加載路徑相當(dāng)于 rack/handler/puma
襟企≈雒矗可能你會想這個路徑哪里來的,從 RAILS 5 開始顽悼,Gemfile 文件里有一行 gem 'puma', '~> 3.0'
曼振,然后啟動應(yīng)用程序時,在 config/boot.rb
文件里已經(jīng)通過 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
方法里調(diào)用的 server.run wrapped_app, options, &blk
實際上是執(zhí)行了 Rack::Handler::Puma.run
。該方法在.rvm/gems/ruby-2.4.0/gems/puma-3.7.1/lib/rack/handler/puma.rb
文件里木羹。到這里暫且不用往下挖了甲雅,可以想象 Puma 服務(wù)器進(jìn)程啟動起來,一切就緒坑填,進(jìn)入服務(wù)器端口監(jiān)聽循環(huán)抛人,隨時等待接收客戶端發(fā)來的請求,或者直到收到系統(tǒng)中斷信號穷遂,關(guān)閉服務(wù)器,回到最開始的位置娱据。還記得 rails s
嗎蚪黑?這時它正微笑著和你打招呼 “嘿,騷年我以為你迷路了呢?”
小結(jié)
從敲下 rails s
命令開始中剩,到服務(wù)器啟動起來忌穿,其實用很短的話就可以概括:rails 命令行解析,創(chuàng)建 Rails::Server 對象结啼,加載 Rails.application掠剑,啟動服務(wù)器監(jiān)聽用戶請求。繞了這么一個大圈子郊愧,似乎懂了點什么朴译,又好像沒懂什么,至少對 Rails
的神秘感減少了幾分吧属铁。