本指南會說明啟動 Ruby on Rails 堆疊以供預設 Rails 應用程式使用時所需的所有方法呼叫,並在過程中詳細說明每個部分。在本指南中,我們將重點說明執行 bin/rails server
以啟動您的應用程式時會發生什麼事。
除非另有說明,否則本指南中的路徑都是相對於 Rails 或 Rails 應用程式。
如果您想在瀏覽 Rails 原始碼的同時進行追蹤,我們建議您使用 t
按鍵繫結在 GitHub 內開啟檔案尋找器並快速尋找檔案。
1 啟動!
讓我們開始啟動並初始化應用程式。Rails 應用程式通常透過執行 bin/rails console
或 bin/rails server
來啟動。
1.1 bin/rails
此檔案如下
#!/usr/bin/env ruby
APP_PATH = File.expand_path('../config/application', __dir__)
require_relative "../config/boot"
require "rails/commands"
APP_PATH
常數稍後會在 rails/commands
中使用。此處引用的 config/boot
檔案是應用程式中的 config/boot.rb
檔案,負責載入 Bundler 並設定它。
1.2 config/boot.rb
config/boot.rb
包含
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
require "bundler/setup" # Set up gems listed in the Gemfile.
在標準 Rails 應用程式中,有一個 Gemfile
宣告應用程式的所有相依性。config/boot.rb
將 ENV['BUNDLE_GEMFILE']
設定為此檔案的位置。如果 Gemfile
存在,則需要 bundler/setup
。Bundler 使用 require 來設定 Gemfile 的相依性的載入路徑。
1.3 rails/commands.rb
一旦 config/boot.rb
完成,下一個需要的檔案是 rails/commands
,它有助於擴充別名。在目前的情況下,ARGV
陣列僅包含 server
,它會傳遞
require "rails/command"
aliases = {
"g" => "generate",
"d" => "destroy",
"c" => "console",
"s" => "server",
"db" => "dbconsole",
"r" => "runner",
"t" => "test"
}
command = ARGV.shift
command = aliases[command] || command
Rails::Command.invoke command, ARGV
如果我們使用 s
而不是 server
,Rails 會使用在此處定義的 aliases
來尋找相符的指令。
1.4 rails/command.rb
當有人輸入 Rails 指令時,invoke
會嘗試尋找給定命名空間的指令,並在找到時執行指令。
如果 Rails 無法辨識指令,它會將控制權移交給 Rake 來執行同名的任務。
如所示,如果 namespace
為空,Rails::Command
會自動顯示說明輸出。
module Rails
module Command
class << self
def invoke(full_namespace, args = [], **config)
namespace = full_namespace = full_namespace.to_s
if char = namespace =~ /:(\w+)$/
command_name, namespace = $1, namespace.slice(0, char)
else
command_name = namespace
end
command_name, namespace = "help", "help" if command_name.blank? || HELP_MAPPINGS.include?(command_name)
command_name, namespace = "version", "version" if %w( -v --version ).include?(command_name)
command = find_by_namespace(namespace, command_name)
if command && command.all_commands[command_name]
command.perform(command_name, args, config)
else
find_by_namespace("rake").perform(full_namespace, args, config)
end
end
end
end
end
使用 server
指令,Rails 將進一步執行下列程式碼
module Rails
module Command
class ServerCommand < Base # :nodoc:
def perform
extract_environment_option_from_argument
set_application_directory!
prepare_restart
Rails::Server.new(server_options).tap do |server|
# Require application after server sets environment to propagate
# the --environment option.
require APP_PATH
Dir.chdir(Rails.application.root)
if server.serveable?
print_boot_information(server.server, server.served_url)
after_stop_callback = -> { say "Exiting" unless options[:daemon] }
server.start(after_stop_callback)
else
say rack_server_suggestion(using)
end
end
end
end
end
end
此檔案將變更為 Rails 根目錄(從指向 config/application.rb
的 APP_PATH
往上一層的目錄),但僅在找不到 config.ru
檔案時才會變更。這接著會啟動 Rails::Server
類別。
1.5 actionpack/lib/action_dispatch.rb
Action Dispatch 是 Rails 架構的路由元件。它會新增路由、工作階段和一般中介軟體等功能。
1.6 rails/commands/server/server_command.rb
Rails::Server
類別在此檔案中定義,方法是繼承自 Rack::Server
。當呼叫 Rails::Server.new
時,這會呼叫 rails/commands/server/server_command.rb
中的 initialize
方法
module Rails
class Server < ::Rack::Server
def initialize(options = nil)
@default_options = options || {}
super(@default_options)
set_environment
end
end
end
首先,會呼叫 super
,這會呼叫 Rack::Server
中的 initialize
方法。
1.7 Rack:lib/rack/server.rb
Rack::Server
負責提供所有基於 Rack 的應用程式(Rails 現在是其中之一)的共用伺服器介面。
Rack::Server
中的 initialize
方法只會設定幾個變數
module Rack
class Server
def initialize(options = nil)
@ignore_options = []
if options
@use_default_options = false
@options = options
@app = options[:app] if options[:app]
else
argv = defined?(SPEC_ARGV) ? SPEC_ARGV : ARGV
@use_default_options = true
@options = parse_options(argv)
end
end
end
end
在此情況下,Rails::Command::ServerCommand#server_options
的傳回值會指定給 options
。當評估 if 陳述式內的行時,會設定幾個執行個體變數。
Rails::Command::ServerCommand
中的 server_options
方法定義如下
module Rails
module Command
class ServerCommand
no_commands do
def server_options
{
user_supplied_options: user_supplied_options,
server: using,
log_stdout: log_to_stdout?,
Port: port,
Host: host,
DoNotReverseLookup: true,
config: options[:config],
environment: environment,
daemonize: options[:daemon],
pid: pid,
caching: options[:dev_caching],
restart_cmd: restart_command,
early_hints: early_hints
}
end
end
end
end
end
值會指定給執行個體變數 @options
。
Rack::Server
中的 super
完成後,我們會跳回 rails/commands/server/server_command.rb
。此時,會在 Rails::Server
物件的內容中呼叫 set_environment
。
module Rails
module Server
def set_environment
ENV["RAILS_ENV"] ||= options[:environment]
end
end
end
在 initialize
完成後,我們跳回需要 APP_PATH
(先前已設定) 的伺服器命令。
1.8 config/application
執行 require APP_PATH
時,會載入 config/application.rb
(請回想 APP_PATH
是在 bin/rails
中定義的)。此檔案存在於您的應用程式中,您可以根據您的需求自由變更。
1.9 Rails::Server#start
載入 config/application
後,會呼叫 server.start
。此方法定義如下
module Rails
class Server < ::Rack::Server
def start(after_stop_callback = nil)
trap(:INT) { exit }
create_tmp_directories
setup_dev_caching
log_to_stdout if options[:log_stdout]
super()
# ...
end
private
def setup_dev_caching
if options[:environment] == "development"
Rails::DevCaching.enable_by_argument(options[:caching])
end
end
def create_tmp_directories
%w(cache pids 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
unless ActiveSupport::Logger.logger_outputs_to?(Rails.logger, STDOUT)
Rails.logger.extend(ActiveSupport::Logger.broadcast(console))
end
end
end
end
此方法會為 INT
訊號建立陷阱,因此如果您對伺服器按 CTRL-C
,它會結束處理程序。從這裡的程式碼中,我們可以看到它會建立 tmp/cache
、tmp/pids
和 tmp/sockets
目錄。然後,如果使用 --dev-caching
呼叫 bin/rails server
,它會在開發中啟用快取。最後,它會呼叫 wrapped_app
,負責建立 Rack 應用程式,然後建立並指定 ActiveSupport::Logger
的執行個體。
super
方法會呼叫 Rack::Server.start
,其定義開頭如下
module Rack
class Server
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).
handle_profiling(options[:heapfile], options[:profile_mode], options[:profile_file]) do
wrapped_app
end
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
end
end
對於 Rails 應用程式來說,最後一行 server.run
很有趣。在這裡,我們再次遇到 wrapped_app
方法,這次我們會更深入探討 (即使它先前已執行,且現在已備忘)。
module Rack
class Server
def wrapped_app
@wrapped_app ||= build_app app
end
end
end
此處的 app
方法定義如下
module Rack
class Server
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)
@options.merge!(options) { |key, old, new| old }
app
end
def build_app_from_string
Rack::Builder.new_from_string(self.options[:builder])
end
end
end
options[:config]
值預設為 config.ru
,其中包含下列內容
# This file is used by Rack-based servers to start the application.
require_relative "config/environment"
run Rails.application
此處的 Rack::Builder.parse_file
方法會從此 config.ru
檔案取得內容,並使用下列程式碼進行剖析
module Rack
class Builder
def self.load_file(path, opts = Server::Options.new)
# ...
app = new_from_string cfgfile, config
# ...
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
end
end
Rack::Builder
的 initialize
方法會採用此區塊,並在 Rack::Builder
的執行個體中執行它。這是 Rails 大部分初始化程序執行的地方。config.ru
中 config/environment.rb
的 require
列為第一個執行的內容
require_relative "config/environment"
1.10 config/environment.rb
這個檔案是 config.ru
(bin/rails server
) 和 Passenger 所共用的檔案。這是這兩種執行伺服器方式的交會點;在此之前的所有內容都是 Rack 和 Rails 的設定。
這個檔案從需要 config/application.rb
開始
require_relative "application"
1.11 config/application.rb
這個檔案需要 config/boot.rb
require_relative "boot"
但僅在之前並未需要時,這會發生在 bin/rails server
中,但不會發生在 Passenger 中。
然後,精彩的內容開始了!
2 載入 Rails
config/application.rb
中的下一行是
require "rails/all"
2.1 railties/lib/rails/all.rb
這個檔案負責需要 Rails 的所有個別架構
require "rails"
%w(
active_record/railtie
active_storage/engine
action_controller/railtie
action_view/railtie
action_mailer/railtie
active_job/railtie
action_cable/engine
action_mailbox/engine
action_text/engine
rails/test_unit/railtie
).each do |railtie|
begin
require 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
。在此,應用程式會使用 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
您只能初始化一次應用程式。Railtie 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
程式碼本身很棘手。Rails 在這裡所做的,是遍歷所有類別祖先,尋找那些對 initializers
方法有回應的類別。然後,它會依名稱對祖先進行排序,並執行它們。例如,Engine
類別會提供 initializers
方法,讓所有引擎都可用。
Rails::Application
類別定義於 railties/lib/rails/application.rb
,它定義了 bootstrap
、railtie
和 finisher
initializers。bootstrap
initializers 會準備應用程式(例如初始化記錄器),而 finisher
initializers(例如建立中間件堆疊)會在最後執行。railtie
initializers 是在 Rails::Application
本身定義的 initializers,而且會在 bootstrap
和 finishers
之間執行。
不要將 Railtie initializers 整體與 load_config_initializers initializer 執行個體或其在 config/initializers
中關聯的設定 initializers 混淆。
完成此步驟後,我們會回到 Rack::Server
。
2.4 Rack:lib/rack/server.rb
上次我們離開時,正在定義 app
方法
module Rack
class Server
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)
@options.merge!(options) { |key, old, new| old }
app
end
def build_app_from_string
Rack::Builder.new_from_string(self.options[:builder])
end
end
end
此時,app
是 Rails 應用程式本身(一個中間件),接下來會發生的是,Rack 會呼叫所有提供的中間件
module Rack
class Server
private
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
end
end
請記住,build_app
在 Rack::Server#start
的最後一行中由 wrapped_app
呼叫。以下是我們離開時的樣子
server.run wrapped_app, options, &blk
此時,server.run
的實作會取決於您使用的伺服器。例如,如果您使用的是 Puma,則 run
方法如下所示
module Rack
module Handler
module Puma
# ...
def self.run(app, options = {})
conf = self.config(app, options)
events = options.delete(:Silent) ? ::Puma::Events.strings : ::Puma::Events.stdio
launcher = ::Puma::Launcher.new(conf, events: events)
yield launcher if block_given?
begin
launcher.run
rescue Interrupt
puts "* Gracefully stopping, waiting for requests to finish"
launcher.stop
puts "* Goodbye!"
end
end
# ...
end
end
end
我們不會深入探討伺服器設定本身,但這是我們在 Rails 初始化過程中最後要探討的部分。
此概觀將有助於您了解您的程式碼何時以及如何執行,並整體成為更優秀的 Rails 開發人員。如果您仍然想要了解更多,Rails 原始程式碼本身可能是您接下來最好的去處。
回饋
歡迎協助我們提升本指南的品質。
如果您發現任何錯字或事實錯誤,請務必貢獻您的力量。首先,您可以閱讀我們的 文件貢獻 部分。
您也可能會發現不完整或過時的內容。請務必為 main 新增任何遺漏的文件。請務必先查看 Edge Guides,以確認問題是否已在主分支中修復。查看 Ruby on Rails 指南指南 以了解風格和慣例。
如果您發現需要修復的內容,但無法自行修補,請 開啟問題。
最後但並非最不重要的一點是,歡迎在 官方 Ruby on Rails 論壇 上討論有關 Ruby on Rails 文件的任何議題。