# Rails 應用的初始化過程
本章節介紹了 Rails 4 應用啟動的內部流程,適合有一定經驗的Rails應用開發者閱讀。
通過學習本章節,您會學到如下知識:
* 如何使用 `rails server`;
* Rails應用初始化的時間序列;
* Rails應用啟動過程都用到哪些文件;
* Rails::Server接口的定義和使用;
### Chapters
1. [啟 動 !](#%E5%90%AF-%E5%8A%A8--bang)
* [`railties/bin/rails`](#railties/bin/rails)
* [`railties/lib/rails/app_rails_loader.rb`](#railties/lib/rails/app_rails_loader.rb)
* [`bin/rails`](#bin/rails)
* [`config/boot.rb`](#config/boot.rb)
* [`rails/commands.rb`](#rails/commands.rb)
* [`rails/commands/command_tasks.rb`](#rails/commands/command_tasks.rb)
* [`actionpack/lib/action_dispatch.rb`](#actionpack/lib/action_dispatch.rb)
* [`rails/commands/server.rb`](#rails/commands/server.rb)
* [Rack: `lib/rack/server.rb`](#%E5%90%AF-%E5%8A%A8--bang-rack:-lib/rack/server.rb)
* [`config/application`](#config/application)
* [`Rails::Server#start`](#rails::server#start)
* [`config/environment.rb`](#config/environment.rb)
* [`config/application.rb`](#config/application.rb)
2. [加載 Rails](#%E5%8A%A0%E8%BD%BD-rails)
* [`railties/lib/rails/all.rb`](#railties/lib/rails/all.rb)
* [回到 `config/environment.rb`](#%E5%9B%9E%E5%88%B0-config/environment.rb)
* [`railties/lib/rails/application.rb`](#railties/lib/rails/application.rb)
* [Rack: lib/rack/server.rb](#%E5%8A%A0%E8%BD%BD-rails-rack:-lib/rack/server.rb)
本章節通過介紹一個基于Ruby on Rails框架默認配置的 Rails 4 應用程序啟動過程中的方法調用,詳細介紹了每個調用的細節。通過本章節,我們將了解當你執行`rails server`命令啟動你的Rails應用時,背后究竟都發生了什么。
提示:本章節中的路徑如果沒有特別說明都是指Rails應用程序下的路徑。
提示:如果你想瀏覽Rails的源代碼[sourcecode](https://github.com/rails/rails),強烈建議您使用快捷鍵 `t`快速查找Github中的文件。
### 1 啟 動 !
我們現在準備啟動和初始化一個Rails 應用。 一個Rails 應用經常是以運行命令 `rails console` 或者 `rails server` 開始的。
#### 1.1 `railties/bin/rails`
Rails應用中的 `rails server`命令是Rails應用程序所在文件中的一個Ruby的可執行程序,該程序包含如下操作:
```
version = ">= 0"
load Gem.bin_path('railties', 'rails', version)
```
如果你在Rails 控制臺中使用上述命令,你將會看到載入`railties/bin/rails`這個路徑。作為 `railties/bin/rails.rb`的一部分,包含如下代碼:
```
require "rails/cli"
```
模塊`railties/lib/rails/cli` 會調用`Rails::AppRailsLoader.exec_app_rails`方法.
#### 1.2 `railties/lib/rails/app_rails_loader.rb`
`exec_app_rails`模塊的主要功能是去執行你的Rails應用中`bin/rails`文件夾下的指令。如果當前文件夾下沒有`bin/rails`文件,它會到父級目錄去搜索,直到找到為止(Windows下應該會去搜索環境變量中的路徑),在Rails應用程序目錄下的任意位置(命令行模式下),都可以執行`rails`的命令。
因為`rails server`命令和下面的操作是等價的:
```
$ exec ruby bin/rails server
```
#### 1.3 `bin/rails`
文件`railties/bin/rails`包含如下代碼:
```
#!/usr/bin/env ruby
APP_PATH = File.expand_path('../../config/application', __FILE__)
require_relative '../config/boot'
require 'rails/commands'
```
`APP_PATH`稍后會在`rails/commands`中用到。`config/boot`在這被引用是因為我們的Rails應用中需要`config/boot.rb`文件來載入Bundler,并初始化Bundler的配置。
#### 1.4 `config/boot.rb`
`config/boot.rb` 包含如下代碼:
```
# Set up gems listed in the Gemfile.
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
```
在一個標準的Rails應用中的`Gemfile`文件會配置它的所有依賴項。`config/boot.rb`文件會根據`ENV['BUNDLE_GEMFILE']`中的值來查找`Gemfile`文件的路徑。如果`Gemfile`文件存在,那么`bundler/setup`操作會被執行,Bundler執行該操作是為了配置Gemfile依賴項的加載路徑。
一個標準的Rails應用會包含若干Gem包,特別是下面這些:
* actionmailer
* actionpack
* actionview
* activemodel
* activerecord
* activesupport
* arel
* builder
* bundler
* erubis
* i18n
* mail
* mime-types
* polyglot
* rack
* rack-cache
* rack-mount
* rack-test
* rails
* railties
* rake
* sqlite3
* thor
* treetop
* tzinfo
#### 1.5 `rails/commands.rb`
一旦`config/boot.rb`執行完畢,接下來要引用的是`rails/commands`文件,這個文件于幫助解析別名。在本應用中,`ARGV` 數組包含的 `server`項會被匹配:
```
ARGV << '--help' if ARGV.empty?
aliases = {
"g" => "generate",
"d" => "destroy",
"c" => "console",
"s" => "server",
"db" => "dbconsole",
"r" => "runner"
}
command = ARGV.shift
command = aliases[command] || command
require 'rails/commands/commands_tasks'
Rails::CommandsTasks.new(ARGV).run_command!(command)
```
提示: 如你所見,一個空的ARGV數組將會讓系統顯示相關的幫助項。
如果我們使用`s`縮寫代替 `server`,Rails系統會從`aliases`中查找匹配的命令。
#### 1.6 `rails/commands/command_tasks.rb`
當你鍵入一個錯誤的rails命令,`run_command`函數會拋出一個錯誤信息。如果命令正確,一個與命令同名的方法會被調用。
```
COMMAND_WHITELIST = %(plugin generate destroy console server dbconsole application runner new version help)
def run_command!(command)
command = parse_command(command)
if COMMAND_WHITELIST.include?(command)
send(command)
else
write_error_message(command)
end
end
```
如果執行`server`命令,Rails將會繼續執行下面的代碼:
```
def set_application_directory!
Dir.chdir(File.expand_path('../../', APP_PATH)) unless File.exist?(File.expand_path("config.ru"))
end
def server
set_application_directory!
require_command!("server")
Rails::Server.new.tap do |server|
# We need to require application after the server sets environment,
# otherwise the --environment option given to the server won't propagate.
require APP_PATH
Dir.chdir(Rails.application.root)
server.start
end
end
def require_command!(command)
require "rails/commands/#{command}"
end
```
這個文件將會指向Rails的根目錄(與`APP_PATH`中指向`config/application.rb`不同),但是如果沒找到`config.ru`文件,接下來將需要`rails/commands/server`來創建`Rails::Server`類。
```
require 'fileutils'
require 'optparse'
require 'action_dispatch'
require 'rails'
module Rails
class Server < ::Rack::Server
```
`fileutils` 和 `optparse` 是Ruby標準庫中幫助操作文件和解析選項的函數。
#### 1.7 `actionpack/lib/action_dispatch.rb`
動作分發(Action Dispatch)是Rails框架中的路徑組件。它增強了路徑,會話和中間件的功能。
#### 1.8 `rails/commands/server.rb`
這個文件中定義的`Rails::Server`類是繼承自`Rack::Server`類的。當`Rails::Server.new`被調用時,會在 `rails/commands/server.rb`中調用一個`initialize`方法:
```
def initialize(*)
super
set_environment
end
```
首先,`super`會調用父類`Rack::Server`中的`initialize`方法。
#### 1.9 Rack: `lib/rack/server.rb`
`Rack::Server`會為所有基于Rack的應用提供服務接口,現在它已經是Rails框架的一部分了。
`Rack::Server`中的`initialize` 方法會簡單的設置一對變量:
```
def initialize(options = nil)
@options = options
@app = options[:app] if options && options[:app]
end
```
在這種情況下,`options` 的值是 `nil`,所以在這個方法中相當于什么都沒做。
當`Rack::Server`中的`super`方法執行完畢后。我們回到`rails/commands/server.rb`,此時此刻,`Rails::Server`對象會調用 `set_environment` 方法,這個方法貌似看上去什么也沒干:
```
def set_environment
ENV["RAILS_ENV"] ||= options[:environment]
end
```
事實上,`options`方法在這做了很多事情。`Rack::Server` 中的這個方法定義如下:
```
def options
@options ||= parse_options(ARGV)
end
```
接著`parse_options`方法部分代碼如下:
```
def parse_options(args)
options = default_options
# Don't evaluate CGI ISINDEX parameters.
# http://www.meb.uni-bonn.de/docs/cgi/cl.html
args.clear if ENV.include?("REQUEST_METHOD")
options.merge! opt_parser.parse!(args)
options[:config] = ::File.expand_path(options[:config])
ENV["RACK_ENV"] = options[:environment]
options
end
```
`default_options`方法的代碼如下:
```
def default_options
environment = ENV['RACK_ENV'] || 'development'
default_host = environment == 'development' ? 'localhost' : '0.0.0.0'
{
:environment => environment,
:pid => nil,
:Port => 9292,
:Host => default_host,
:AccessLog => [],
:config => "config.ru"
}
end
```
`ENV`中沒有`REQUEST_METHOD`項,所以我們可以忽略這一行。接下來是已經在 `Rack::Server`被定義好的`opt_parser`方法:
```
def opt_parser
Options.new
end
```
**這個**方法已經在`Rack::Server`被定義過了,但是在`Rails::Server` 使用不同的參數進行了重載。他的 `parse!`方法如下:
```
def parse!(args)
args, options = args.dup, {}
opt_parser = OptionParser.new do |opts|
opts.banner = "Usage: rails server [mongrel, thin, etc] [options]"
opts.on("-p", "--port=port", Integer,
"Runs Rails on the specified port.", "Default: 3000") { |v| options[:Port] = v }
...
```
這個方法為`options`建立一些配置選項,以便給Rails決定如何運行服務提供支持。`initialize`方法執行完畢后。我們將回到`rails/server`目錄下,就是`APP_PATH`中的路徑。
#### 1.10 `config/application`
當`require APP_PATH`操作執行完畢后。`config/application.rb` 被載入了 (重新調用`bin/rails`中的`APP_PATH`), 在你的應用中,你可以根據需求對該文件進行配置。
#### 1.11 `Rails::Server#start`
`config/application`載入后,`server.start`方法被調用了。這個方法定義如下:
```
def start
print_boot_information
trap(:INT) { exit }
create_tmp_directories
log_to_stdout if options[:log_stdout]
super
...
end
private
def print_boot_information
...
puts "=> Run `rails server -h` for more startup options"
...
puts "=> Ctrl-C to shutdown server" unless options[:daemonize]
end
def create_tmp_directories
%w(cache pids sessions 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
Rails.logger.extend(ActiveSupport::Logger.broadcast(console))
end
```
這是Rails初始化過程中的第一次控制臺輸出。這個方法創建了一個`INT`中斷信號,所以當你在服務端控制臺按下`CTRL-C`鍵后,這將終止Server的運行。我們可以看到,它創建了`tmp/cache`,`tmp/pids`, `tmp/sessions`和`tmp/sockets`等目錄。在創建和聲明`ActiveSupport::Logger`之前,會調用 `wrapped_app`方法來創建一個Rake 應用程序。
`super`會調用`Rack::Server.start` 方法,該方法定義如下:
```
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).
wrapped_app
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
```
上述Rails 應用有趣的部分在最后一行,`server.run`方法。它再次調用了`wrapped_app`方法(溫故而知新)。
```
@wrapped_app ||= build_app app
```
這里的`app`方法定義如下:
```
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)
self.options.merge! options
app
end
def build_app_from_string
Rack::Builder.new_from_string(self.options[:builder])
end
```
`options[:config]`中的值默認會從 `config.ru` 中獲取,包含如下代碼:
```
# This file is used by Rack-based servers to start the application.
require ::File.expand_path('../config/environment', __FILE__)
run <%= app_const %>
```
`Rack::Builder.parse_file`方法會從`config.ru`中獲取內容,包含如下代碼:
```
app = new_from_string cfgfile, config
...
def self.new_from_string(builder_script, file="(rackup)")
eval "Rack::Builder.new {\n" + builder_script + "\n}.to_app",
TOPLEVEL_BINDING, file, 0
end
```
`Rack::Builder`中的`initialize`方法會創建一個新的`Rack::Builder`實例,這是Rails應用初始化過程中主要內容。接下來`config.ru`中的`require`項`config/environment.rb`會繼續執行:
```
require ::File.expand_path('../config/environment', __FILE__)
```
#### 1.12 `config/environment.rb`
這是`config.ru` (`rails server`)和信使(Passenger)都要用到的文件,是兩者交流的媒介。之前的操作都是為了創建Rack和Rails。
這個文件是以引用 `config/application.rb`開始的:
```
require File.expand_path('../application', __FILE__)
```
#### 1.13 `config/application.rb`
這個文件需要引用`config/boot.rb`:
```
require File.expand_path('../boot', __FILE__)
```
如果之前在`rails server`中沒有引用上述的依賴項,那么它**將不會**和信使(Passenger)發生聯系。
現在,有趣的部分要開始了!
### 2 加載 Rails
`config/application.rb`中的下一行是這樣的:
```
require 'rails/all'
```
#### 2.1 `railties/lib/rails/all.rb`
本文件中將引用和Rails框架相關的所有內容:
```
require "rails"
%w(
active_record
action_controller
action_view
action_mailer
rails/test_unit
sprockets
).each do |framework|
begin
require "#{framework}/railtie"
rescue LoadError
end
end
```
這樣Rails框架中的所有組件已經準備就緒了。我們將不會深入介紹這些框架的內部細節,不過強烈建議您去探索和發現她們。
現在,我們關心的模塊比如Rails engines,I18n 和 Rails configuration 都已經準備就緒了。
#### 2.2 回到 `config/environment.rb`
`config/application.rb`為`Rails::Application`定義了Rails應用初始化之后所有需要用到的資源。當`config/application.rb` 加載了Rails和命名空間后,我們回到`config/environment.rb`,就是初始化完成的地方。比如我們的應用叫‘blog’,我們將在`rails/application.rb`中調用`Rails.application.initialize!`方法。
#### 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
```
如你所見,一個應用只能初始化一次。初始化器通過在`railties/lib/rails/initializable.rb`中的`run_initializers`方法運行:
```
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`之間。
這些完成后,我們將回到`Rack::Server` 。
#### 2.4 Rack: lib/rack/server.rb
上次我們離開的時候,`app` 方法代碼如下:
```
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)
self.options.merge! options
app
end
def build_app_from_string
Rack::Builder.new_from_string(self.options[:builder])
end
```
此時此刻,`app`是Rails 應用本身(中間件)。接下來就是Rack調用所有的依賴項了(提供支持的中間件):
```
def build_app(app)
middleware[options[:environment]].reverse_each do |middleware|
middleware = middleware.call(self) if middleware.respond_to?(:call)
next unless middleware
klass = middleware.shift
app = klass.new(app, *middleware)
end
app
end
```
必須牢記,`Server#start`最后一行中調用了`build_app`方法(被`wrapped_app`調用)了。接下來我們看看還剩下什么:
```
server.run wrapped_app, options, &blk
```
此時此刻,調用`server.run` 方法將依賴于你所用的Server類型 。比如,如果你的Server是Puma, 那么就會是下面這個結果:
```
...
DEFAULT_OPTIONS = {
:Host => '0.0.0.0',
:Port => 8080,
:Threads => '0:16',
:Verbose => false
}
def self.run(app, options = {})
options = DEFAULT_OPTIONS.merge(options)
if options[:Verbose]
app = Rack::CommonLogger.new(app, STDOUT)
end
if options[:environment]
ENV['RACK_ENV'] = options[:environment].to_s
end
server = ::Puma::Server.new(app)
min, max = options[:Threads].split(':', 2)
puts "Puma #{::Puma::Const::PUMA_VERSION} starting..."
puts "* Min threads: #{min}, max threads: #{max}"
puts "* Environment: #{ENV['RACK_ENV']}"
puts "* Listening on tcp://#{options[:Host]}:#{options[:Port]}"
server.add_tcp_listener options[:Host], options[:Port]
server.min_threads = min
server.max_threads = max
yield server if block_given?
begin
server.run.join
rescue Interrupt
puts "* Gracefully stopping, waiting for requests to finish"
server.stop(true)
puts "* Goodbye!"
end
end
```
我們沒有深入到服務端配置的細節,因為這是我們探索Rails應用初始化過程之旅的終點了。
高層次的閱讀將有助于您提高編寫代碼的水平,成為Rail開發高手。如果你想要知道更多,那么去讀Rails的源代碼將是你的不二選擇。
### 反饋
歡迎幫忙改善指南質量。
如發現任何錯誤,歡迎修正。開始貢獻前,可先行閱讀[貢獻指南:文檔](http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#contributing-to-the-rails-documentation)。
翻譯如有錯誤,深感抱歉,歡迎 [Fork](https://github.com/ruby-china/guides/fork) 修正,或至此處[回報](https://github.com/ruby-china/guides/issues/new)。
文章可能有未完成或過時的內容。請先檢查 [Edge Guides](http://edgeguides.rubyonrails.org) 來確定問題在 master 是否已經修掉了。再上 master 補上缺少的文件。內容參考 [Ruby on Rails 指南準則](ruby_on_rails_guides_guidelines.html)來了解行文風格。
最后,任何關于 Ruby on Rails 文檔的討論,歡迎到 [rubyonrails-docs 郵件群組](http://groups.google.com/group/rubyonrails-docs)。
- Ruby on Rails 指南 (651bba1)
- 入門
- Rails 入門
- 模型
- Active Record 基礎
- Active Record 數據庫遷移
- Active Record 數據驗證
- Active Record 回調
- Active Record 關聯
- Active Record 查詢
- 視圖
- Action View 基礎
- Rails 布局和視圖渲染
- 表單幫助方法
- 控制器
- Action Controller 簡介
- Rails 路由全解
- 深入
- Active Support 核心擴展
- Rails 國際化 API
- Action Mailer 基礎
- Active Job 基礎
- Rails 程序測試指南
- Rails 安全指南
- 調試 Rails 程序
- 設置 Rails 程序
- Rails 命令行
- Rails 緩存簡介
- Asset Pipeline
- 在 Rails 中使用 JavaScript
- 引擎入門
- Rails 應用的初始化過程
- Autoloading and Reloading Constants
- 擴展 Rails
- Rails 插件入門
- Rails on Rack
- 個性化Rails生成器與模板
- Rails應用模版
- 貢獻 Ruby on Rails
- Contributing to Ruby on Rails
- API Documentation Guidelines
- Ruby on Rails Guides Guidelines
- Ruby on Rails 維護方針
- 發布記
- A Guide for Upgrading Ruby on Rails
- Ruby on Rails 4.2 發布記
- Ruby on Rails 4.1 發布記
- Ruby on Rails 4.0 Release Notes
- Ruby on Rails 3.2 Release Notes
- Ruby on Rails 3.1 Release Notes
- Ruby on Rails 3.0 Release Notes
- Ruby on Rails 2.3 Release Notes
- Ruby on Rails 2.2 Release Notes