هيكلة إضافة لـ Rails autoloading

العديد من الإضافات (plugins) تتضمن تعريفات كثيرة للفئات (classes) داخل plugin.rb، أو تستخدم require_relative لتحميل ملفات روبي (ruby). هذا يعمل، ولكنه يأتي مع بعض السلبيات:

  1. لا يوجد إعادة تحميل تلقائية للتغييرات أثناء التطوير. أي تغييرات تتطلب إعادة تشغيل كاملة للخادم.
  2. ترتيب استدعاءات require الصحيح يمكن أن يكون مؤلماً.
  3. إذا تم استدعاؤها خارج كتلة after_initialize، فقد لا تكون الفئات/الوحدات التي يتم تحميلها تلقائياً متاحة للآخرين.

هناك حل! يمكن للإضافات الاعتماد على نظام التحميل التلقائي القياسي في Rails. بالنسبة للإضافات الجديدة، كل ما تحتاجه محدد في هيكل الإضافة (plugin-skeleton). يصف هذا الموضوع كيفية تكييف إضافة موجودة وتوسيع التكوين.

1. تعريف وحدة (module) ومحرك (Rails::Engine) لإضافتك

في plugin.rb، قم بتعريف وحدة لإضافتك باسم فريد PLUGIN_NAME، وأضف سطر require_relative لتحميل ملف المحرك الذي سنقوم بإنشائه الآن.

# name: my-plugin-name
# ...

module ::MyPluginModule
  PLUGIN_NAME = "my-plugin-name"
end

require_relative "lib/my_plugin_module/engine"

الآن قم بإنشاء {plugin}/lib/my_plugin_module/engine.rb:

module ::MyPluginModule
  class Engine < ::Rails::Engine
    engine_name PLUGIN_NAME
    isolate_namespace MyPluginModule
  end
end

أمور مهمة يجب ملاحظتها:

  1. في plugin.rb، يجب تضمين :: قبل اسم الوحدة لتعريفها في مساحة الاسم الجذرية (وإلا، سيتم تعريفها تحت Plugin::Instance).

  2. يجب أن يكون require_relative "lib/.../engine" في الجذر الخاص بملف plugin.rb، وليس داخل كتلة after_initialize.

  3. وضع المحرك في ملفه الخاص تحت lib/ أمر مهم. تعريفه مباشرة في ملف plugin.rb لن يعمل. (يستخدم Rails وجود دليل lib/ لتحديد جذر المحرك).

  4. يجب أن يتضمن مسار الملف اسم الوحدة، وفقاً لـ قواعد Zeitwerk.

  5. يُستخدم engine_name كبادئة لمهام الـ rake وأي مسارات (routes) محددة بواسطة المحرك (:link: وثائق rails).

  6. يساعد isolate_namespace في منع تسرب الأشياء بين النواة والإضافة (:link: وثائق Rails).

2. تعريف ملفات روبي في هيكل الدليل الصحيح

