عملية تهيئة ريلز

من موسوعة حسوب

يشرح هذا الدليل العمل الداخلي لعملية التهيئة في ريلز. وهو دليل متعمق للغاية وموصى به لمطوري ريلز المتقدمّين.

ستتعلم بعد قراءة هذا الدليل:

  • كيفيّة استخدام خادم ريلز (rails server).
  • التسلسل الزمني لتهيئة ريلز.
  • أين تُطلَب الملفّات المختلفة أثناء تسلسل التمهيد.
  • كيفيّة تعريف واجهة Rails::Server واستخدامها.

يتضمّن هذا الدليل كل استدعاء لتابع مطلوب لتشغيل مكّدس Ruby on Rails (أي stack) لتطبيق ريلز افتراضي، مع شرح كل جزء بالتفصيل. بالنسبة لهذا الدليل، سنركّز على ما يحدث عند تنفيذ rails server لبدء إقلاع (boot) تطبيقك.

المسارات في هذا الدليل نسبيّة إلى Rails أو تطبيقات Rails ما لم يذكر خلاف ذلك.

نوصيك باستخدام المفتاح t المربوط لفتح الباحث عن الملف داخل GitHub والعثور على الملفّات بسرعة إن أردت المتابعة أثناء تصفح شيفرة ريلز المصدرية.

إطلاق!

لنبدأ بالإقلاع وتهيئة التطبيق. يبدأ تطبيق ريلز عادةً بتشغيل rails console rails server أو rails server.

الملف railties/exe/rails

يحتوي هذا الملف على ملفّ روبي قابل للتنفيذ في مسار تحميلك. ويحتوي على الأسطر التالية:

version = ">= 0"
load Gem.bin_path('railties', 'rails', version)

سترى إن جرّبت هذا الأمر في وحدة تحكّم ريلز أنه يحمّل الملف railties/exe/rails. يحتوي جزء من ملف railties/exe/rails.rb على الشفرة التالية:

require "rails/cli"

يستدعي الملف railties/lib/rails/cli بدوره Rails::AppLoader.exec_app.

الملف railties/lib/rails/app_loader.rb

الهدف الأساسي من الدالّة exec_app هو تنفيذ bin/rails لتطبيقك. إن لم يحتوي المجلّد الحالي على bin/rails، فسيتنقل إلى الأعلى حتى يعثر على ملف bin/rails للتنفيذ. وبالتالي يمكن للمرء استدعاء الأمر rails من أي مكان داخل تطبيق ريلز.

بالنسبة للأمر rails server، يُنفّذ الأمر المكافئ للأمر التالي:

$ exec ruby bin/rails server

bin/rails

خذ الملف التالي على سبيل المثال:

#!/usr/bin/env ruby
APP_PATH = File.expand_path('../config/application', __dir__)
require_relative '../config/boot'
require 'rails/commands'

سيُستخدم الثابت APP_PATH لاحقًا في rails/commands. الملف config/boot المشار إليه هنا هو الملف config/boot.rb في تطبيقنا وهو المسؤول عن تحميل Bundler وإعداده.

الملف config/boot.rb

يحتوي الملف config/boot.rb على:

ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
 
require 'bundler/setup' # Set up gems listed in the Gemfile.

في تطبيق ريلز قياسي، يوجد الملف Gemfile الذي يعلن عن كل اعتماديّات (dependencies) التطبيق. يضبط الملف config/boot.rb المتغيّر ENV['BUNDLE_GEMFILE']‎ إلى موقع هذا الملف. إن وُجد Gemfile، يصبح وجود bundler/setup ضروريًّا.

يعتمد أي تطبيق قياسي في ريلز على العديد من الجواهر (gems) تحديدًا:

  • actioncable
  • actionmailer
  • actionpack
  • actionview
  • activejob
  • activemodel
  • activerecord
  • activestorage
  • activesupport
  • arel
  • builder
  • bundler
  • erubi
  • i18n
  • mail
  • mime-types
  • rack
  • rack-test
  • rails
  • railties
  • rake
  • sqlite3
  • thor
  • tzinfo

