v7.0.0
更多資訊請前往 rubyonrails.org: 更多在 Ruby on Rails

Rails 初始化過程

本指南解釋了 Rails 中初始化過程的內部結構。 這是一份非常深入的指南,推薦給高階 Rails 開發人員。

閱讀本指南後,您將瞭解:

本指南介紹了每個方法呼叫 需要在 Rails 堆疊上為預設的 Rails 啟動 Ruby 應用程式,沿途詳細解釋每個部分。為了這 指南,我們將關注執行 bin/rails server 時會發生什麼 啟動您的應用程式。

除非另有說明,本指南中的 NOTE: 路徑與 Rails 或 Rails 應用程式相關。

提示:如果您想在瀏覽 Rails 源 code,建議使用t key 繫結開啟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.

在標準的 Rails 應用程式中,有一個 Gemfile 宣告所有 應用程式的依賴關係。 config/boot.rbENV['BUNDLE_GEMFILE'] 到這個檔案的位置。如果 Gemfile 存在,則需要 bundler/setup。 Bundler 使用 require 來 為您的 Gemfile 的依賴項設定載入路徑。

一個標準的 Rails 應用程式依賴於幾個 gem,特別是:

  • actioncable
  • actionmailer
  • actionpack
  • actionview
  • 主動工作
  • activemodel
  • 活動記錄
  • 主動儲存
  • 主動支援
  • action郵箱
  • actiontext
  • 阿雷爾
  • 建造者
  • 捆綁器
  • erubi
  • i18n
  • 郵件
  • 啞劇型別
  • rack
  • rack-測試
  • rails
  • railties
  • 耙子
  • sqlite3
  • 雷神
  • 資訊

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 定義here來查詢匹配的命令。

1.4 rails/command.rb

當鍵入 Rails 命令時,invoke 嘗試查詢給定的命令 名稱空間並在找到時執行命令。

如果 Rails 不能識別該命令,它會將控制權交給 Rake 運行同名任務。

如圖所示,如果 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 框架的路由元件。 它添加了路由、session 和通用中介軟體等功能。

1.6 rails/commands/server/server_command.rb

Rails::Server 類是在這個檔案中定義的,繼承自 Rack::Server。當呼叫 Rails::Server.new 時,這會呼叫 initialize rails/commands/server/server_command.rb 中的方法:

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 的 value 將分配給 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

value 將被分配給實例變數 @options

superRack::Server 中完成後,我們跳回 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 目錄。然後在開發中啟用快取 如果用 --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] value 預設為 config.ru,其中包含:

# 這個檔案被根據 Rack 的伺服器用來啟動應用程式。

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.ruconfig/environment.rbrequire 行首先執行:

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
  sprockets/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 上定義的初始值設定項 本身並在 bootstrapfinishers 之間執行。

注意: 不要將 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

請記住,在 Rack::Server#start 的最後一行呼叫了 build_app(由 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 初始化過程中的最後一段旅程。

這個高層次的 overview 將幫助您瞭解您的程式碼何時 執行和如何,並總體上成為一個更好的 Rails 開發人員。如果你 還想了解更多,Rails原始碼本身大概就是 接下來最好的去處。

回饋

我們鼓勵您幫助提高本指南的品質。

如果您發現任何拼寫錯誤或資訊錯誤,請提供回饋。 要開始回饋,您可以閱讀我們的 回饋 部分。

您還可能會發現不完整的內容或不是最新的內容。 請務必為 main 新增任何遺漏的文件。假設是 非穩定版指南(edge guides) 請先驗證問題是否已經在主分支上解決。 請前往 Ruby on Rails 指南寫作準則 查看寫作風格和慣例。

如果由於某種原因您發現要修復的內容但無法自行修補,請您 提出 issue

關於 Ruby on Rails 的任何類型的討論歡迎提供任何文件至 rubyonrails-docs 討論區