استخدام modifyClass لتغيير السلوك الأساسي

للمواضيع والإضافات المتقدمة، يوفّر Discourse نظام modifyClass. يتيح لك هذا النظام توسيع وظائف العديد من فئات جافاسكربت الأساسية والتغلب عليها.

متى يجب استخدام modifyClass

يجب أن يكون modifyClass الخيار الأخير، عندما لا يمكن إجراء التخصيص المطلوب عبر واجهات برمجة التطبيقات (APIs) الأكثر استقرارًا في Discourse (مثل دوال plugin-api، ومخارج الإضافات plugin outlets، والمحوّلات transformers).

يمكن أن يتغير الكود الأساسي في أي وقت. وبالتالي، فإن أي تخصيصات تتم عبر modifyClass قد تتعطل في أي وقت. عند استخدام هذه الواجهة، يجب أن تتأكد من وجود ضوابط في مكانها لاكتشاف هذه المشكلات قبل وصولها إلى الموقع الإنتاجي. على سبيل المثال، يمكنك إضافة اختبارات آلية إلى الموضوع/الإضافة، أو يمكنك استخدام موقع تجريبي (staging site) لاختبار تحديثات Discourse الواردة مقابل موضوعك/إضافتك.

الاستخدام الأساسي

يمكن استخدام api.modifyClass لتعديل الدوال والخصائص في أي فئة يمكن الوصول إليها عبر محلل Ember (Ember resolver). ويشمل ذلك مسارات Discourse (routes)، ومُتحكّماتها (controllers)، وخدماتها (services)، ومكوّناتها (components).

تتقبّل modifyClass حجتين:

  • resolverName (نص string) - يُبنى هذا الاسم باستخدام النوع (مثل component/controller/إلخ)، متبوعًا بنقطة وفاصلة، ثم اسم ملف الفئة (المُنقّط dasherized). على سبيل المثال: component:d-button، component:modal/login، controller:user، route:application، إلخ.

  • callback (دالة function) - دالة تستقبل تعريف الفئة الموجود، ثم تُعيد نسخة موسّعة منه.

على سبيل المثال، لتعديل إجراء click() على d-button:

api.modifyClass(
  "component:d-button",
  (Superclass) =>
    class extends Superclass {
      @action
      click() {
        console.log("button was clicked");
        super.click();
      }
    }
);

تقلّد صيغة class extends ... تلك الخاصة بفئات جافاسكربت الفرعية. بشكل عام، يمكن تطبيق أي صيغة/ميزة مدعومة من قبل الفئات الفرعية هنا. ويشمل ذلك super، والخصائص/الدوال الثابتة (static properties/functions)، وأكثر من ذلك.

ومع ذلك، هناك بعض القيود. يكتشف نظام modifyClass التغييرات في prototype الخاص بفئة جافاسكربت فقط. عمليًا، يعني ذلك:

  • إدخال أو تعديل constructor() غير مدعوم

    api.modifyClass(
      "component:foo",
      (Superclass) =>
        class extends Superclass {
          constructor() {
            // This is not supported. The constructor will be ignored
          }
        }
    );
    
  • إدخال أو تعديل حقول الفئة (class fields) غير مدعوم (على الرغم من أن بعض حقول الفئة المزينة، مثل @tracked، يمكن استخدامها)

    api.modifyClass(
      "component:foo",
      (Superclass) =>
        class extends Superclass {
          someField = "foo"; // NOT SUPPORTED - do not copy
          @tracked someOtherField = "foo"; // This is ok
        }
    );
    
  • لا يمكن التغلب على الحقول البسيطة للفئة في التنفيذ الأصلي بأي طريقة (على الرغم من أنه، كما هو موضح أعلاه، يمكن التغلب على حقول @tracked بحقل @tracked آخر)

    // Core code:
    class Foo extends Component {
      // This core field cannot be overridden
      someField = "original";
    
      // This core tracked field can be overridden by including
      // `@tracked someTrackedField =` in the modifyClass call
      @tracked someTrackedField = "original";
    }
    

إذا وجدت نفسك ترغب في القيام بهذه الأمور، فقد يكون استخدامك أفضل تلبية من خلال تقديم طلب سحبه (PR) لإدخال واجهات برمجة تطبيقات جديدة في الكود الأساسي (مثل مخارج الإضافات، والمحوّلات، أو واجهات مخصصة).

ترقية الصيغة القديمة (Legacy Syntax)

في الماضي، كان يُستدعى modifyClass باستخدام صيغة كائن الحرفي (object-literal syntax) على النحو التالي:

// Outdated syntax - do not use
api.modifyClass("component:some-component", {
  someFunction() {
    const original = this._super();
    return original + " some change";
  }
  pluginId: "some-unique-id"
});

لم تعد هذه الصيغة موصى بها، ولديها أخطاء معروفة (مثل التغلب على المُرجِّعات getters أو @actions). يجب تحديث أي كود يستخدم هذه الصيغة لاستخدام صيغة الفئة الأصلية (native-class syntax) الموصوفة أعلاه. بشكل عام، يمكن إجراء التحويل من خلال:

  1. إزالة pluginId - لم تعد مطلوبة
  2. التحديث إلى صيغة الفئة الأصلية الحديثة الموصوفة أعلاه
  3. اختبار تغييراتك

استكشاف الأخطاء وإصلاحها

تم تهيئة الفئة مسبقًا (Class already initialized)

عند استخدام modifyClass في مُهيّئ (initializer)، قد ترى هذا التحذير في وحدة التحكم (console):

