在控制台输入rails s,到最后启动完成rails项目,是一个怎样的过程?
1. rails s 中的命令中的 rails 是位于加载路径中的一个 Ruby 可执行文件,路径railties/exe/rails
源码如下
# File.expand_path # 获取相对路径对应的绝对路径
# $: $LOAD_PATH 指的是Ruby读取外部文件的一个环境变量
# 扩展路径
git_path = File.expand_path("../../.git", __dir__)
# 如果存在.git文件夹
if File.exist?(git_path)
# 扩展路径
railties_path = File.expand_path("../lib", __dir__)
# 添加到$LOAD_PATH中
$:.unshift(railties_path)
end
# 引入rails/cli
require "rails/cli"
2. cli.rb模块代码
require "rails/cli"后执行
# frozen_string_literal: true
# 加载app_loader 模块
require "rails/app_loader"
# If we are inside a Rails application this method performs an exec and thus
# the rest of this script is not run.
# 执行 exec_app 方法启动应用,否则进入新建 app 流程。
Rails::AppLoader.exec_app
# ruby 版本检查
require "rails/ruby_version_check"
Signal.trap("INT") { puts; exit(1) }
require "rails/command"
# 如果第一个参数是“plugin”,则使用剩余的参数调用插件命令。
if ARGV.first == "plugin"
ARGV.shift
Rails::Command.invoke :plugin, ARGV
else
# 否则,使用参数调用应用程序命令。
Rails::Command.invoke :application, ARGV
end
3. app_loader.rb 模块
exec_app 方法的主要作用是执行应用中的 bin/rails 文件。如果在当前文件夹中未找到 bin/rails 文件,就会继续在上层文件夹中查找,直到找到为止。因此,我们可以在 Rails 应用中的任何位置执行 rails 命令。如果找到,并且文件中设置了 APP_PATH,则执行该文件;如果只找到了来文件没有APP_PATH,则会重新配置APP_PATH后继续执行
# EXECUTABLES = ["bin/rails", "script/rails"]
loop do
if exe = find_executable
contents = File.read(exe)
if /(APP|ENGINE)_PATH/.match?(contents)
exec RUBY, exe, *ARGV
break # non reachable, hack to be able to stub exec in the test suite
elsif exe.end_with?("bin/rails") && contents.include?("This file was generated by Bundler")
$stderr.puts(BUNDLER_WARNING)
Object.const_set(:APP_PATH, File.expand_path("config/application", Dir.pwd))
require File.expand_path("../boot", APP_PATH)
require "rails/commands"
break
end
end
# If we exhaust the search there is no executable, this could be a
# call to generate a new application, so restore the original cwd.
Dir.chdir(original_cwd) && return if Pathname.new(Dir.pwd).root?
# Otherwise keep moving upwards in search of an executable.
Dir.chdir("..")
end
end
#exec RUBY, exe, *ARGV
等价于命令行 $ exec ruby bin/rails server
4. bin/rails 可执行文件内容
# 加载 spring,Rails Spring是一个应用程序,它可以在Rails应用程序的开发过程中提高开发人员
# 的生产力。它通过在后台运行Rails应用程序,使其在开发过程中更快地启动和运行。这样,
# 开发人员可以更快地进行测试和调试,从而更快地完成开发任务。
begin
load File.expand_path('../spring', __FILE__)
rescue LoadError => e
raise unless e.message.include?('spring')
end
# 设置 APP_PATH,将在后面的rails/commands 中使用
APP_PATH = File.expand_path('../config/application', __dir__)
# 引入必要的文件
# 用于加载并设置Bundler
require_relative '../config/boot'
require 'rails/commands'
5. config/boot.rb 文件源码
# ENV['BUNDLE_GEMFILE'] 设置为 Gemfile 文件路径
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
# 根据 Gemfile 中的依赖关系的加载gem
require 'bundler/setup' # 设置 Gemfile 中的 gem
# 它通过优化rails启动时的处理(缓存路径和ruby编译结果)来缩短启动时间
require 'bootsnap/setup'
6. rails/commands.rb 将要执行命令了
执行完boot文件,下一步就是要执行rails/commands,如果是缩写参数,将会给你转化成扩展别名。rails s => rails server,
# frozen_string_literal: true
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 s 为例
# command => server,ARGV => ''
Rails::Command.invoke command, ARGV
7. 进入rails/command.rb 文件
部分源码,invoke 会尝试查找指定命名空间下的命令perform,如果没找到命令,就会委托rake执行同名perform任务,当前面command 为空的时候,Rails::Command会自动显示帮助信息
# Receives a namespace, arguments and the behavior to invoke the command.
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)
# raills s
# command : Rails::Command::ServerCommand
# args : ''
# config : {}
if command && command.all_commands[command_name]
command.perform(command_name, args, config)
else
#invoke 会尝试查找指定命名空间下的命令,如果没找到命令,就会委托rake执行同名任务
find_by_namespace("rake").perform(full_namespace, args, config)
end
end
8. rails/commands/server/server_command.rb 文件
def perform
# 从参数中提取环境配置
extract_environment_option_from_argument
# 设置应用程序目录,当config.ru 不存在时
set_application_directory!
# 准备重启,配置项中有options[:restart]
prepare_restart
Rails::Server.new(server_options).tap do |server|
# 服务器设置环境后,要求应用程序以传播--environment选项。
# 执行require APP_PATH时,会加载config/application.rb文件
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
# 如果无法提供服务,则建议使用rack服务器
say rack_server_suggestion(using)
end
end
end
文件中Rails::Server继承自Rack::Server,当调用Rails::Server.new,就会调用自己的和父类的initialize方法,初始化配置信息。端口、pid、缓存、host等信息的配置
def initialize(options = nil)
@default_options = options || {}
super(@default_options)
set_environment
end
# 设置当前 RAILS_ENV
def set_environment
ENV["RAILS_ENV"] ||= options[:environment]
end
9. Rails::Server#start
INT信号创建了一个监听,只要在服务区运行时按下CTRL-C,服务器就会退出。
创建了tmp目录和相应的文件夹。
如果在运行rails server 命令时指定了 --dev-caching 参数,在开发环境启用缓存。
log_to_stdout 中会调用wrapped_app 方法,其作用是先创建rack应用,在创建ActiveSupport::Logger 类的实例
最后还会调用父类的super,Rack::Server#start
def start(after_stop_callback = nil)
# 退出程序
trap(:INT) { exit }
# 创建临时目录
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 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
10. Rack::Server.start
def start(&block)
# 如果设置了警告选项,则开启警告
if options[:warn]
$-w = true
end
# 如果设置了包含路径,则将其添加到 $LOAD_PATH 中
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
# 检查 PID 文件是否存在
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,以便在守护进程化之前加载 config.ru
handle_profiling(options[:heapfile], options[:profile_mode], options[:profile_file]) do
# 加载中间件,生成rack对象
wrapped_app
end
# 如果设置了守护进程化选项,则守护进程化应用
daemonize_app if options[:daemonize]
# 写入 PID 文件
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
wrapped_app方法
@wrapped_app ||= build_app app
build_app 加载各种中间件,需要等前面 app 全部调用完成
app 有config.ru时返回一个 Rack::Builder对象
def app
@app ||= options[:builder] ? build_app_from_string : build_app_and_options_from_config
end
# options[:config] 的默认值为 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.parse_file 会加载读取config.ru 文件
# Evaluate the given +builder_script+ string in the context of
# a Rack::Builder block, returning a Rack application.
# 将给定的 +builder_script+ 字符串在 Rack::Builder 块的上下文中求值,返回 Rack 应用程序。
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
eval 可以把字符串当做代码来执行,并返回结果;第二个参数如果是 Proc 对象或 Binding 对象,将在该对象保存的执行环境(作用域)中对字符串解析执行。 这样,就可以通过binding改变 eval 的执行上下文
11. config.ru 文件
# This file is used by Rack-based servers to start the application.
require_relative 'config/environment'
run Rails.application
12. config/environment.rb
加载rails程序和初始化rails程序()
# Load the Rails application.
require_relative 'application'
# Initialize the Rails application.
Rails.application.initialize!
app 就是rails应用本身这个中间件,然后继续执行前面的build_app(app,
在 Server#start 方法定义的最后一行代码中,通过 wrapped_app 方法调用了 build_app 方法。
server.run wrapped_app, options, &blk
后续就是puma等一些服务器了
本次先讲到这里,后续在好好梳理下,不理解的地方再补充优化。