
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 iluvrailsLe 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 (comme after_initialize, add_routing_paths ou set_load_path) pour étendre Rails et/ou modifier le processus d'initialisation.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à.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 funRetour à ./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.
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.
À 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, ARGVRails 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
endCe qui est le plus intéressant pour nous ici est qu'il:
Rails::Server qui est une sous-classe de Rack::ServerAPP_PATH, qui pointe vers notre ./config/application.rbquires Rails.application.root#start sur l'instance de Rails::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.
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
endTout 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
endCeci 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
endOk, 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
}
endTL;DR : On dit à Rake de charger ./config.ru
# This file is used by Rack-based servers to start the application.
require_relative 'config/environment'
run Rails.applicationOk, suivons cette piste. Nous chargeons d'abordconfig/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
endC'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}.rbload_config_initializers qui charge ./config/initializers/*.rbLe 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:
APP_PATH to ./config/application.rbENV['BUNDLE_GEMFILE'] to ./GemfileRails::Server (sous-classe de Rack::Server)application qui est une sous-classe de Rails::Application.Rails::Server initialisé précédemment.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.