本指南将介绍启动默认 Rails 应用程序的 Ruby on 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.
require "bootsnap/setup" # Speed up boot time by caching expensive operations.
在标准 Rails 应用程序中,有一个 Gemfile
,它声明了应用程序的所有依赖项。config/boot.rb
将 ENV['BUNDLE_GEMFILE']
设置为该文件的路径。如果 Gemfile
存在,则需要 bundler/setup
。该 require 被 Bundler 用于配置 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 来运行同名任务。
如所示,Rails::Command
会在 namespace
为空时自动显示帮助输出。
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
。此时,set_environment
在 Rails::Server
对象的上下文中被调用。
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
目录。然后,如果 bin/rails server
被调用时带有 --dev-caching
,它将在开发环境中启用缓存。最后,它将调用 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
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
中的 require
行,用于 config/environment.rb
,是第一个运行的
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 初始化器 通过 `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` 初始化器。 `bootstrap` 初始化器准备应用程序(例如初始化日志记录器),而 `finisher` 初始化器(例如构建中间件堆栈)最后运行。 `railtie` 初始化器是在 `Rails::Application` 本身上定义的初始化器,并在 `bootstrap` 和 `finishers` 之间运行。
不要将 Railtie 初始化器与 load_config_initializers 初始化器实例或 `config/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 源代码本身可能是最好的下一步去处。