Ce guide passe en revue chaque appel de méthode nécessaire pour démarrer la pile Ruby on Rails pour une application Rails par défaut, expliquant chaque partie en détail tout au long du processus. Pour ce guide, nous nous concentrerons sur ce qui se passe lorsque vous exécutez bin/rails server
pour démarrer votre application.
NOTE : Les chemins dans ce guide sont relatifs à Rails ou à une application Rails, sauf indication contraire.
CONSEIL : Si vous souhaitez suivre en même temps que vous parcourez le code source de Rails, nous vous recommandons d'utiliser la touche t
pour ouvrir le chercheur de fichiers à l'intérieur de GitHub et trouver rapidement les fichiers.
1 C'est parti !
Commençons par démarrer et initialiser l'application. Une application Rails est généralement lancée en exécutant bin/rails console
ou bin/rails server
.
1.1 bin/rails
Ce fichier est le suivant :
#!/usr/bin/env ruby
APP_PATH = File.expand_path('../config/application', __dir__)
require_relative "../config/boot"
require "rails/commands"
La constante APP_PATH
sera utilisée ultérieurement dans rails/commands
. Le fichier config/boot
référencé ici est le fichier config/boot.rb
de notre application, qui est responsable du chargement de Bundler et de sa configuration.
1.2 config/boot.rb
config/boot.rb
contient :
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
require "bundler/setup" # Configure les gems répertoriées dans le Gemfile.
Dans une application Rails standard, il y a un Gemfile
qui déclare toutes les dépendances de l'application. config/boot.rb
définit ENV['BUNDLE_GEMFILE']
sur l'emplacement de ce fichier. Si le Gemfile
existe, alors bundler/setup
est requis. Le require est utilisé par Bundler pour configurer le chemin de chargement des dépendances de votre Gemfile.
1.3 rails/commands.rb
Une fois que config/boot.rb
est terminé, le fichier suivant requis est rails/commands
, qui aide à étendre les alias. Dans le cas actuel, le tableau ARGV
contient simplement server
qui sera transmis :
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
Si nous avions utilisé s
au lieu de server
, Rails aurait utilisé les aliases
définis ici pour trouver la commande correspondante.
1.4 rails/command.rb
Lorsqu'on tape une commande Rails, invoke
essaie de rechercher une commande pour l'espace de noms donné et exécute la commande si elle est trouvée.
Si Rails ne reconnaît pas la commande, il passe les rênes à Rake pour exécuter une tâche du même nom.
Comme indiqué, Rails::Command
affiche automatiquement la sortie d'aide si l'espace de noms est vide.
module Rails
module Command
class << self
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)
end
end
end
end
end
Avec la commande server
, Rails exécutera ensuite le code suivant :
module Rails
module Command
class ServerCommand < Base # :nodoc:
def perform
extract_environment_option_from_argument
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(using)
end
end
end
end
end
end
Ce fichier se déplacera vers le répertoire racine de Rails (un chemin deux répertoires au-dessus de APP_PATH
qui pointe vers config/application.rb
), mais seulement si le fichier config.ru
n'est pas trouvé. Cela démarre ensuite la classe Rails::Server
.
1.5 actionpack/lib/action_dispatch.rb
Action Dispatch est le composant de routage du framework Rails. Il ajoute des fonctionnalités telles que le routage, la session et les middlewares communs.
1.6 rails/commands/server/server_command.rb
La classe Rails::Server
est définie dans ce fichier en héritant de Rack::Server
. Lorsque Rails::Server.new
est appelé, cela appelle la méthode initialize
dans rails/commands/server/server_command.rb
:
module Rails
class Server < ::Rack::Server
def initialize(options = nil)
@default_options = options || {}
super(@default_options)
set_environment
end
end
end
Tout d'abord, super
est appelé, ce qui appelle la méthode initialize
sur Rack::Server
.
1.7 Rack: lib/rack/server.rb
Rack::Server
est responsable de fournir une interface de serveur commune pour toutes les applications basées sur Rack, dont Rails fait maintenant partie.
La méthode initialize
dans Rack::Server
définit simplement plusieurs variables :
module Rack
class Server
def initialize(options = nil)
@ignore_options = []
if options
@use_default_options = false
@options = options
@app = options[:app] if options[:app]
else
argv = defined?(SPEC_ARGV) ? SPEC_ARGV : ARGV
@use_default_options = true
@options = parse_options(argv)
end
end
end
end
Dans ce cas, la valeur de retour de Rails::Command::ServerCommand#server_options
sera assignée à options
.
Lorsque les lignes à l'intérieur de l'instruction if sont évaluées, plusieurs variables d'instance seront définies.
La méthode server_options
dans Rails::Command::ServerCommand
est définie comme suit :
module Rails
module Command
class ServerCommand
no_commands do
def server_options
{
user_supplied_options: user_supplied_options,
server: 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
end
end
end
La valeur sera assignée à la variable d'instance @options
.
Après que super
ait terminé dans Rack::Server
, nous revenons à rails/commands/server/server_command.rb
. À ce stade, set_environment
est appelé dans le contexte de l'objet Rails::Server
.
module Rails
module Server
def set_environment
ENV["RAILS_ENV"] ||= options[:environment]
end
end
end
Après que initialize
ait terminé, nous revenons à la commande du serveur
où APP_PATH
(qui a été défini précédemment) est requis.
1.8 config/application
Lorsque require APP_PATH
est exécuté, config/application.rb
est chargé (rappelez-vous
que APP_PATH
est défini dans bin/rails
). Ce fichier existe dans votre application
et vous êtes libre de le modifier en fonction de vos besoins.
1.9 Rails::Server#start
Après le chargement de config/application
, server.start
est appelé. Cette méthode est
définie comme ceci :
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]
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
Cette méthode crée un piège pour les signaux INT
, donc si vous appuyez sur CTRL-C
sur le serveur, il quittera le processus.
Comme nous pouvons le voir à partir du code ici, il créera les répertoires tmp/cache
,
tmp/pids
et tmp/sockets
. Il active ensuite le caching en développement
si bin/rails server
est appelé avec --dev-caching
. Enfin, il appelle wrapped_app
qui est
responsable de la création de l'application Rack, avant de créer et d'assigner une instance
de ActiveSupport::Logger
.
La méthode super
appellera Rack::Server.start
qui commence sa définition comme suit :
module Rack
class Server
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).
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, &blk
end
end
end
La partie intéressante pour une application Rails est la dernière ligne, server.run
. Ici, nous rencontrons à nouveau la méthode wrapped_app
, que nous allons explorer plus en détail cette fois-ci (même si elle a été exécutée auparavant et
donc mémorisée à présent).
module Rack
class Server
def wrapped_app
@wrapped_app ||= build_app app
end
end
end
La méthode app
ici est définie comme ceci :
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]
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
La valeur de options[:config]
est par défaut config.ru
qui contient ceci :
# This file is used by Rack-based servers to start the application.
require_relative "config/environment"
run Rails.application
La méthode Rack::Builder.parse_file
ici prend le contenu de ce fichier config.ru
et le parse en utilisant ce code :
module Rack
class Builder
def self.load_file(path, opts = Server::Options.new)
# ...
app = new_from_string cfgfile, config
# ...
end
# ...
def self.new_from_string(builder_script, file = "(rackup)")
eval "Rack::Builder.new {\n" + builder_script + "\n}.to_app",
TOPLEVEL_BINDING, file, 0
end
end
end
La méthode initialize
de Rack::Builder
prendra le bloc ici et l'exécutera dans une instance de Rack::Builder
.
C'est là que se déroule la majeure partie du processus d'initialisation de Rails.
La ligne require
pour config/environment.rb
dans config.ru
est la première à s'exécuter :
require_relative "config/environment"
1.10 config/environment.rb
Ce fichier est le fichier commun requis par config.ru
(bin/rails server
) et Passenger. C'est là que ces deux façons d'exécuter le serveur se rencontrent ; tout ce qui précède ce point a été la configuration de Rack et Rails.
Ce fichier commence par exiger config/application.rb
:
require_relative "application"
1.11 config/application.rb
Ce fichier exige config/boot.rb
:
require_relative "boot"
Mais seulement s'il n'a pas été requis auparavant, ce qui serait le cas dans bin/rails server
mais ne serait pas le cas avec Passenger.
Ensuite, le plaisir commence !
2 Chargement de Rails
La ligne suivante dans config/application.rb
est :
require "rails/all"
2.1 railties/lib/rails/all.rb
Ce fichier est responsable de l'exigence de tous les frameworks individuels de Rails :
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
).each do |railtie|
begin
require railtie
rescue LoadError
end
end
C'est là que tous les frameworks de Rails sont chargés et donc rendus disponibles pour l'application. Nous n'entrerons pas dans les détails de ce qui se passe à l'intérieur de chacun de ces frameworks, mais vous êtes encouragé à essayer et à les explorer par vous-même.
Pour l'instant, gardez simplement à l'esprit que des fonctionnalités communes comme les moteurs Rails, I18n et la configuration Rails sont toutes définies ici.
2.2 Retour à config/environment.rb
Le reste de config/application.rb
définit la configuration pour
Rails::Application
qui sera utilisée une fois que l'application sera entièrement
initialisée. Lorsque config/application.rb
a terminé de charger Rails et a défini
l'espace de noms de l'application, nous revenons à config/environment.rb
. Ici, l'
application est initialisée avec Rails.application.initialize!
, qui est
défini dans rails/application.rb
.
2.3 railties/lib/rails/application.rb
La méthode initialize!
ressemble à ceci :
def initialize!(group = :default) # :nodoc:
raise "Application has been already initialized." if @initialized
run_initializers(group, self)
@initialized = true
self
end
Vous ne pouvez initialiser une application qu'une seule fois. Les initialiseurs de Railtie sont exécutés à l'aide de la méthode run_initializers
qui est définie dans 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
Le code run_initializers
lui-même est délicat. Ce que fait Rails ici, c'est
parcourir tous les ancêtres de classe à la recherche de ceux qui répondent à une
méthode initializers
. Il trie ensuite les ancêtres par nom et les exécute.
Par exemple, la classe Engine
rendra tous les moteurs disponibles en
fournissant une méthode initializers
sur eux.
La classe Rails::Application
, telle que définie dans railties/lib/rails/application.rb
,
définit les initialiseurs bootstrap
, railtie
et finisher
. Les initialiseurs bootstrap
préparent l'application (comme l'initialisation du journal) tandis que les initialiseurs finisher
(comme la construction de la pile de middleware) sont exécutés en dernier. Les initialiseurs railtie
sont les initialiseurs qui ont été définis sur Rails::Application
lui-même et sont exécutés entre les initialiseurs bootstrap
et finishers
.
NOTE : Ne confondez pas les initialiseurs de Railtie en général avec l'initialiseur load_config_initializers
ou ses initialiseurs de configuration associés dans config/initializers
.
Une fois cela fait, nous revenons à Rack::Server
.
2.4 Rack: lib/rack/server.rb
La dernière fois, nous avons laissé la méthode app
en cours de définition :
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]
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
À ce stade, app
est l'application Rails elle-même (un middleware), et ce
qui se passe ensuite, c'est que Rack appellera tous les middlewares fournis :
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
Rappelez-vous, build_app
a été appelé (par wrapped_app
) à la dernière ligne de Rack::Server#start
.
Voici à quoi cela ressemblait lorsque nous avons quitté :
server.run wrapped_app, options, &blk
À ce stade, la mise en œuvre de server.run
dépendra du
serveur que vous utilisez. Par exemple, si vous utilisez Puma, voici à quoi
ressemblerait la méthode run
:
module Rack
module Handler
module Puma
# ...
def self.run(app, options = {})
conf = self.config(app, options)
events = options.delete(:Silent) ? ::Puma::Events.strings : ::Puma::Events.stdio
launcher = ::Puma::Launcher.new(conf, events: events)
yield launcher if block_given?
begin
launcher.run
rescue Interrupt
puts "* Gracefully stopping, waiting for requests to finish"
launcher.stop
puts "* Goodbye!"
end
end
# ...
end
end
end
Nous n'allons pas nous plonger dans la configuration du serveur lui-même, mais ceci est la dernière étape de notre parcours dans le processus d'initialisation de Rails.
Cette vue d'ensemble vous aidera à comprendre quand votre code est exécuté et comment, et en général à devenir un meilleur développeur Rails. Si vous voulez en savoir plus, le code source de Rails lui-même est probablement le meilleur endroit où aller ensuite.
Retour d'information
Vous êtes encouragé à contribuer à l'amélioration de la qualité de ce guide.
Veuillez contribuer si vous trouvez des fautes de frappe ou des erreurs factuelles. Pour commencer, vous pouvez lire notre contribution à la documentation section.
Vous pouvez également trouver du contenu incomplet ou des informations qui ne sont pas à jour. Veuillez ajouter toute documentation manquante pour la version principale. Assurez-vous de vérifier Edge Guides d'abord pour vérifier si les problèmes ont déjà été résolus ou non sur la branche principale. Consultez les Directives des guides Ruby on Rails pour le style et les conventions.
Si pour une raison quelconque vous repérez quelque chose à corriger mais ne pouvez pas le faire vous-même, veuillez ouvrir un problème.
Et enfin, toute discussion concernant la documentation de Ruby on Rails est la bienvenue sur le Forum officiel de Ruby on Rails.