سيقوم المحرك الآن بالتحميل التلقائي لجميع الملفات الموجودة في {plugin}/app/{type}/*. على سبيل المثال، يمكننا تعريف وحدة تحكم (controller):

{plugin}/app/controllers/my_plugin_module/examples_controller.rb

module ::MyPluginModule
  class ExamplesController < ::ApplicationController
    requires_plugin PLUGIN_NAME

    def index
      render json: { hello: "world" }
    end
  end
end

سيتم الآن تحميل هذا تلقائياً كلما حاول أي شيء في Rails الوصول إلى ::MyPluginModule::MyController. لاختبار الأشياء، حاول الوصول إلى تلك الفئة من وحدة تحكم rails.

لكي يعمل التحميل التلقائي بشكل صحيح، يجب أن تتطابق مسارات الملفات مع التسلسل الهرمي الكامل للوحدة/الفئة وفقاً للقواعد المحددة بواسطة Zeitwerk.

3. تعريف المسارات (routes) على محرك الإضافة

أنشئ ملف {plugin}/config/routes.rb

MyPluginModule::Engine.routes.draw do
  get "/examples" => "examples#index"
  # define routes here
end

Discourse::Application.routes.draw do
  mount ::MyPluginModule::Engine, at: "my-plugin"
end

سيتم تحميل هذا الملف تلقائياً بواسطة المحرك، وستدخل التغييرات حيز التنفيذ دون الحاجة لإعادة تشغيل الخادم. في هذه الحالة، سيكون إجراء وحدة التحكم متاحاً عند /my-plugin/examples.json.

4. إضافة المزيد من المسارات التي يتم تحميلها تلقائياً

قد ترغب أحياناً في إدخال أدلة إضافية لملفات روبي التي يمكن تحميلها تلقائياً. المثال الأكثر شيوعاً هو دليل lib/ في الإضافة.

قم بتعديل تعريف المحرك الخاص بك لإلحاق lib/ بمسارات التحميل التلقائي للمحرك:

class Engine < ::Rails::Engine
  engine_name PLUGIN_NAME
  isolate_namespace MyPluginModule
  config.autoload_paths << File.join(config.root, "lib")
end

الآن يمكنك تعريف وحدة مكتبة مثل
{plugin}/lib/my_plugin_module/some_lib_module.rb

module ::MyPluginModule::SomeLibModule
end

والآن أي إشارات إلى ::MyPluginModule::SomeLibModule ستقوم بتحميل الوحدة تلقائياً من هذا الملف.

5. الربح!

سيتم الآن تحميل جميع هذه الملفات تلقائياً دون أي استدعاءات require متعمدة. سيتم التقاط التغييرات تلقائياً بواسطة rails وإعادة تحميلها في مكانها دون إعادة تشغيل الخادم.


يتم التحكم في إصدار هذه الوثيقة - اقترح تغييرات على github.

17 إعجابًا

ديفيد، ما الذي دفع هذه الوثائق؟ تغيير في المصدر؟

لا أتذكر أنني اضطررت للقلق بشأن الحفاظ على هيكل معين لتحقيق إعادة التحميل التلقائي، أو ربما كنت فقط أتبع الهيكل المناسب لحسن الحظ؟ …

إعجاب واحد (1)

لا يوجد تغيير في المنبع - مجرد توثيق لهيكل استخدمناه في عدد قليل من الإضافات (مثل anonymous-moderators، client-performance)، ونود البدء في استخدامه في المزيد من إضافاتنا.

إذا قبلت العيوب الثلاثة في أعلى المنشور، فإن أي هيكل سيعمل. إذا اتبعت هذا الهيكل، فسيتم حل هذه المشكلات (ويجب أن تكون تجربة المطور أكثر سلاسة!)

إعجابَين (2)

نعم، إعادة التحميل التلقائي مرغوبة للغاية.

شكراً على التوضيح والتوثيق!

إعجاب واحد (1)

وللتوضيح أيضًا - عندما أقول “التحميل التلقائي”، أعني قدرة Rails (عبر Zeitwerk) على سحب تغييرات التعليمات البرمجية إلى عملية قيد التشغيل على الفور تقريبًا، دون الحاجة إلى إعادة التشغيل. إذا كانت لديك استدعاءات load أو require في ملف plugin.rb الخاص بك، فمن شبه المؤكد أن المكون الإضافي الخاص بك لا يستخدم التحميل التلقائي.

بشكل منفصل، لدى Discourse مساعد يكتشف التغييرات في الملفات الياقوتية غير القابلة لإعادة التحميل، ويقوم بإعادة تشغيل الخادم بالكامل تلقائيًا. يستغرق هذا بضع ثوانٍ. عند تشغيل هذا النظام، سترى شيئًا كهذا في وحدة التحكم

[DEV]: تم تحرير الملفات التي لا يتم تحميلها تلقائيًا. جارٍ إعادة تشغيل الخادم...
       - plugins/discourse-follow/plugin.rb
إعجابَين (2)

آه!! هذا هو التمييز الذي كنت أفتقده، شكرًا لك! كنت سعيدًا بمجرد إعادة التشغيل التلقائي، ولكن هذا رائع حقًا!

إعجابَين (2)

ديفيد،

لقد واجهت مشكلة مثيرة للاهتمام، أعتقد أنني قمت بحلها، أتساءل عما إذا كان لديك وقت للتعليق؟

كان لدي ثابت يتم تعريفه مرة واحدة فقط في وحدة المكون الإضافي الخاصة بي ضمن /lib/locations

مع كل إعادة بناء كنت أحصل على التحذير التالي خطأ:

image

warning: already initialized constant Locations::REQUEST_PARTS
warning: already previous definition of REQUEST_PARTS was here

يبدو أن نقل هذا إلى plugin.rb على هذا النحو يحل المشكلة:

ولكن … لماذا؟

رأيي هو أنه يقوم بتقييم plugin.rb مرة واحدة فقط، ولكن أي شيء في /lib/plugin-module قد يتم تقييمه أكثر من مرة …؟ ولكن لماذا لا يشتكي من رموز أخرى؟

إعجاب واحد (1)

نعم بالضبط.

عندما يقوم Rails/Zeitwerk بإعادة تحميل التعليمات البرمجية، فإنه يقوم بتنفيذ remove_const :Blah، ثم يقوم بتحميل الملف بالاسم ذي الصلة. لذلك بالنسبة لملف lib/locations/geocode.rb الخاص بك، يقوم المحمل التلقائي بشيء مثل

Locations.remove_const :Geocode
load "lib/locations/geocode.rb"

لهذا السبب لا تحصل على خطأ بشأن تعريف الثابت Geocode مسبقًا - يقوم Rails/Zeitwerk بإزالته تلقائيًا قبل إعادة تحميل الملف.

ومع ذلك، لا توجد طريقة للمحمل التلقائي لمعرفة أن ملفك كان يعرف أيضًا Locations::REQUEST_PARTS، لذلك لم يقم بتنفيذ remove_const قبل التحميل.

لذلك، إذا كنت ترغب في الاحتفاظ بالثابت في ملف geocode.rb، يمكنك نقل REQUEST_PARTS داخل فئة Geocode (Locations::Geocode::REQUEST_PARTS)

ولكن إذا كنت ترغب في الاحتفاظ به في Locations::REQUEST_PARTS، فأعتقد أن النقل إلى plugin.rb منطقي. أو، إذا كنت ترغب في جعله قابلاً للتحميل التلقائي بالكامل، فقد تتمكن من نقل تعريف module ::Locations، بما في ذلك الثابت REQUEST_PARTS، إلى ملف خاص به مثل lib/locations.rb.

3 إعجابات

هذا منطقي تمامًا، شكرًا لك!

أعتقد أنه قد يكون هناك شيء آخر أيضًا؟

هل من العدل أيضًا القول إنه غير ضروري تحميل هذه الملفات بشكل صريح في plugin.rb كما في:

after_initialize do
  %w(
    ../app/models/location_country_default_site_setting.rb
    ../app/models/location_geocoding_language_site_setting.rb
    ../app/models/locations/user_location.rb
    ../app/models/locations/topic_location.rb
**    ../lib/locations/user_location_process.rb**
**    ../lib/locations/topic_location_process.rb**
**    ../lib/locations/country.rb**
**    ../lib/locations/geocode.rb**
**    ../lib/locations/helper.rb**
**    ../lib/locations/map.rb**
    ../lib/users_map.rb
    ../app/serializers/locations/geo_location_serializer.rb
    ../app/controllers/locations/geocode_controller.rb
    ../app/controllers/locations/users_map_controller.rb
  ).each do |path|
    load File.expand_path(path, __FILE__)
  end

لأنها ضمن الهيكل الذي يتم التعامل معه أيضًا عن طريق التحميل التلقائي؟

نعم، بما أن /lib مضمن في إعداد التحميل التلقائي الخاص بك، فلا ينبغي أن تكون هناك حاجة إلى load يدويًا :100:

إعجابَين (2)

من لا يحب تبسيط الكود الجيد؟! :chefs_kiss:

إعجاب واحد (1)