Vous êtes-vous déjà demandé comment votre application Rails démarre ? Je veux dire, quand vous exécutez le rails server
, que se passe-t-il ?
Pour répondre à cette question, nous allons commencer par générer une nouvelle application Rails 6 (j'utilise actuellement la version 6.0.0.rc1).
$ rails new iluvrails
$ cd iluvrails
./bin/rails
Le point de départ de la séquence de démarrage est l'exécutable rails. Pour simplifier ce blog, nous commencerons notre voyage dans ./bin/rails
. Au fait, qu'est-ce que la gemme rails ? C'est un packaging pour tout ce qui suit :
$ gem dependency rails -v 6.0.0.rc1
Gem rails-6.0.0.rc1
actioncable (= 6.0.0.rc1)
actionmailbox (= 6.0.0.rc1)
actionmailer (= 6.0.0.rc1)
actionpack (= 6.0.0.rc1)
actiontext (= 6.0.0.rc1)
actionview (= 6.0.0.rc1)
activejob (= 6.0.0.rc1)
activemodel (= 6.0.0.rc1)
activerecord (= 6.0.0.rc1)
activestorage (= 6.0.0.rc1)
activesupport (= 6.0.0.rc1)
bundler (>= 1.3.0)
railties (= 6.0.0.rc1)
sprockets-rails (>= 2.0.0)
Le noyau de ce système est railties
.
Rappel rapide :
Rails::Railtie
est le noyau du framework Rails. Il fournit un ensemble de hooks (commeafter_initialize, add_routing_paths ou set_load_path
) pour étendre Rails et/ou modifier le processus d'initialisation.Un railtie est une sous-classe de
Rails::Railtie
qui va étendre Rails. Elle utilise les crochets fournis par les Railties pour se connecter à Rails. En d'autres termes, ce n'est pas Rails qui connaît les autres composants à l'avance et qui les requiert, mais plutôt les composants qui implémentent chacun un railtie et qui s'intègrent à Rails, en faisant savoir à Rails qu'ils sont là.Un moteur est un railtie avec certains initialisateurs déjà définis.
Rails::Application
est un moteur.
Si vous voulez en savoir plus, il n'y a pas de meilleur moyen que de lire le code source :
$ cd `bundle show railties`
$ ls
$ # have fun
Retour à ./bin/rails.
Qu'est-ce qu'il y a dedans ?
#!/usr/bin/env ruby
begin
load File.expand_path('../spring', __FILE__)
rescue LoadError => e
raise unless e.message.include?('spring')
end
APP_PATH = File.expand_path('../config/application', __dir__)
require_relative '../config/boot'
require 'rails/commands'
La partie concernant Spring n'entre pas dans le cadre de ce blog, nous allons donc la passer sous silence. Au cas où vous ne sauriez pas ce que c'est :
Spring est un préchargeur d'applications Rails. Il accélère le développement en maintenant votre application en arrière-plan, de sorte que vous n'avez pas besoin de la démarrer à chaque fois que vous exécutez un test, une tâche rake ou une migration.
APP_PATH = File.expand_path('../config/application', __dir__)
Cela permet de trouver le chemin absolu vers ./config/application.rb
qui définit Iluvrails::Application
(qui hérite de Rails::Application
).c'est à ce moment que nous commençons à relier les points : votre application Rails est une railtie. Elle ne se contente pas d'inclure Rails : elle se branche elle-même sur Rails. Les deux dernières instructions de ./bin/rails
sont :
require_relative '../config/boot'
require 'rails/commands'
Nous allons examiner chacun d'entre eux.
./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.
Tout d'abord, nous définissons ENV['BUNDLE_GEMFILE']
(s'il n'est pas déjà défini) au chemin absolu de notre Gemfile. C'est pour que bundler puisse charger les gems nécessaires plus tard. Ensuite, nous avons besoin de bundler/setup et de bootsnap/setup. bundler/setup
vérifie votre version de ruby, la plate-forme que vous utilisez, s'assure que votre Gemfile et Gemfile.lock correspondent, etc. Bootsnap est un outil qui permet d'accélérer le temps de démarrage d'une application grâce à des opérations de mise en cache. Quant à Spring, c'est hors du champ de ce blog, donc je passe cette partie.
rails/commands
À ce stade, Rails va exécuter la commande que vous lui avez demandée : server. Voici un aperçu de la façon dont cela se passe.
# 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::Command.invoke command, ARGV
Rails va demander rails/command
puis lancer Rails::Command.invoke command, ARGV
qui finira par appeler Rails::Command::ServerCommand.perform
. Jetez un coup d'oeil à son code :
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
Ce qui est le plus intéressant pour nous ici est qu'il:
crée une nouvelle instance de
Rails::Server
qui est une sous-classe deRack::Server
requiert
APP_PATH
, qui pointe vers notre./config/application.rbquires
change le répertoire courant en
Rails.application.root
puis appelle
#start
sur l'instance deRails::Server
.
Rails fournit une interface minimale, modulaire et adaptable pour développer des applications web en Ruby. En enveloppant les requêtes et les réponses HTTP de la manière la plus simple possible, il unifie et distille l'API des serveurs web, des frameworks web et des logiciels intermédiaires (ce qu'on appelle le middleware) en un seul appel de méthode.
À ce stade, si tout s'est bien passé, l'application démarre et vous pouvez y accéder depuis votre navigateur Web.C'est tout. Merci de votre lecture! Humm... pas si vite. N'avons-nous pas lu une déclaration APP_PATH
obligatoire ? Eh bien, voyons ce qui se passe ici.
./config/application.rb
require_relative 'boot'
require 'rails/all'
# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)
module Iluvrails
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 6.0
# Settings in config/environments/* take precedence over those specified here.
# Application configuration can go into files in config/initializers
# -- all .rb files in that directory are automatically loaded after loading
# the framework and any gems in your application.
end
end
Tout d'abord, il y a require_relative 'boot'
. Nous avons déjà demandé ce fichier, donc rien ne se passe à ce stade. Ensuite, nous demandons 'rails/all'
.
# frozen_string_literal: true
# rubocop:disable Style/RedundantBegin
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
sprockets/railtie
).each do |railtie|
begin
require railtie
rescue LoadError
end
end
Ceci requiert le railtie de chaque composant Rails. Si vous n'avez pas besoin de tous ces composants, vous pouvez alléger votre application en supprimant l'instruction require "I want everything" et en ne demandant manuellement que ceux dont vous avez besoin. Vous remplacerez require 'rails/all'
par quelque chose comme ceci :
require "rails"
require "active_record/railtie"
require "active_storage/engine"
require "action_controller/railtie"
require "action_view/railtie"
require "action_mailer/railtie"
require "active_job/railtie"
require "action_cable/engine"
require "action_mailbox/engine"
require "action_text/engine"
# require "rails/test_unit/railtie"
require "sprockets/railtie"
Une fois que vous avez requis les composants de rails, il est temps pour les gems de votre application d'être requis en utilisant bundler :
Bundler.require(*Rails.groups)
Ensuite, nous définissons notre classe d'application :
module Iluvrails
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 6.0
# Settings in config/environments/* take precedence over those specified here.
# Application configuration can go into files in config/initializers
# -- all .rb files in that directory are automatically loaded after loading
# the framework and any gems in your application.
end
end
Ok, nous avons donc défini un railtie, mais il manque encore une pièce au puzzle. Quand les initialisateurs et ./config/environments/#{Rails.env}.rb
sont-ils chargés ? De retour à Rails::Command::ServerCommand.perform
, nous voyons que Rails::Server
est initialisé de la manière suivante : Rails::Server.new(server_options)
et lorsque nous recherchons server_options, nous voyons qu'il s'agit d'un hash avec les valeurs par défaut suivantes :
class_option :config, aliases: "-c", type: :string, default: "config.ru",
desc: "Uses a custom rackup configuration.", banner: :file
# [...]
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
TL;DR : On dit à Rake de charger ./config.ru
./config.ru
# This file is used by Rack-based servers to start the application.
require_relative 'config/environment'
run Rails.application
Ok, suivons cette piste. Nous chargeons d'abord config/environment.rb
:
# Load the Rails application.
require_relative 'application'
# Initialize the Rails application.
Rails.application.initialize!
Ainsi, après avoir demandé ./config/application.rb
(qui est déjà nécessaire à ce stade), #initialize !
est appelé.
# Initialize the application passing the given group. By default, the
# group is :default
def initialize!(group = :default) #:nodoc:
raise "Application has been already initialized." if @initialized
run_initializers(group, self)
@initialized = true
self
end
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
C'est un gros morceau qui mériterait un article de blog à lui tout seul. Sans trop entrer dans les détails, rappelons que Rails est constitué de nombreux hooks. Certains d'entre eux sont liés à l'initialisation. Pendant #run_initializers
sera exécuté parmi d'autres hooks :
load_environment_config
qui charge./config/environments/#{Rails.env}.rb
load_config_initializers
qui charge ./config/initializers/*.rb
Note de clôture
Le code source de Rails::Application
nous donne un rapide rappel du déroulement du processus de démarrage :
1) nécessite "config/boot.rb" pour configurer les chemins de chargement.
2) require railties et engines
3) Définissez Rails.application comme "class MyApp::Application < Rails::Application".
4) Exécuter les callbacks config.before_configuration
5) Chargement de config/environments/ENV.rb
6) Exécuter les callbacks config.before_initialize
7) Exécuter Railtie#initializer défini par railties, moteurs et application. Un par un, chaque moteur configure ses chemins de chargement, ses routes et exécute ses fichiers config/initializers/*.
8) Les initialisateurs Railtie# personnalisés ajoutés par les chemins de fer, les moteurs et les applications sont exécutés.
9) Construction de la pile middleware et exécution des callbacks to_prepare.
10) Exécutez config.before_eager_load et eager_load ! si eager_load est vrai.
11) Exécution des callbacks config.after_initializeSaid differently:
Défini
APP_PATH
to./config/application.rb
Défini
ENV['BUNDLE_GEMFILE']
to./Gemfile
Configuration de Bundler (sans avoir besoin de gems pour le moment)
Initialisation d'un nouveau
Rails::Server
(sous-classe deRack::Server
)Nécessite tous les composants Rails (ActiveRecord, ActionPack, etc.)
Nécessite toutes les gemmes de votre Gemfile
Définissez une
application
qui est une sous-classe deRails::Application
.Changez le répertoire à la racine de votre application Rails.
Lancez le
Rails::Server
initialisé précédemment.Exécutez les hooks Rails de manière ordonnée (chargez la configuration, exécutez les initialisateurs, etc.)
Votre serveur est maintenant en attente de requêtes !
Nous aimons Rails pour toute la magie qu'il opère pour nous, mais il est préférable de comprendre comment cette magie fonctionne.