Intro
其實整篇文章,差不多就是 Rails Guide 的翻譯,但要跟著看過一次其實也不是件容易的事,但因為滿想知道啟動過程的,所以就跟著介紹走了一次流程
先說聲抱歉,寫的滿亂的,真的有興趣的還是建議直接看 Rails Guide 可能比較有條理,這篇偏向給自己看的
其中如果有些沒有在 Rails guide 看到,就是我自己爬到或者去看其他文章的
Files order
這裡大致列上啟動過程中 load 檔案的順序
bin/rails
config/boot.rb
rails/commands.rb
rails/command.rb(source code)
actionpack/lib/action_dispatch.rb
rails/commands/server/server_command.rb
config/application.rb
Rails::Server#start
config.ru
config/environment.rb
config/application.rb
railties/lib/rails/application.rb
lib/rack/server.rb
Process
首先我們通常都用 rails 這指令開始,所以從 bin/rails
這個檔案開始
1 2 3 4 5 APP_PATH = File.expand_path('../config/application' , __dir__ ) require_relative "../config/boot" require "rails/commands"
其中 APP_PATH
等等在 rails/command 會用到
接著是 require config/boot
這檔案
1 2 3 4 ENV['BUNDLE_GEMFILE' ] || = File.expand_path('../Gemfile' , __dir__ ) require "bundler/setup"
Bundler 負責確保你可以找到 Gemfile 裡面的所有 gems
下面那一行讓所有你寫在 Gemfile 裡面的 gem 可以在 ruby code 裡面使用
接著要回來看 rails/commands.rb
這檔案
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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
最後是用 Rails::Command
invoke,來看看這裡的 code(有精簡過)
如果找不到這個 command 會試圖丟給 rake 執行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 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
如果 run 的是 rails server 會跑下面的 code
這段 code 還有 load action_dispatch,在這裡會把 Routing / Session 等等 modules 跟一些 middleware 讀進來
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 module Rails module Command class ServerCommand < Base def perform extract_environment_option_from_argument set_application_directory! prepare_restart Rails::Server.new(server_options).tap do |server| 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
這裡的 server_options 裡面包括 port / host /config / environment 等等常見的 variable
Rails::Server 寫在同一個地方,這裡有去 call ::Rack::Server
的 initialize,但那裡面也只是設定其他的一些 variable
::Rack::Server
主要負責提供一個 common interface 給所有的 Rack-based application 使用
1 2 3 4 5 6 7 8 9 module Rails class Server < ::Rack::Server def initialize (options = nil ) @default_options = options || {} super (@default_options) set_environment end ...
回到前面的內容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 Rails::Server.new(server_options).tap do |server| 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
在 new 完之後會去 require APP_PATH
,預設是 config/application.rb
在這之後會 call server.start
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 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 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
在進 super 之前如果執行 log_to_stdout 會先做出 rack app
1 2 3 4 5 6 7 module Rack class Server def wrapped_app @wrapped_app || = build_app app end end end
首先來看 app
會做什麼事
在 Rack 裡面這段 code 大致上是下面這樣
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 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] 的 default 就是 config.ru
這檔案
下面又可以知道 Rack::Builder.parse_file(self.options[:config], opt_parser)
第一個回傳值是一個 app instance
所以就是透過 config.ru 這個檔案為主要起點來 initialize Rails app
config.ru 裡面 deafult 長這樣:
1 2 3 4 require_relative "config/environment" run Rails.application Rails.application.load_server
第一行就是 require_relative "config/environment"
如果是用別的 app server 的話,像是 Passenger 也會 require config/environment
這檔案,所以前面實作可能不同,但從這裡開始會是一樣的
1 2 3 4 5 6 7 require_relative "application" Rails.application.initialize!
一開始是 require application
1 2 3 4 5 6 7 8 9 10 11 12 13 require_relative "boot" require "rails/all" Bundler.require (*Rails.groups) Dotenv::Railtie.load module Homework class Application < Rails::Application config.load_defaults 6.1 end end
require_relative "boot"
這一行如果是直接 run rails s
不會有用,因為一開始已經 require 過了,但像是 passenger 這種 app server 沒有 require 過,就會去把 gem require 進來
require "rails/all"
這行會把 rails 的一些 framework 都載進來,也就是真的開始 load Rails 的 code 了
Rails 真正的核心是 railties
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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 engines, I18n and Rails configuration 這些設定都在上面這裡面定義
1 2 3 4 5 6 module MyApp class Application < Rails::Application config.load_defaults 6.1 end end
剩下的這部分就是看自己有沒有客製定義 Rails::Application 的 configuration ,這部分算是把 Rails load 完跟 application namespace 定義完了
接著回到 config/environement.rb
1 2 3 4 5 6 7 require_relative "application" Rails.application.initialize!
Rails.application.initialize! 這句做了什麼?
1 2 3 4 5 6 7 def initialize! (group = :default ) raise "Application has been already initialized." if @initialized run_initializers(group, self ) @initialized = true self end
1 2 3 4 5 6 7 8 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 裡面,會找到所有可以 responsd initializers
這個 method 的 class 的祖先(在 tsort_each 裡面做)
會把這些祖先按照 name 的順序排列,每個送 run
的 message 給他去執行
像是 Rails Engine 就會讓所有的 engine 有 initializers 這個 method,所以這些 engine 都會在這時候啟動
Rails application 有定義 bootstrap, railtie, and finisher initializers:
1 2 3 4 5 6 7 def initializers Bootstrap.initializers_for(self ) + railties_initializers(super ) + Finisher.initializers_for(self ) end
Bootstrap 這邊是 prepare 用的,像是準備 logger
finisher 可能會做一些像是 build middeleware stack 的事情,我們在 initializer 裡面有時候會用到 to_prepare
的 block,這也是在這一部執行
1 2 3 4 5 6 initializer :add_to_prepare_blocks do |app| config.to_prepare_blocks.each do |block| app.reloader.to_prepare(&block) end end
要特別注意這裡的 initailizer 並不是我們寫在 config/initiailizers
裡面的那些!
剛剛都在 server.start 裡面的步驟,終於把寫的 config parse 結束,回到 start 剩下的步驟
1 2 3 4 5 6 7 8 9 10 11 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
Rack Server start 最後一步
server.run wrapped_app, options, &blk
wrapped_app
會 call build_app
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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 call 所有的 middlewares
接著 server 怎麼 run 就要根據不同 server 的實作,像是 puma / passenger 會做得不一樣
References
Rails guide
Bundler docs
鐵人賽文章