rails s 经历了什么

231 阅读6分钟

在控制台输入rails s,到最后启动完成rails项目,是一个怎样的过程?

1. rails s 中的命令中的 rails 是位于加载路径中的一个 Ruby 可执行文件,路径railties/exe/rails

截屏2023-03-23 下午3.37.07.png 截屏2023-03-23 下午3.38.09.png

源码如下

# 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等一些服务器了

本次先讲到这里,后续在好好梳理下,不理解的地方再补充优化。