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

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

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

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

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

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

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

يأخذ 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 فئة JS. عمليًا، هذا يعني:

  • تقديم أو تعديل 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) لتقديم واجهات برمجة تطبيقات جديدة في الكود الأساسي (على سبيل المثال، مآخذ توصيل الإضافات (plugin outlets)، أو المحولات (transformers)، أو واجهات برمجة تطبيقات خاصة).

ترقية بناء الجملة القديم (Legacy Syntax)

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

// بناء جملة قديم - لا تستخدمه
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 مبكرًا جدًا في عملية التشغيل (boot process)، فسيؤدي ذلك إلى فشل أي استدعاءات لاحقة لـ modifyClass. في هذه الحالة، يجب أن تحاول نقل البحث ليحدث لاحقًا. على سبيل المثال، ستقوم بتغيير شيء مثل هذا:

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

    إلى هذا:

    // بحث "عند الحاجة" للخدمة (جيد!)
    export default apiInitializer((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 إعجابًا