Attempted to modify "{name}", but it was already initialized earlier in the boot process

في تطوير المواضيع/الإضافات، هناك طريقتان يتم من خلالهما إدخال هذا الخطأ عادةً:

  • إضافة lookup() تسببت في الخطأ

    إذا قمت بـ lookup() لحالة مفردة (singleton) مبكرًا جدًا في عملية الإقلاع (boot process)، فسيتسبب ذلك في فشل أي استدعاءات modifyClass لاحقة. في هذه الحالة، يجب أن تحاول نقل عملية البحث (lookup) لتحدث في وقت لاحق. على سبيل المثال، ستقوم بتغيير شيء ما مثل هذا:

    // Lookup service in initializer, then use it at runtime (bad!)
    export default apiInitializer((api) => {
      const composerService = api.container.lookup("service:composer");
      api.composerBeforeSave(async () => {
        composerService.doSomething();
      });
    });
    

    إلى هذا:

    // 'Just in time' lookup of service (good!)
    export default apiInitializer((api) => {
      api.composerBeforeSave(async () => {
        const composerService = api.container.lookup("service:composer");
        composerService.doSomething();
      });
    });
    
  • إضافة modifyClass جديد تسببت في الخطأ

    إذا كان الخطأ ناتجًا عن إضافة موضوعك/إضافتك لاستدعاء modifyClass، فستحتاج إلى نقله إلى وقت مبكر في عملية الإقلاع. يحدث هذا عادةً عند التغلب على الدوال في الخدمات (مثل topicTrackingState)، وفي النماذج (models) التي يتم تهيئتها مبكرًا في عملية إقلاع التطبيق (مثل model:user الذي يتم تهيئته لـ service:current-user).

    نقل استدعاء modifyClass إلى وقت مبكر في عملية الإقلاع يعني عادةً نقل الاستدعاء إلى مُهيّئ مسبق (pre-initializer)، وتكوينه ليتم تشغيله قبل مُهيّئ ‘inject-discourse-objects’ الخاص بـ Discourse. على سبيل المثال:

    // (plugin)/assets/javascripts/discourse/pre-initializers/extend-user-for-my-plugin.js
    // or
    // (theme)/javascripts/discourse/pre-initializers/extend-user-for-my-plugin.js
    
    import { withPluginApi } from "discourse/lib/plugin-api";
    
    export default {
      name: "extend-user-for-my-plugin",
      before: "inject-discourse-objects",
    
      initializeWithApi(api) {
        api.modifyClass("model:user", (Superclass) => class extends Superclass {
          myNewUserFunction() {
            return "hello world";
          },
        });
      },
    
      initialize() {
        withPluginApi(this.initializeWithApi);
      },
    };
    

    يجب أن يعمل هذا التعديل على نموذج المستخدم الآن دون طباعة تحذير، وستكون الدالة الجديدة متاحة على كائن currentUser.


هذا المستند مُتحكَّم به في إصداراته - اقترح تغييرات على github.

16 إعجابًا

أفترض أنه من المستحيل أو على الأقل غير موثوق به محاولة استخدام modifyClass (ضمن حالات الاستخدام القانونية المذكورة أعلاه) داخل مكون إضافي لمكون إضافي آخر في نفس التثبيت؟

حتى لو كان مكونًا إضافيًا مضمنًا في النواة (مثل الدردشة أو الاستطلاع)؟

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

إذا تم تثبيت كلا المكونين الإضافيين وتم تمكينهما، فيجب أن يعمل كل شيء بدون مشاكل. إذا لم يتم تثبيت/تمكين المكون المستهدف، فستتلقى تحذيرًا في وحدة التحكم. ولكن يمكنك استخدام ignoreMissing parameter لكتم هذا التحذير.

api.modifyClass(
  "component:some-component",
  (Superclass) => ...,
  { ignoreMissing: true }
);

بالطبع، لا تزال نصائح modifyClass القياسية سارية: يجب أن يكون الملاذ الأخير، ويمكن أن يتعطل في أي وقت، لذا يجب عليك التأكد من أن اختباراتك جيدة بما يكفي لتحديد المشكلات بسرعة. سيكون استخدام transformers استراتيجية أكثر أمانًا بكثير.

3 إعجابات

إذًا، كيف يعمل ذلك، هل يؤجل التطبيق حتى يتم تسجيل وتحميل جميع المكونات من جميع المكونات الإضافية؟

أعتقد أن لدي حالة لا تبدو أنها تعمل.

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

يتم تعريف جميع وحدات ES6 (بما في ذلك المكونات) أولاً، ثم نقوم بتشغيل التهيئة المسبقة، ثم نقوم بتشغيل التهيئة العادية. لذا، بحلول الوقت الذي تعمل فيه أي تهيئة، يمكن حل جميع المكونات.

يسعدني إلقاء نظرة إذا كان بإمكانك مشاركة مقتطف أو فرع :eyes:

4 إعجابات

عذراً، يجب أن تكون حذراً وتوفر المسار الكامل!

على سبيل المثال:

api.modifyClass("component:chat/modal/create-channel", :white_check_mark:

لا:

api.modifyClass("component:create-channel", :cross_mark:

ولا حتى:

api.modifyClass("component:modal/create-channel", :cross_mark:

تكفي!

5 إعجابات

لا يزال المثال الخاص بـ api.modifyClass في plugin-api.gjs يستخدم الصيغة القديمة. ربما يحتاج إلى تحديث؟

4 إعجابات