عملية تهيئة ريلز
يشرح هذا الدليل العمل الداخلي لعملية التهيئة في ريلز. وهو دليل متعمق للغاية وموصى به لمطوري ريلز المتقدمّين.
ستتعلم بعد قراءة هذا الدليل:
- كيفيّة استخدام خادم ريلز (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
- 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
لن نتطرّق إلى تهيئة الخادم نفسه ولكن هذا آخر جزء من رحلتنا في عمليّة تهيئة ريلز.
ستساعدك هذه النظرة العامة عالية المستوى على فهم متى تُنفّذ تعليماتك البرمجية وبصفة عامّة كيف تصبح مطوّر ريلز أفضل. إن كنت ماتزال ترغب في معرفة المزيد، ربما تكون شيفرة ريلز المصدرية ذاتها أفضل مكان للانتقال إليه بعد ذلك.