更多信息请访问 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 来运行同名任务。

如所示,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.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 类在该文件中定义,它继承自 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_environmentRails::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_PATHbin/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/cachetmp/pidstmp/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::Builderinitialize 方法将采用这里的块,并在 Rack::Builder 的实例中执行它。这是 Rails 初始化过程的大部分内容发生的地方。config.ru 中的 require 行,用于 config/environment.rb,是第一个运行的

require_relative "config/environment"

1.10 config/environment.rb

该文件是 config.rubin/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 源代码本身可能是最好的下一步去处。



返回顶部