الملف rails/commands.rb

بمجرّد الانتهاء من الملف config/boot.rb، الملف المطلوب التالي هو rails/commands الذي يساعد في توسيع الأسماء البديلة. تحتوي المصفوفة ARGV في الحالة الحاليّة على الخادم server الذي سيُمرّر:

require_relative "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

كانت ريلز لتستخدم الأسماء البديلة aliases لو استخدمنا s بدلاً من server هنا للعثور على الأمر المطابق.

الملف rails/command.rb

عندما يكتب الشخص أمر ريلز، يحاول invoke البحث عن أمر لمجال الاسم المحدد وتنفيذ الأمر إن عثر عليه.

تسلّم ريلز زمام الأمور إلى Rake إن لم يتعرّف على الأمر لتشغيل مهمة بنفس الاسم.

يعرض Rails::Command كما هو موضّح إخراج (output) المساعدة تلقائيًا إن كانت args فارغة.

module Rails::Command
  class << self
    def invoke(namespace, args = [], **config)
      namespace = namespace.to_s
      namespace = "help" if namespace.blank? || HELP_MAPPINGS.include?(namespace)
      namespace = "version" if %w( -v --version ).include? namespace
 
      if command = find_by_namespace(namespace)
        command.perform(namespace, args, config)
      else
        find_by_namespace("rake").perform(namespace, args, config)
      end
    end
  end
end

باستخدام الأمر server، ستعمل ريلز أيضًا على تشغيل الشيفرة التالية:

module Rails
  module Command
    class ServerCommand < Base # :nodoc:
      def perform
        set_application_directory!
 
        Rails::Server.new.tap do |server|
          # Require application after server sets environment to propagate
          # the --environment option.
          require APP_PATH
          Dir.chdir(Rails.application.root)
          server.start
        end
      end
    end
  end
end

سيتغيّر هذا الملف ليصبح المجلّد الجذر لريلز (مسار فوق APP_PATH بمُجلّدين الذي يشير إلى config/application.rb) ولكن فقط إن لم يُعثر على الملف config.ru. يبدأ بعدئذٍ الصنف Rails::Server.

الملف actionpack/lib/action_dispatch.rb

إرسال الفعل (Action Dispatch) هو مكوّن التوجيه في إطار ريلز. ويضيف وظائف مثل التوجيه (routing)، والجلسات، والبرمجيّات الوسيطة المشتركة.

الملف rails/commands/server/server_command.rb

يُعرّف الصنف Rails::Server في هذا الملف بالوراثة من Rack::Server. عند استدعاء Rails::Server.new، يُستدعى التابع initialize في rails/commands/server/server_command.rb:

def initialize(*)
  super
  set_environment
end

أولًا، يُستدعَى super والذي يستدعي بدوره التابع initialize على Rack::Server.

الملف Rack: lib/rack/server.rb

الصنف Rack::Server هو المسؤول عن عن توفير واجهة خادم مشتركة لكل التطبيقات القائمة على Rack والتي أصبحت ريلز جزءًا منها الآن.

التابع initialize في Rack::Server يُعيّن زوجين من المتغيرات:

def initialize(options = nil)
  @options = options
  @app = options[:app] if options && options[:app]
end

ستكون options في هذه الحالة هي nil، لذا لا يحدث شيء بهذا التابع. نقفز بعد الانتهاء من super في Rack::Server إلى rails/commands/server/server_command.rb. في هذه النقطة، تُستدعى set_environment داخل سياق الكائن Rails::Server ولا يبدو أن هذا التابع يعمل بالوهلة الأولى:

def set_environment
  ENV["RAILS_ENV"] ||= options[:environment]
end

