Rails initialization process
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 這個檔案開始
# bin/rails
#!/usr/bin/env ruby
APP_PATH = File.expand_path('../config/application', __dir__)
require_relative "../config/boot"
require "rails/commands"
其中 APP_PATH 等等在 rails/command 會用到
接著是 require config/boot 這檔案
#config/boot.rb
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
require "bundler/setup" # Set up gems listed in the Gemfile.
Bundler 負責確保你可以找到 Gemfile 裡面的所有 gems
下面那一行讓所有你寫在 Gemfile 裡面的 gem 可以在 ruby code 裡面使用
接著要回來看 rails/commands.rb 這檔案
# rails/commands.rb
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 執行
module Rails
module Command
class << self
# full_namespace = 'server'
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) # => 找不到 command 會丟給 rake 執行
end
end
end
end
end
如果 run 的是 rails server 會跑下面的 code
這段 code 還有 load action_dispatch,在這裡會把 Routing / Session 等等 modules 跟一些 middleware 讀進來
# railties/lib/rails/commands/server/server_command.rb
module Rails
module Command
class ServerCommand < Base # :nodoc:
def perform
extract_environment_option_from_argument # <= 把丟進來的參數抽成 env var
set_application_directory! # 如果沒有 config.ru 這個檔案,就去 APP_PATH 的上上層啟動 server,讓我們不管在哪個路徑都可以啟動 server
prepare_restart # <= 這步只是把 pid file 移除
Rails::Server.new(server_options).tap do |server|
# Require application after server sets environment to propagate
# the --environment option.
require APP_PATH # default 是 config/application
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) # <= 啟動 server
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 使用
# railties/lib/rails/commands/server/server_command.rb
module Rails
class Server < ::Rack::Server
def initialize(options = nil)
@default_options = options || {}
super(@default_options)
set_environment # ENV["RAILS_ENV"] ||= options[:environment]
end
...
回到前面的內容
Rails::Server.new(server_options).tap do |server|
# Require application after server sets environment to propagate
# the --environment option.
require APP_PATH # default 是 config/application
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) # <= 啟動 server
else
say rack_server_suggestion(using)
end
end
在 new 完之後會去 require APP_PATH,預設是 config/application.rb
在這之後會 call server.start
module Rails
class Server < ::Rack::Server
def start(after_stop_callback = nil)
trap(:INT) { exit } # 如果 ctrl - C 就會 exit process
create_tmp_directories # 做出 tmp 底下的 cache / pids / sockets 三個 folder
setup_dev_caching # --dev-caching => 這參數會 trigger development 環境 cache
log_to_stdout if options[:log_stdout] # 這步 wrapped_app 會先 create rack app
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
在進 super 之前如果執行 log_to_stdout 會先做出 rack app
module Rack
class Server
def wrapped_app
@wrapped_app ||= build_app app
end
end
end
首先來看 app 會做什麼事
在 Rack 裡面這段 code 大致上是下面這樣
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] # options[:config] 的 default 就是 config.ru 這檔案
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 長這樣:
require_relative "config/environment"
run Rails.application
Rails.application.load_server
第一行就是 require_relative "config/environment"
如果是用別的 app server 的話,像是 Passenger 也會 require config/environment 這檔案,所以前面實作可能不同,但從這裡開始會是一樣的
# config/environment.rb
# Load the Rails application.
require_relative "application"
# Initialize the Rails application.
Rails.application.initialize!
一開始是 require application
#config/application.rb
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
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 這些設定都在上面這裡面定義
# config/application.rb
module MyApp
class Application < Rails::Application
config.load_defaults 6.1
end
end
剩下的這部分就是看自己有沒有客製定義 Rails::Application 的 configuration ,這部分算是把 Rails load 完跟 application namespace 定義完了
接著回到 config/environement.rb
# config/environment.rb
# Load the Rails application.
require_relative "application"
# Initialize the Rails application.
Rails.application.initialize!
Rails.application.initialize! 這句做了什麼?
# railties/lib/rails/application.rb
def initialize!(group = :default) #:nodoc:
raise "Application has been already initialized." if @initialized
run_initializers(group, self)
@initialized = true
self
end
# 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 裡面,會找到所有可以 responsd initializers 這個 method 的 class 的祖先(在 tsort_each 裡面做)
會把這些祖先按照 name 的順序排列,每個送 run 的 message 給他去執行
像是 Rails Engine 就會讓所有的 engine 有 initializers 這個 method,所以這些 engine 都會在這時候啟動
Rails application 有定義 bootstrap, railtie, and finisher initializers:
# railties/lib/rails/application.rb
def initializers # :nodoc:
Bootstrap.initializers_for(self) +
railties_initializers(super) +
Finisher.initializers_for(self)
end
Bootstrap 這邊是 prepare 用的,像是準備 logger
finisher 可能會做一些像是 build middeleware stack 的事情,我們在 initializer 裡面有時候會用到 to_prepare 的 block,這也是在這一部執行
# railties/lib/rails/application/finisher.rb
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 剩下的步驟
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] # <= 剛剛都在這其中有一步是 parse file
super() # <= 現在要來這
# ...
end
Rack Server start 最後一步
server.run wrapped_app, options, &blk
wrapped_app 會 call build_app
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 會做得不一樣