edge
เพิ่มเติมที่ rubyonrails.org: เพิ่มเติมเกี่ยวกับ Ruby on Rails

กระบวนการเริ่มต้น Rails

เอกสารนี้อธิบายเกี่ยวกับกระบวนการเริ่มต้นใน Rails ซึ่งเป็นเอกสารที่ลึกซึ้งมากและแนะนำสำหรับนักพัฒนา Rails ระดับสูง

หลังจากอ่านเอกสารนี้คุณจะรู้:

เอกสารนี้จะอธิบายเกี่ยวกับการเรียกใช้เมธอดที่จำเป็นทุกครั้งในกระบวนการเริ่มต้น Ruby on Rails สำหรับแอปพลิเคชัน Rails ที่มีค่าเริ่มต้น โดยอธิบายแต่ละส่วนอย่างละเอียดในขณะที่เดินทาง สำหรับเอกสารนี้ เราจะให้ความสนใจกับสิ่งที่เกิดขึ้นเมื่อคุณเรียกใช้ bin/rails server เพื่อเริ่มต้นแอปของคุณ

หมายเหตุ: พาธในเอกสารนี้เป็นพาธที่เกี่ยวข้องกับ Rails หรือแอปพลิเคชัน Rails ยกเว้นที่ระบุไว้เป็นอย่างอื่น

เคล็ดลับ: หากคุณต้องการติดตามข้อมูลในขณะที่เรียกดู source code ของ 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.

ในแอปพลิเคชัน Rails มาตรฐาน จะมี Gemfile ที่ระบุความขึ้นอยู่กับแอปพลิเคชัน ไฟล์ config/boot.rb จะตั้งค่า ENV['BUNDLE_GEMFILE'] เพื่อชี้ไปยังตำแหน่งของไฟล์นี้ หากมี Gemfile อยู่ จะต้องระบุ bundler/setup ไว้ คำสั่ง require นี้จะถูกใช้โดย Bundler เพื่อกำหนดค่าเส้นทางการโหลดสำหรับ dependencies ใน 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 (เส้นทางสองระดับขึ้นจาก APP_PATH ซึ่งชี้ไปที่ config/application.rb) แต่เฉพาะกรณีที่ไม่พบไฟล์ 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 จะเรียกใช้เมธอด 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 จะถูกเรียกซึ่งจะเรียกใช้เมธอด initialize ใน Rack::Server

1.7 Rack: lib/rack/server.rb

Rack::Server รับผิดชอบในการให้เซิร์ฟเวอร์อินเตอร์เฟซทั่วไปสำหรับแอปพลิเคชันทั้งหมดที่ใช้ Rack ซึ่งเป็นส่วนหนึ่งของ Rails ตอนนี้

เมื่อเรียกใช้เมธอด initialize ใน Rack::Server จะตั้งค่าตัวแปรหลายตัวดังนี้:

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 ของ Rack::Server เมื่อคำสั่งใน if statement ถูกประเมิน ตัวแปรอินสแตนซ์หลายตัวจะถูกตั้งค่า

เมธอด server_options ใน Rails::Command::ServerCommand ถูกกำหนดดังนี้:

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

หลังจากที่ super สิ้นสุดลงใน Rack::Server เรากลับมาที่ 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 สิ้นสุดลง เรากลับมาที่คำสั่ง server ที่ 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
        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] จะเริ่มต้นด้วย 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

เมธอด initialize ของ Rack::Builder จะใช้บล็อกนี้และดำเนินการด้วยตัวอย่างของ Rack::Builder. นี่คือส่วนสำคัญส่วนใหญ่ของกระบวนการเตรียมการของ Rails บรรทัด require สำหรับ config/environment.rb ใน config.ru เป็นบรรทัดแรกที่รัน:

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 คือ: ruby app = self.app

Now, app is passed to build_app and all the middlewares are called on it. ruby 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 เองเป็นที่ดีที่สุดที่คุณควรไปต่อ

ข้อเสนอแนะ

คุณสามารถช่วยปรับปรุงคุณภาพของคู่มือนี้ได้

กรุณาช่วยเพิ่มเติมหากพบข้อผิดพลาดหรือข้อผิดพลาดทางความจริง เพื่อเริ่มต้นคุณสามารถอ่านส่วน การสนับสนุนเอกสาร ของเราได้

คุณอาจพบเนื้อหาที่ไม่สมบูรณ์หรือเนื้อหาที่ไม่ได้อัปเดต กรุณาเพิ่มเอกสารที่ขาดหายไปสำหรับเนื้อหาหลัก โปรดตรวจสอบ Edge Guides ก่อนเพื่อตรวจสอบ ว่าปัญหาได้รับการแก้ไขหรือไม่ในสาขาหลัก ตรวจสอบ คู่มือแนวทาง Ruby on Rails เพื่อดูรูปแบบและกฎเกณฑ์

หากคุณพบข้อผิดพลาดแต่ไม่สามารถแก้ไขได้เอง กรุณา เปิดปัญหา.

และสุดท้าย การสนทนาใด ๆ เกี่ยวกับ Ruby on Rails เอกสารยินดีต้อนรับที่สุดใน เว็บบอร์ดอย่างเป็นทางการของ Ruby on Rails.