في الواقع يفعل التابع options هنا الكثير. يُعرّف هذا التابع في Rack::Server كما يلي:

def options
  @options ||= parse_options(ARGV)
end

يُعرّف parse_options كما يلي:

def parse_options(args)
  options = default_options
 
  # Don't evaluate CGI ISINDEX parameters.
  # http://www.meb.uni-bonn.de/docs/cgi/cl.html
  args.clear if ENV.include?("REQUEST_METHOD")
 
  options.merge! opt_parser.parse!(args)
  options[:config] = ::File.expand_path(options[:config])
  ENV["RACK_ENV"] = options[:environment]
  options
end

مع تعيين هذه القيم في default_options:

def default_options
  super.merge(
    Port:               ENV.fetch("PORT", 3000).to_i,
    Host:               ENV.fetch("HOST", "localhost").dup,
    DoNotReverseLookup: true,
    environment:        (ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development").dup,
    daemonize:          false,
    caching:            nil,
    pid:                Options::DEFAULT_PID_PATH,
    restart_cmd:        restart_command)
end

لا يوجد المفتاح REQUEST_METHOD في ENV، لذا نستطيع تخطي ذاك السطر. يدمج السطر التالي الخيارات من opt_parser المُعرّف بوضوح في Rack::Server:

def opt_parser
  Options.new
end

يُعرّف الصنف في Rack::Server ولكن يُعاد تعريفه في Rails::Server لتتّخذ وسائط مختلفة. تابعها !parse يبدو كالتالي:

def parse!(args)
  args, options = args.dup, {}
 
  option_parser(options).parse! args
 
  options[:log_stdout] = options[:daemonize].blank? && (options[:environment] || Rails.env) == "development"
  options[:server]     = args.shift
  options
end

سيعيد هذا التابع المفاتيح options التي ستتمكّن ريلز بعدها من استخدامها لتحديد كيفيّة عمل خادمها. نقفز بعد الانتهاء من initialize مرة أخرى إلى أمر الخادم حيث يكون APP_PATH (الذي عُيّن سابقًا) مطلوبًا.

config/application

عند تنفيذ require APP_PATH يُحمّل الملف config/application.rb (تذكّر أن APP_PATH معرّف في bin/rails). يوجد هذا الملف بتطبيقك ويمكنك تغييره حسب احتياجاتك.

Rails::Server#start

بعد تحميل config/application، يستدعى server.start. يُعرّف هذا التابع كما يلي:

def start
  print_boot_information
  trap(:INT) { exit }
  create_tmp_directories
  setup_dev_caching
  log_to_stdout if options[:log_stdout]
 
  super
  ...
end
 
private
  def print_boot_information
    ...
    puts "=> Run `rails server -h` for more startup options"
  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 setup_dev_caching
    if options[:environment] == "development"
      Rails::DevCaching.enable_by_argument(options[:caching])
    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

هذا مكان حدوث أوّل إخراج في تهيئة ريلز. ينشئ هذا التابع فخّا للإشارات INT، لذلك إن ضغطت CTRL-C بالخادم سيخرج من العملية. كما يمكننا أن نرى من الشيفرة هنا أنها ستُنشئ المجلّدات tmp/cache و tmp/pids و tmp/sockets. يُفعّل بعدها التخزين المؤقت في التطوير إن استدعي rails server مع dev-caching--. وأخيرًا، يستدعي wrapped_app المسئول عن إنشاء تطبيق Rack، قبل إنشاء وتعيين نسخة لـ ActiveSupport::Logger. التابع super سيستدعي Rack::Server.start الذي يبدأ تعريفه هكذا:

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).
  wrapped_app
 
  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

الجزء المثير للاهتمام بتطبيق ريلز هو السطر الأخير أي server.run. هنا نواجه التابع wrapped_app مرةً أخرى لكن سنستكشفه هذه المرة أكثر (على الرغم من أنه نُفّذ من قبل، وبالتالي كُتب في الذاكرة الآن).

