更多内容请访问 rubyonrails.org:

Rails 初始化过程

本指南解释了 Rails 初始化过程的内部机制。这是一份极其深入的指南,推荐给高级 Rails 开发者阅读。

阅读本指南后,您将了解

  • 如何使用 bin/rails server
  • Rails 初始化序列的时间轴。
  • 启动序列在何处需要不同的文件。
  • Rails::Server 接口如何定义和使用。

本指南将详细介绍启动默认 Rails 应用程序的 Ruby on Rails 堆栈所需的所有方法调用,并一路详细解释每个部分。在本指南中,我们将重点关注当您执行 bin/rails server 启动应用程序时发生的情况。

除非另有说明,本指南中的路径相对于 Rails 或 Rails 应用程序。

如果您想在浏览 Rails 源代码 时同步学习,我们建议您使用 t 键绑定在 GitHub 中打开文件查找器以快速查找文件。

1. 启动!

让我们开始启动和初始化应用程序。Rails 应用程序通常通过运行 bin/rails consolebin/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.rbENV['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.rbAPP_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_PATHbin/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/cachetmp/pidstmp/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::Builderinitialize 方法将在此处获取块并在 Rack::Builder 的实例中执行它。这是 Rails 初始化过程的大部分发生的地方。config.ruconfig/environment.rbrequire 行首先运行

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 中定义)定义了 bootstraprailtiefinisher 初始化器。bootstrap 初始化器准备应用程序(如初始化日志器),而 finisher 初始化器(如构建中间件堆栈)最后运行。railtie 初始化器是已在 Rails::Application 本身定义的初始化器,它们在 bootstrapfinisher 之间运行。

不要将 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 源代码本身可能是下一步的最佳去处。



回到顶部