本指南将详细介绍启动默认 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 来运行同名任务。
如所示,如果 namespace 为空,Rails::Command 会自动显示帮助输出。
module Rails
module Command
class << self
def invoke(full_namespace, args = [], **config)
args = ["--help"] if rails_new_with_no_path?(args)
full_namespace = full_namespace.to_s
namespace, command_name = split_namespace(full_namespace)
command = find_by_namespace(namespace, command_name)
with_argv(args) do
if command && command.all_commands[command_name]
command.perform(command_name, args, config)
else
invoke_rake(full_namespace, args, config)
end
end
rescue UnrecognizedCommandError => error
if error.name == full_namespace && command && command_name == full_namespace
command.perform("help", [], config)
else
puts error.detailed_message
end
exit(1)
end
end
end
end
使用 server 命令,Rails 将进一步运行以下代码
module Rails
module Command
class ServerCommand < Base # :nodoc:
def perform
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(options[: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 类在此文件中通过继承 Rackup::Server 定义。当调用 Rails::Server.new 时,这会调用 rails/commands/server/server_command.rb 中的 initialize 方法
module Rails
class Server < Rackup::Server
def initialize(options = nil)
@default_options = options || {}
super(@default_options)
set_environment
end
end
end
首先,调用 super,它调用 Rackup::Server 上的 initialize 方法。
1.7. Rackup: lib/rackup/server.rb
Rackup::Server 负责为所有基于 Rack 的应用程序提供通用的服务器接口,Rails 现在是其中的一部分。
Rackup::Server 中的 initialize 方法只是设置了几个变量
module Rackup
class Server
def initialize(options = nil)
@ignore_options = []
if options
@use_default_options = false
@options = options
@app = options[:app] if options[:app]
else
@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 < Base # :nodoc:
no_commands do
def server_options
{
user_supplied_options: user_supplied_options,
server: options[: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。
在 Rackup::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 < ::Rackup::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, STDERR, STDOUT)
Rails.logger.broadcast_to(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 方法将调用 Rackup::Server.start,其定义如下
module Rackup
class Server
def start(&block)
if options[:warn]
$-w = true
end
if includes = options[:include]
$LOAD_PATH.unshift(*includes)
end
Array(options[:require]).each do |library|
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, &block)
end
end
end
对于 Rails 应用程序来说,有趣的部分是最后一行 server.run。在这里,我们再次遇到 wrapped_app 方法,这次我们将更深入地探讨它(尽管它之前已经执行过,并且现在已经备忘录化了)。
module Rackup
class Server
def wrapped_app
@wrapped_app ||= build_app app
end
end
end
这里的 app 方法定义如下
module Rackup
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
Rack::Builder.parse_file(self.options[:config])
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
Rails.application.load_server
这里的 Rack::Builder.parse_file 方法从 config.ru 文件中获取内容并使用此代码进行解析
module Rack
class Builder
def self.load_file(path, **options)
# ...
new_from_string(config, path, **options)
end
# ...
def self.new_from_string(builder_script, path = "(rackup)", **options)
builder = self.new(**options)
# We want to build a variant of TOPLEVEL_BINDING with self as a Rack::Builder instance.
# We cannot use instance_eval(String) as that would resolve constants differently.
binding = BUILDER_TOPLEVEL_BINDING.call(builder)
eval(builder_script, binding, path)
builder.to_app
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 的设置。
此文件以 require config/application.rb 开头
require_relative "application"
1.11. config/application.rb
此文件需要 config/boot.rb
require_relative "boot"
但前提是它之前没有被 require 过,这在 bin/rails server 的情况下会发生,但 **不会** 在 Passenger 的情况下发生。
然后乐趣就开始了!
2. 加载 Rails
config/application.rb 中的下一行是
require "rails/all"
2.1. railties/lib/rails/all.rb
此文件负责 require 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 和 finisher 之间运行。
不要将 Railtie 初始化器与 load_config_initializers 初始化器实例或其在 config/initializers 中关联的配置初始化器混淆。
完成此操作后,我们回到 Rackup::Server。
2.4. Rack: lib/rack/server.rb
上次我们离开时,app 方法正在定义
module Rackup
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
Rack::Builder.parse_file(self.options[:config])
end
def build_app_from_string
Rack::Builder.new_from_string(self.options[:builder])
end
end
end
此时 app 是 Rails 应用程序本身(一个中间件),接下来 Rack 将调用所有提供的中间件
module Rackup
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 是在 Rackup::Server#start 的最后一行被调用(通过 wrapped_app)。我们离开时它看起来是这样的
server.run(wrapped_app, **options, &block)
此时,server.run 的实现将取决于您使用的服务器。例如,如果您使用 Puma,run 方法将如下所示
module Rack
module Handler
module Puma
# ...
def self.run(app, options = {})
conf = self.config(app, options)
log_writer = options.delete(:Silent) ? ::Puma::LogWriter.strings : ::Puma::LogWriter.stdio
launcher = ::Puma::Launcher.new(conf, log_writer: log_writer, 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 源代码本身可能是下一步的最佳去处。