@wrapped_app ||= build_app app

يُعرّف التابع app هنا على النحو التالي:

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)
    self.options.merge! options
    app
  end
 
  def build_app_from_string
    Rack::Builder.new_from_string(self.options[:builder])
  end

قيم options[:config]‎الافتراضية هي config.ru والتي تحتوي على:

‎‎# لبدء تشغيل التطبيق Rack هذا الملف يُستخدَم من طرف الخوادم التي تعتمد على
 
require_relative 'config/environment'
run <%= app_const %>

يأخذ التابع Rack::Builder.parse_file هنا المحتوى من الملف config.ru ويحلله باستخدام هذه الشفرة:

app = new_from_string cfgfile, config
 
...
 
def self.new_from_string(builder_script, file="(rackup)")
  eval "Rack::Builder.new {\n" + builder_script + "\n}.to_app",
    TOPLEVEL_BINDING, file, 0
end

سيأخذ التابع initialize الذي يخص الصنف Rack::Builder الكتلة (block) هنا وينفّذها داخل نسخة من Rack::Builder. هذا مكان حدوث أغلبيّة عمليّة التهيئة لريلز. وأوّل ما ينفّذ هو السطر require من أجل config/environment.rb في config.ru:

require_relative 'config/environment'

الملف config/environment.rb

هذا الملف ملفٌ مشتركٌ ومطلوبٌ من قبل config.ru (rails server)‎ و Passenger. هنا تتقابل هاتان الطريقتان لتشغيل الخادم؛ كل شيء قبل هذه النقطة كان إعداد Rack و Rails.

يبدأ هذا الملف أوّلًا مع طلب config/application.rb:

require_relative 'application'

الملف config/application.rb

هذا يتطلّب الملف config/boot.rb:

require_relative 'boot'

ولكن فقط إن لم يُطلب من قبل وهو الحال في rails server ولكن ليس مع Passenger.

ثم يبدأ المرح!

تحميل ريلز

السطر التالي في config/application.rb هو:

require 'rails/all'

الملف railties/lib/rails/all.rb

هذا الملف مسؤول عن طلب كل الإطارات الفردية لريلز:

require "rails"
 
%w(
  active_record/railtie
  action_controller/railtie
  action_view/railtie
  action_mailer/railtie
  active_job/railtie
  action_cable/engine
  active_storage/engine
  rails/test_unit/railtie
  sprockets/railtie
).each do |railtie|
  begin
    require railtie
  rescue LoadError
  end
end

تُحمّل هنا كل إطارات ريلز وبالتالي تُتاح للتطبيق. لن نتطرّق إلى تفاصيل ما يحدث داخل كل إطار من هذه الإطارات، ولكننا نشجّعك على تجربتها واستكشافها بنفسك.

ضع في اعتبارك حاليًّا أن جميع الوظائف الشائعة مثل محرّكات ريلز، وإعدادات I18n وضبط ريلز تُعرّف هنا.

الرجوع إلى الملف config/environment.rb

يعرّف باقي الملف config/application.rb الإعدادت لـ Rails::Application التي ستُستخدم بمجرّد اكتمال تهيئة التطبيق. عند انتهاء config/application.rb من تحميل ريلز وتحديد مجال اسم التطبيق، نعود إلى config/environment.rb. يُهيّئ التطبيق هنا مع !Rails.application.initialize المُعرّفة في rails/application.rb.

الملف railties/lib/rails/application.rb

يبدو التابع initialize!‎ كالتالي:

def initialize!(group=:default) #:nodoc:
  raise "Application has been already initialized." if @initialized
  run_initializers(group, self)
  @initialized = true
  self
end

يمكنك كما ترى تهيئة تطبيق ما مرّة واحدة فقط. تُشغّل المهيّئات من خلال التابع run_initializers التي تُعرّف في الملف 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

