本篇針對 Rails 4,走一遍啟動 Rails 所需的每個方法呼叫。過程中詳細解釋每個步驟的用途,特別針對從 rails server, 到應用程式啟動起來之間的過程做解說。
除非特別聲明,本篇提及的路徑都是相對於 Rails 原始碼的目錄,或相對於 Rails 應用程式的路徑。
若想跟著瀏覽 Rails 的原始碼,推薦使用 GitHub 提供的檔案搜索功能,來快速找到檔案,在 GitHub Repository 頁面按 t 即可使用。
1 啟動!
從初始化(Initialize)與啟動(boot)應用程式開始。Rails 應用程式通常在執行 rails server 或 rails console 時會啟動。
1.1 railties/bin/rails
rails server 命令裡的 rails,是放在載入路徑(load path)下的 Ruby 執行檔。這個執行檔的內容如下:
version = ">= 0"
load Gem.bin_path('railties', 'rails', version)
若在 Rails Console 裡試這個命令,會看到這個命令載入了 railties/bin/rails。
railties/bin/rails 裡有這一行:
require "rails/cli"
railties/lib/rails/cli 接著呼叫 Rails::AppRailsLoader.exec_app_rails。
1.2 railties/lib/rails/app_rails_loader.rb
exec_app_rails 的主要目的是執行應用程式的 bin/rails,若當前目錄沒有 bin/rails,會往上搜索,看找不找的到 bin/rails。這也是為什麼可以在 rails 應用程式裡的任何目錄下使用 rails 命令。
rails server 實際上等於下面這個命令:
$ exec ruby bin/rails server
1.3 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 使用。這裡引用的 config/boot 檔案是指 config/boot.rb,負責載入與設定 Bundler。
1.4 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 應用程式裡,會有一個檔案,裡面記錄所有依賴的 RubyGems:Gemfile。config/boot.rb 將 ENV['BUNDLE_GEMFILE'] 設為 Gemfile 的位置。若 Gemfile 存在,則需要 require 'bundler/setup'。這一行是 Bundler 用來設定 Gemfile 內所有相依 RubyGems 的載入路徑。
標準的 Rails 應用程式依賴以下 RubyGems:
- actionmailer
- actionpack
- actionview
- activemodel
- activerecord
- activesupport
- arel
- builder
- bundler
- erubis
- i18n
- mime-types
- rack
- rack-cache
- rack-mount
- rack-test
- rails
- railties
- rake
- sqlite3
- thor
- tzinfo
1.5 rails/commands.rb
config/boot.rb 執行完畢後,下個 require 的檔案是 rails/commands,用來展開命令的別名(alias)。在 rails server 這個情況裡,ARGV 的內容是 server,無需展開:
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)
如上所見,ARGV 為空時,Rails 會印出幫助訊息。
若用了別名,如 rails s,便會用 aliases 展開成對應的命令:
1.6 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
假設傳入的命令是 server,Rails 會執行以下的程式碼:
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
private
def set_application_directory!
Dir.chdir(File.expand_path('../../', APP_PATH)) unless File.exist?(File.expand_path("config.ru"))
end
def require_command!(command)
require "rails/commands/#{command}"
end
沒找到 config.ru 時,會切換到 Rails 的根目錄(從 APP_PATH 往上兩層,APP_PATH 指向 config/application.rb )。接著 require rails/commands/server([rails/commands/server.rb](rails/commands/server),會把 Rails::Server 類別設定好。
require 'fileutils' require 'optparse' require 'action_dispatch' require 'rails' module Rails class Server < ::Rack::Server
fileutils 和 optparse 是 Ruby 的標準函式庫,用來處理檔案與解析命令行參數。
1.7 actionpack/lib/action_dispatch.rb
Action Dispatch 是 Rails 框架負責處理路由的元件。為 Rails 加入像是路由、Session 以及常見的 Middlewares。
1.8 rails/commands/server.rb
Rails::Server 在這個檔案裡定義,繼承自 Rack::Server。呼叫 Rails::Server.new 時,會呼叫 rails/commands/server.rb 裡的 initialize 方法:
def initialize(*) super set_environment end
首先呼叫 super,super 會呼叫 Rack::Server 的 initialize。
1.9 Rack: lib/rack/server.rb
Rack::Server 負責給所有基於 Rack 的應用程式,提供通用的伺服器接口(interface),Rails 也是基於 Rack 的應用程式。
Rack::Server 的 initialize 方法只是設定幾個變數而已:
def initialize(options = nil) @options = options @app = options[:app] if options && options[:app] end
這個情況裡,options 會是 nil,所以 initialize 什麼也沒做。
super 結束之後,回到 rails/commands/server.rb。接著在 Rails::Server 的上下文裡呼叫 set_environment,猛一看好像沒做什麼:
def set_environment ENV["RAILS_ENV"] ||= options[:environment] end
實際上 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! opt_parser.parse!(args),把從命令行來的參數與 opt_parser 的選項合併,opt_parser 在 Rack::Server 裡定義:
def opt_parser Options.new end
雖然 parse! 是在 Rack::Server 裡定義,但在 Rails::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 }
...
這個方法會設定好 options 所有的鍵,Rails 根據這些鍵,決定伺服器該怎麼執行。在 initialize 結束之後,回到 rails/commands/command_tasks.rb,見 # Back to here:
def server
set_application_directory!
require_command!("server")
# Back to here
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
1.10 config/application.rb
當 require APP_PATH 執行時,會載入 config/application.rb。回想一下,APP_PATH 在 Rails 應用程式下的 bin/rails 裡定義:
APP_PATH = File.expand_path('../../config/application', __FILE__)
config/application.rb 裡放的是任何要對應用程式修改的設定。
1.11 Rails::Server#start
config/application.rb 載入完畢後,呼叫了 server.start。#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
Rails 啟動過程“初次輸出訊息”的地方。這個方法會捕捉 INT 信號,所以當你按下 CTRL-C 時,才能從進程(process)裡離開。從這段程式碼可以看到,會建立出 tmp/cache、tmp/pids、tmp/sessions 以及 tmp/sockets 這四個目錄。接著呼叫 wrapped_app,這個方法負責在指定 ActiveSupport::Logger 之前,建立出 Rack 應用程式。
上面 start 方法裡的 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 的定義:
@wrapped_app ||= build_app app
app 方法的定義:
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
options[: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 的 initialize 方法接受區塊參數,會在 Rack::Builder 的實體裡執行這個區塊。Rails 啟動過程主要都在這裡發生。最先執行的是 config.ru 裡的這一行:
require ::File.expand_path('../config/environment', __FILE__)
1.12 config/environment.rb
這個檔案通常由 config.ru(即 rails server)與 Passenger require 進來。這也是兩種啟動伺服器方法首次相遇的地方。在這之前都只是在設定 Rack 與 Rails 而已。
這個檔案從 require config/application.rb 開始:
# Load the Rails application.
require File.expand_path('../application', __FILE__)
1.13 config/application.rb
這個檔案 require config/boot.rb:
require File.expand_path('../boot', __FILE__)
但只在 config/boot.rb 沒有被 require 的前提下才會進行 require。如此一來 rails server 才不會重複 require,但 Passenger 每次都會重新 require config/boot.rb。
有趣的事情開始了!
2 載入 Rails
config/application.rb 檔案的下一行是:
require 'rails/all'
2.1 railties/lib/rails/all.rb
這個檔案負責 require Rails 框架的各個元件:
require "rails"
%w(
active_record
action_controller
action_view
action_mailer
rails/test_unit
sprockets
).each do |framework|
begin
require "#{framework}/railtie"
rescue LoadError
end
end
這是整個 Rails 框架載入的地方,讓每個元件在應用程式裡都可以使用。每個部分怎麼載入的不深入探究,但有興趣可以自己深入研究。
現在只要記得,共用的功能像是 Rails 引擎、I18n 以及 Rails 所有的設定都是在這裡定義完成。
2.2 回到 config/environment.rb
config/application.rb 的其他部分定義了 Rails::Application 的設定,這些設定在應用程式啟動完畢時會全部載入進來。當 config/application.rb 載入 Rails 完畢時,以及應用程式命名空間定義完畢時,會回到應用程式初始化的地方,也就是 config/environment.rb。舉個例子,若應用程式叫做 Blog,則會找到 Rails.application.initialize!,這個方法在 rails/application.rb 裡定義。
2.3 railties/lib/rails/application.rb
initialize! 方法:
def initialize!(group=:default) #:nodoc: raise "Application has been already initialized." if @initialized run_initializers(group, self) @initialized = true self end
可以看到應用程式只會初始化一次。Initializers(config/initializers 目錄下的設定檔)透過 run_initializers 方法依序執行,run_initializers 方法在 railties/lib/rails/initializable.rb 裡定義:
def run_initializers(group=:default, *args)
return if instance_variable_defined?(:@ran)
initializers.tsort_each do |initializer|
initializer.run(*args) if initializer.belongs_to?(group)
end
@ran = true
end
run_initializers 很巧妙。在這裡會遍歷所有類別的祖先,找出有回應 initializers 方法的類別。接著按名稱將這些類別排序,再執行它們。舉例來說,Engine 類別透過給每個 Engine 提供 initializers 方法,讓這些 Engine 都可以引用進來。
Rails::Application 類別(在 railties/lib/rails/application.rb 裡定義)定義了 bootstrap、railtie、finisher 這三個 Initializers。第一個執行的 Initializer 是 bootstrap,bootstrap 將應用程式準備好(像是初始化 logger),而 finisher initializer 則是最後執行(像是把 Middleware 都建好)。而 railtie initializers 則是在 Rails::Application 裡定義,在 bootstrap 與 finisher 之間執行。
Initializers 都執行完畢後,回到 Rack::Server。
2.4 Rack: lib/rack/server.rb
在 1.9 小節 ,我們看過 app 是如何被定義的:
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
到了這一步,app 便是 Rails 應用程式本身(Middleware),接下來 Rack 會呼叫所有的 Middlewares:
def build_app(app)
middleware[options[:environment]].reverse_each do |middleware|
middleware = middleware.call(self) if middleware.respond_to?(:call)
next unless middleware
klass = middleware.shift
app = klass.new(app, *middleware)
end
app
end
記得 wrapped_app 在 Server#start 呼叫了 build_app (最後一行):
server.run wrapped_app, options, &blk
到這裡 server.run 取決於所使用的伺服器實作是那一個。假設用的是 Puma,下面是 Puma 的 run 方法:
...
DEFAULT_OPTIONS = {
:Host => '0.0.0.0',
:Port => 8080,
:Threads => '0:16',
:Verbose => false
}
def self.run(app, options = {})
options = DEFAULT_OPTIONS.merge(options)
if options[:Verbose]
app = Rack::CommonLogger.new(app, STDOUT)
end
if options[:environment]
ENV['RACK_ENV'] = options[:environment].to_s
end
server = ::Puma::Server.new(app)
min, max = options[:Threads].split(':', 2)
puts "Puma #{::Puma::Const::PUMA_VERSION} starting..."
puts "* Min threads: #{min}, max threads: #{max}"
puts "* Environment: #{ENV['RACK_ENV']}"
puts "* Listening on tcp://#{options[:Host]}:#{options[:Port]}"
server.add_tcp_listener options[:Host], options[:Port]
server.min_threads = min
server.max_threads = max
yield server if block_given?
begin
server.run.join
rescue Interrupt
puts "* Gracefully stopping, waiting for requests to finish"
server.stop(true)
puts "* Goodbye!"
end
end
伺服器本身的實作不深入探究,但這是 Rails 啟動過程整個旅程的最後一站。
希望這高度抽象的綜覽能幫助你更好的了解 Rails 程式是如何執行的,進而成為一個更好的 Rails 開發者。若想了解更多的話,那就閱讀 Rails 的原始碼吧!
反饋
歡迎幫忙改善指南的品質。
如發現任何錯誤之處,歡迎修正。開始貢獻前,可以先閱讀貢獻指南:文件。
翻譯如有錯誤,深感抱歉,歡迎 Fork 修正,或至此處回報。
文章可能有未完成或過時的內容。請先檢查 Edge Guides 來確定問題在 master 是否已經修掉了。再上 master 補上缺少的文件。內容參考 Ruby on Rails 指南準則來了解行文風格。
最後,任何關於 Ruby on Rails 文件的討論,歡迎至 rubyonrails-docs 郵件論壇。