本篇針對 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 郵件論壇。