الشيفرة run_initializers بحد ذاتها صعبة. ما تفعله ريلز هنا هو عبور كل أسلاف (ancestors) الصنف باحثًا عن تلك التي تستجيب للتابع initializers. ترتب بعدها الأسلاف حسب الاسم وتشغّلها. على سبيل المثال، سيوفّر الصنف Engine جميع المحرّكات من خلال توفير التابع initializers بها.

يُعرّف الصنف Rails::Application - كما هو مُعرّف في الملف railties/lib/rails/application.rb - المهيّئات railtie، و bootstrap، و railtie، و finisher. يُعدّ المُهيّئ bootstrap التطبيق (مثلا بتهيئة المُسجل) بينما المهيئات finisher (مثل إنشاء مكدس برمجيّات وسيطة) آخر ما تُنفّذ. المهيّئات railtie هي المهيّئات التي عُرّفت على Rails::Application نفسه وتُشغّل بين برنامج bootstrap و finishers.

بعد الانتهاء من ذلك نعود إلى Rack::Server.

الملف Rack: lib/rack/server.rb

آخر مرة تركناه فيها كانت عند تعريف التابع app:

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)
    self.options.merge! options
    app
  end
 
  def build_app_from_string
    Rack::Builder.new_from_string(self.options[:builder])
  end

في هذه المرحلة، app هو تطبيق ريلز بحد ذاته (برنامج وسيط) وما سيحدث بعد ذلك هو استدعاء Rack لجميع البرامج الوسيطة (middlewares) المعطاة:

def build_app(app)
  middleware[options[:environment]].reverse_each do |middleware|
    middleware = middleware.call(self) if middleware.respond_to?(:call)
    next unless middleware
    klass = middleware.shift
    app = klass.new(app, *middleware)
  end
  app
end

تذكّر أن build_app قد استدعي (من طرف wrapped_app) في السطر الأخير من Server#start. هذا شكله لما تركناه:

server.run wrapped_app, options, &blk

يعتمد تنفيذ server.run في هذه المرحلة على الخادم الذي تستخدمه. إن كنت تستخدم Puma مثلًا، فإليك ما سيبدو عليه تابع run:

...
DEFAULT_OPTIONS = {
  :Host => '0.0.0.0',
  :Port => 8080,
  :Threads => '0:16',
  :Verbose => false
}
 
def self.run(app, options = {})
  options = DEFAULT_OPTIONS.merge(options)
 
  if options[:Verbose]
    app = Rack::CommonLogger.new(app, STDOUT)
  end
 
  if options[:environment]
    ENV['RACK_ENV'] = options[:environment].to_s
  end
 
  server   = ::Puma::Server.new(app)
  min, max = options[:Threads].split(':', 2)
 
  puts "Puma #{::Puma::Const::PUMA_VERSION} starting..."
  puts "* Min threads: #{min}, max threads: #{max}"
  puts "* Environment: #{ENV['RACK_ENV']}"
  puts "* Listening on tcp://#{options[:Host]}:#{options[:Port]}"
 
  server.add_tcp_listener options[:Host], options[:Port]
  server.min_threads = min
  server.max_threads = max
  yield server if block_given?
 
  begin
    server.run.join
  rescue Interrupt
    puts "* Gracefully stopping, waiting for requests to finish"
    server.stop(true)
    puts "* Goodbye!"
  end
 
end

لن نتطرّق إلى تهيئة الخادم نفسه ولكن هذا آخر جزء من رحلتنا في عمليّة تهيئة ريلز.

ستساعدك هذه النظرة العامة عالية المستوى على فهم متى تُنفّذ تعليماتك البرمجية وبصفة عامّة كيف تصبح مطوّر ريلز أفضل. إن كنت ماتزال ترغب في معرفة المزيد، ربما تكون شيفرة ريلز المصدرية ذاتها أفضل مكان للانتقال إليه بعد ذلك.

مصادر