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/commands 中使用
1.config/boot.rb 文件
此文件包含以下内容
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
require "bundler/setup" # Set up gems listed in the Gemfile.
require "bootsnap/setup" # Speed up boot time by caching expensive operations.
-
环境变量【BUNDLE_GEMFILE】 用来制定 bundle 文件路径
-
bundler/setup
在Rails中,`require "bundler/setup"`的目的是 加载Bundler库并设置应用程序的环境。Bundler是一个Ruby的依赖管理器,用于管理应用程序所需的Gem依赖项,require "bundler/setup"会做以下几件事情:-
加载Bundler库:该语句会加载Bundler库,使应用程序能够使用Bundler提供的功能和方法。
- bundle install:
bundle install命令会根据Gemfile文件中定义的依赖关系,安装并管理所需的Gem。它会自动下载并安装Gem,解决版本冲突,并将Gem安装到指定的Gem加载路径中。 - Gem 版本管理:Bundler提供了一些方法来管理Gem的版本。可以使用
gem关键字在Gemfile中指定特定的版本要求,例如gem 'rails', '~> 6.0'表示需要安装6.0版本及其后续的Rails Gem。 - 环境管理:Bundler支持不同的环境,如开发环境、测试环境和生产环境。可以在Gemfile中使用
group关键字来指定特定环境下的Gem依赖项,然后使用bundle install --group <group_name>来安装特定环境的Gem,如某一个gem 只在某一个环境中安装等 - Gem源管理:Bundler允许配置和管理Gem的源。可以在Gemfile中指定Gem的源,也可以使用
bundle config命令来配置默认的Gem源。
- bundle install:
2.设置Gem加载路径:Bundler会根据应用程序的Gemfile配置文件,将Gem的加载路径设置为应用程序的
Gemfile中指定的位置。这样,当在应用程序中使用require语句加载Gem时,Bundler会确保从正确的位置加载Gem文件。3.检查Gem依赖项:Bundler会检查应用程序的Gemfile中列出的Gem依赖项,并确保它们都已安装。如果有任何缺失或版本冲突的Gem,Bundler会抛出相应的错误消息
-
总之,require "bundler/setup"是Rails应用程序中的一步重要操作,它确保了Gem的正确加载和配置,并帮助管理和维护Gem依赖项。
- require "bootsnap/setup"(高版本才有这个 Bootsnap的
1.4.0才有)
Bootsnap是一个用于加速Ruby应用程序启动时间的库,特别是在大型Rails应用程序中。它通过优化和缓存加载路径和加载的Ruby代码来提高应用程序的性能
require "bootsnap/setup"会做以下几件事情:
- 加载Bootsnap库:该语句会加载Bootsnap库,使应用程序能够使用Bootsnap提供的功能和方法。
- 设置加载路径缓存:Bootsnap会缓存Ruby的加载路径,以便在应用程序的后续启动中加快加载速度。它会分析应用程序的加载路径,包括Gem路径和应用程序的自定义路径,并生成相应的缓存文件。这样,下次应用程序启动时,Bootsnap将使用缓存的加载路径,避免重新扫描和计算加载路径。
- 优化Ruby代码加载:Bootsnap通过预编译和缓存Ruby代码来加速加载过程。它会分析应用程序的Ruby代码,并将其预编译为更快的格式,以便在加载时可以更快地执行。这样,应用程序启动时,Bootsnap可以直接加载和执行预编译的代码,而无需重新解析和编译。
2.require "rails/commands"
# frozen_string_literal: true
# 文件路径: railties/lib/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
如果输入的命令使用的是 s 而不是 server,Rails 就会在上面定义的 aliases 散列中查找对应的命令
3.require "rails/command"
# 文件路径:https://github.com/rails/rails/blob/c2a25803ddce92c43342690d7b6198b14c2a15b4/railties/lib/rails/commands/server/server_command.rb
module Rails
class Server < Rackup::Server
class Options
def parse!(args)
Rails::Command::ServerCommand.new([], args).server_options
end
end
def initialize(options = nil)
@default_options = options || {}
super(@default_options)
set_environment
end
def opt_parser
Options.new
end
# 设置环境变量
def set_environment
ENV["RAILS_ENV"] ||= options[:environment]
end
def start(after_stop_callback = nil)
trap(:INT) { exit } # start 方法为 INT 信号创建了一个陷阱,只要在服务器运行时按下 CTRL-C,服务器进程就会退出
create_tmp_directories
setup_dev_caching
log_to_stdout if options[:log_stdout] # 标准输出
super()
ensure
after_stop_callback.call if after_stop_callback
end
def serveable? # :nodoc:
server
true
rescue LoadError, NameError
false
end
def middleware
Hash.new([])
end
def default_options
super.merge(@default_options)
end
def served_url
"#{options[:SSLEnable] ? 'https' : 'http'}://#{options[:Host]}:#{options[:Port]}" unless use_puma?
end
private
#命令时指定了 --dev-caching 参数,在开发环境中启用缓存
def setup_dev_caching
if options[:environment] == "development"
Rails::DevCaching.enable_by_argument(options[:caching])
end
end
# 上述代码会创建 tmp/cache、tmp/pids 和 tmp/sockets 文件夹
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, STDERR, STDOUT)
Rails.logger.extend(ActiveSupport::Logger.broadcast(console))
end
end
def use_puma?
server.to_s == "Rack::Handler::Puma"
end
end
module Command
class ServerCommand < Base # :nodoc:
include EnvironmentArgument
# Hard-coding a bunch of handlers here as we don't have a public way of
# querying them from the Rack::Handler registry.
RACK_SERVERS = %w(cgi fastcgi webrick lsws scgi thin puma unicorn falcon)
RECOMMENDED_SERVER = "puma"
DEFAULT_PORT = 3000
DEFAULT_PIDFILE = "tmp/pids/server.pid"
class_option :port, aliases: "-p", type: :numeric,
desc: "Run Rails on the specified port - defaults to 3000.", banner: :port
class_option :binding, aliases: "-b", type: :string,
desc: "Bind Rails to the specified IP - defaults to 'localhost' in development and '0.0.0.0' in other environments'.",
banner: :IP
class_option :config, aliases: "-c", type: :string, default: "config.ru",
desc: "Use a custom rackup configuration.", banner: :file
class_option :daemon, aliases: "-d", type: :boolean, default: false,
desc: "Run server as a Daemon."
class_option :using, aliases: "-u", type: :string,
desc: "Specify the Rack server used to run the application (thin/puma/webrick).", banner: :name
class_option :pid, aliases: "-P", type: :string,
desc: "Specify the PID file - defaults to #{DEFAULT_PIDFILE}."
class_option :dev_caching, aliases: "-C", type: :boolean, default: nil,
desc: "Specify whether to perform caching in development."
class_option :restart, type: :boolean, default: nil, hide: true
class_option :early_hints, type: :boolean, default: nil, desc: "Enable HTTP/2 early hints."
class_option :log_to_stdout, type: :boolean, default: nil, optional: true,
desc: "Whether to log to stdout. Enabled by default in development when not daemonized."
def initialize(args, local_options, *)
super
@original_options = local_options - %w( --restart )
end
desc "server", "Start the Rails server"
def perform
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(options[:using])
end
end
end
no_commands do
def server_options
{
user_supplied_options: user_supplied_options,
server: options[: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
private
def user_supplied_options
@user_supplied_options ||= begin
# Convert incoming options array to a hash of flags
# ["-p3001", "-C", "--binding", "127.0.0.1"] # => {"-p"=>true, "-C"=>true, "--binding"=>true}
user_flag = {}
@original_options.each do |command|
if command.start_with?("--")
option = command.split("=")[0]
user_flag[option] = true
elsif command =~ /\A(-.)/
user_flag[Regexp.last_match[0]] = true
end
end
# Collect all options that the user has explicitly defined so we can
# differentiate them from defaults
user_supplied_options = []
self.class.class_options.select do |key, option|
if option.aliases.any? { |name| user_flag[name] } || user_flag["--#{option.name}"]
name = option.name.to_sym
case name
when :port
name = :Port
when :binding
name = :Host
when :dev_caching
name = :caching
when :daemonize
name = :daemon
end
user_supplied_options << name
end
end
user_supplied_options << :Host if ENV["HOST"] || ENV["BINDING"]
user_supplied_options << :Port if ENV["PORT"]
user_supplied_options << :pid if ENV["PIDFILE"]
user_supplied_options.uniq
end
end
def port
options[:port] || ENV.fetch("PORT", DEFAULT_PORT).to_i
end
def host
if options[:binding]
options[:binding]
else
default_host = environment == "development" ? "localhost" : "0.0.0.0"
ENV.fetch("BINDING", default_host)
end
end
def environment
options[:environment] || Rails::Command.environment
end
def restart_command
"#{executable} #{@original_options.join(" ")} --restart"
end
def early_hints
options[:early_hints]
end
def log_to_stdout?
options.fetch(:log_to_stdout) do
options[:daemon].blank? && environment == "development"
end
end
def pid
File.expand_path(options[:pid] || ENV.fetch("PIDFILE", DEFAULT_PIDFILE))
end
# 有报错时会执行这个方法
def rack_server_suggestion(server)
if server.nil?
<<~MSG
Could not find a server gem. Maybe you need to add one to the Gemfile?
gem "#{RECOMMENDED_SERVER}"
Run `#{executable} --help` for more options.
MSG
elsif server.in?(RACK_SERVERS)
<<~MSG
Could not load server "#{server}". Maybe you need to the add it to the Gemfile?
gem "#{server}"
Run `#{executable} --help` for more options.
MSG
else
error = CorrectableNameError.new("Could not find server '#{server}'.", server, RACK_SERVERS)
<<~MSG
#{error.detailed_message}
Run `#{executable} --help` for more options.
MSG
end
end
def print_boot_information(server, url)
say <<~MSG
=> Booting #{ActiveSupport::Inflector.demodulize(server)}
=> Rails #{Rails.version} application starting in #{Rails.env} #{url}
=> Run `#{executable} --help` for more startup options
MSG
end
end
end
end
4. Rack:lib/rack/server.rb 文件
# 文件链接地址https://github.com/rack/rack/blob/2-2-stable/lib/rack/server.rb
def start(&block)
if options[:warn]
$-w = true
end
if includes = options[:include]
$LOAD_PATH.unshift(*includes)
end
Array(options[:require]).each do |library|
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). 创建守护进程前,加载 searver 的必要文件
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, &block)
end
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
def wrapped_app
@wrapped_app ||= build_app app
end
def daemonize_app
# Cannot be covered as it forks
# :nocov:
Process.daemon
# :nocov:
end
def app
@app ||= options[:builder] ? build_app_from_string : build_app_and_options_from_config
end
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" # rack 加载的文件 ‘`options[:config]` 的默认值为 `config.ru`,此文件包含如下内容: require_relative "config/environment" run Rails.applicationRails.application.load_server’
}
end
# 解析 config.ru
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
# Rack::Builder 类中
def self.parse_file(config, opts = Server::Options.new)
if config.end_with?('.ru')
return self.load_file(config, opts)
else
require config
app = Object.const_get(::File.basename(config, '.rb').split('_').map(&:capitalize).join(''))
return app, {}
end
end
def self.new_from_string(builder_script, file = "(rackup)")
# We want to build a variant of TOPLEVEL_BINDING with self as a Rack::Builder instance.
# We cannot use instance_eval(String) as that would resolve constants differently.
binding, builder = TOPLEVEL_BINDING.eval('Rack::Builder.new.instance_eval { [binding, self] }')
eval builder_script, binding, file
builder.to_app
end
# `Rack::Builder` 类的 `initialize` 方法会把接收到的代码块在 `Rack::Builder` 类的实例中执行,Rails 初始化过程中的大部分工作都在这一步完成。在 `config.ru` 文件中,加载 `config/environment.rb` 文件的这一行代码首先被执行:
graph TD
rails --> APP_PATH
APP_PATH --> boot
boot --> commands
commands--> 'Rails::Server'
'Rails::Server' --> 'Rack::Server'
'Rack::Server' -->'Start'
二、为什么要用Rack
Rack是一个Ruby的Web服务器接口,它定义了一种简单而通用的方式,用于开发Web应用程序框架、中间件和服务器之间的交互。它可以看作是连接Web服务器和Web应用程序框架之间的胶水层。
以下是Rack的一些主要用途和功能:
-
中间件支持:Rack提供了中间件的概念,允许开发者在Web应用程序的请求和响应处理过程中添加各种处理逻辑,例如日志记录、会话管理、静态文件服务等。中间件可以按顺序串联,构建一个灵活的处理管道。
-
可移植性:通过使用Rack,开发者可以编写与特定Web服务器无关的应用程序代码。这意味着可以在不同的Web服务器上运行相同的应用程序,而不需要更改应用程序的核心逻辑。
-
架构灵活性:Rack采用了简单而灵活的接口规范,使得开发者可以选择合适的Web应用程序框架和服务器组合。无论是使用Ruby on Rails、Sinatra、Hanami还是其他框架,只要它们符合Rack接口规范,就可以与任何Rack兼容的服务器一起使用。
-
中间件生态系统:Rack拥有丰富的中间件生态系统,开发者可以轻松地选择和集成各种中间件,以满足应用程序的需求。这些中间件可以通过简单的配置进行启用和定制。
-
开发和测试:Rack简化了Web应用程序的开发和测试。通过使用Rack提供的测试工具和框架,开发者可以编写自动化测试用例,模拟请求和响应,并对应用程序的行为进行测试。
总之,Rack提供了一个通用的接口,用于连接Web服务器和Web应用程序框架之间的交互。它使得开发者能够以一种简单、可移植和灵活的方式构建Web应用程序,并享受丰富的中间件生态系统和开发工具。
问题: 如何在rails 中添加中间件? 在 Rails 中,可以通过在应用程序的配置文件中添加中间件来注册和使用中间件。以下是向 Rails 应用程序添加中间件的一般步骤:
-
打开应用程序的配置文件
config/application.rb或config/environments/*.rb(具体取决于想要添加中间件的环境)。 -
在配置文件中找到
config.middleware或类似的部分。如果不存在,请根据需要添加该部分。 -
使用
config.middleware.use方法注册中间件。可以使用该方法按顺序注册多个中间件。例如:
config.middleware.use MiddlewareClass
其中 MiddlewareClass 是要添加的中间件类的名称。
- 保存配置文件并重新启动应用程序。
通过上述步骤,可以将中间件添加到 Rails 应用程序中并使其生效。请确保在添加中间件时了解其使用方法和配置选项,并根据需要进行适当的设置。
需要注意的是,有些中间件可能需要在特定的环境配置文件(如 config/environments/development.rb)中添加,以便仅在特定环境中启用。
另外,Rails 还提供了一些内置的中间件,如日志中间件和异常处理中间件,它们已经在默认的配置文件中添加好了,无需手动添加。
总之,通过在配置文件中使用 config.middleware.use 方法,可以向 Rails 应用程序添加中间件并控制其顺序。这为提供了灵活性来自定义和扩展应用程序的请求和响应处理过程。
在 Rails 中,中间件的添加顺序决定了它们在请求和响应处理过程中的执行顺序。中间件按照它们添加到应用程序的顺序进行执行,先添加的中间件先执行,后添加的中间件后执行。
具体而言,当请求到达 Rails 应用程序时,请求将首先进入最先添加的中间件,然后依次通过后续的中间件,最后到达应用程序的路由处理部分。响应的处理过程则与请求相反,从应用程序的路由处理部分开始,然后通过后续的中间件,最后返回给客户端。
以下是一个示例中间件添加的顺序和执行顺序的示意图:
请求进入 响应返回
+------------------+ +------------------+
| Middleware1 | | Middleware1 |
+------------------+ +------------------+
| Middleware2 | | Middleware2 |
+------------------+ => +------------------+
| Middleware3 | | Middleware3 |
+------------------+ +------------------+
| 应用程序处理部分 | | 应用程序处理部分 |
+------------------+ +------------------+
在上述示意图中,首先添加的中间件 Middleware1 将首先处理请求,然后是 Middleware2,最后是 Middleware3。在响应的处理过程中,Middleware3 将首先处理响应,然后是 Middleware2,最后是 Middleware1。
注意,每个中间件都可以在请求和响应处理过程中对请求和响应进行修改、添加额外的功能或处理特定的逻辑。因此,中间件的顺序非常重要,可以影响应用程序的行为和功能。
可以根据需要通过调整中间件的添加顺序来控制它们的执行顺序,以满足特定的需求和业务逻辑。确保了解每个中间件的功能和作用,并根据应用程序的需求进行适当的配置和顺序调整。
参考文献:
github.com/rails/rails…