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

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

متى تستخدم modifyClass

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

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

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

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

تأخذ modifyClass وسيطتين:

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

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

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

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

تُحاكي صيغة class extends ... صيغة فئات JS الفرعية. بشكل عام، يمكن تطبيق أي صيغة/ميزات مدعومة من قبل الفئات الفرعية هنا. ويشمل ذلك super، والخصائص/الوظائف الثابتة، والمزيد.

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

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

    api.modifyClass(
      "component:foo",
      (Superclass) =>
        class extends Superclass {
          constructor() {
            // هذا غير مدعوم. سيتم تجاهل المُنشئ
          }
        }
    );
    
  • إدخال أو تعديل حقول الفئة غير مدعوم (على الرغم من أنه يمكن استخدام بعض حقول الفئة المزينة، مثل @tracked)

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

    // الكود الأساسي:
    class Foo extends Component {
      // لا يمكن تجاوز حقل الأساس هذا
      someField = "original";
    
      // يمكن تجاوز حقل tracked الأساسي هذا عن طريق تضمين
      // `@tracked someTrackedField =` في استدعاء modifyClass
      @tracked someTrackedField = "original";
    }
    

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

ترقية الصيغة القديمة

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

// صيغة قديمة - لا تستخدم
api.modifyClass("component:some-component", {
  someFunction() {
    const original = this._super();
    return original + " some change";
  }
  pluginId: "some-unique-id"
});

لم تعد هذه الصيغة موصى بها، ولديها أخطاء معروفة (على سبيل المثال، تجاوز أدوات الحصول (getters) أو @actions). يجب تحديث أي كود يستخدم هذه الصيغة لاستخدام صيغة الفئة الأصلية الموضحة أعلاه. بشكل عام، يمكن أن يتم التحويل عن طريق:

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

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

تم تهيئة الفئة بالفعل

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

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

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

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

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

    // البحث عن الخدمة في المُهيئ، ثم استخدامها في وقت التشغيل (سيئ!)
    export default apiInitializer("0.8", (api) => {
      const composerService = api.container.lookup("service:composer");
      api.composerBeforeSave(async () => {
        composerService.doSomething();
      });
    });
    

    إلى هذا:

    // بحث "في الوقت المناسب" للخدمة (جيد!)
    export default apiInitializer("0.8", (api) => {
      api.composerBeforeSave(async () => {
        const composerService = api.container.lookup("service:composer");
        composerService.doSomething();
      });
    });
    
  • تسبب إضافة modifyClass جديد في حدوث الخطأ

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

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

    // (plugin)/assets/javascripts/discourse/pre-initializers/extend-user-for-my-plugin.js
    // أو
    // (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 إعجابات