تصميم مكون إضافي لتحميل تلقائي في Rails

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

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

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

1. تعريف وحدة و 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 وأي مسارات يحددها المحرك (:link: وثائق rails)

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

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

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

{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. تعريف المسارات على محرك الإضافة

قم بإنشاء ملف {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)