coreの動作を変更するためにmodifyClassを使用する

高度なテーマやプラグインのために、Discourse は modifyClass システムを提供しています。これにより、コアの多くの JavaScript クラスの機能を拡張およびオーバーライドできます。

modifyClass を使用するタイミング

modifyClass は、カスタマイズが Discourse のより安定したカスタマイズ API (例: plugin-api メソッド、plugin outlet、transformer) 経由で行えない場合の最終手段とすべきです。

コアのコードはいつでも変更される可能性があります。したがって、modifyClass を使用したカスタマイズはいつでも壊れる可能性があります。この API を使用する場合は、カスタマイズが本番サイトに到達する前に問題を検出するための制御が設定されていることを確認する必要があります。たとえば、テーマ/プラグインに自動テストを追加するか、ステージングサイトを使用して、受信する Discourse の更新をテーマ/プラグインに対してテストすることができます。

基本的な使用法

api.modifyClass は、Ember リゾルバ経由でアクセス可能な任意のクラスの関数とプロパティを変更するために使用できます。これには、Discourse のルート、コントローラ、サービス、およびコンポーネントが含まれます。

modifyClass は 2 つの引数を取ります。

  • resolverName (文字列) - これは、タイプ (例: component/controller/etc.)、それに続くコロン、それに続く (ダッシュ化された) クラスのファイル名を使用して構成します。例: component:d-buttoncomponent:modal/logincontroller:userroute:application など。

  • callback (関数) - 既存のクラス定義を受け取り、拡張されたバージョンを返す関数。

たとえば、d-button の click() アクションを変更するには:

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

class extends ... 構文は、JS 子クラスの構文を模倣しています。一般的に、子クラスでサポートされている構文/機能はすべてここで適用できます。これには、super、静的プロパティ/関数などが含まれます。

ただし、いくつかの制限があります。modifyClass システムは、クラスの JS 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 フィールドは、modifyClass 呼び出しに
      // `@tracked someTrackedField =` を含めることでオーバーライドできます
      @tracked someTrackedField = "original";
    }
    

これらのことを行いたい場合は、コアに新しい API (例: plugin outlet、transformer、または専用 API) を導入するために PR を作成する方が適している可能性があります。

従来の構文のアップグレード

以前は、modifyClass は次のようなオブジェクトリテラル構文を使用して呼び出されていました。

// 古い構文 - 使用しないでください
api.modifyClass("component:some-component", {
  someFunction() {
    const original = this._super();
    return original + " some change";
  }
  pluginId: "some-unique-id"
});

この構文は推奨されなくなり、既知のバグ (例: ゲッターや @actions のオーバーライド) があります。この構文を使用しているコードは、上記で説明されているネイティブクラス構文を使用するように更新する必要があります。一般的に、変換は次の手順で行うことができます。

  1. pluginId を削除する - これはもう必要ありません
  2. 上記の最新のネイティブクラス構文に更新する
  3. 変更をテストする

トラブルシューティング

クラスがすでに初期化されている

initializer で modifyClass を使用すると、コンソールに次の警告が表示されることがあります。

Attempted to modify "{name}", but it was already initialized earlier in the boot process (ブートプロセスの早い段階で “{name}” の変更を試みましたが、すでに初期化されていました)

テーマ/プラグイン開発では、通常、次の 2 つの方法でこのエラーが発生します。

  • lookup() の追加が原因でエラーが発生した

    ブートプロセスの早い段階でシングルトンを lookup() すると、その後の modifyClass 呼び出しが失敗する原因となります。この状況では、lookup が後で発生するように試みる必要があります。たとえば、次のような変更を行います。

    // initializer でサービスを lookup し、実行時に使用する (悪い!)
    export default apiInitializer("0.8", (api) => {
      const composerService = api.container.lookup("service:composer");
      api.composerBeforeSave(async () => {
        composerService.doSomething();
      });
    });
    

    次のように変更します。

    // サービスの「ジャストインタイム」lookup (良い!)
    export default apiInitializer("0.8", (api) => {
      api.composerBeforeSave(async () => {
        const composerService = api.container.lookup("service:composer");
        composerService.doSomething();
      });
    });
    
  • 新しい modifyClass の追加が原因でエラーが発生した

    エラーがテーマ/プラグインによる modifyClass の呼び出しの追加によって発生する場合は、ブートプロセスの早い段階に移動する必要があります。これは、サービス (例: topicTrackingState) のメソッドをオーバーライドする場合や、アプリのブートプロセスの早い段階で初期化されるモデル (例: model:userservice:current-user のために初期化される) でよく発生します。

    modifyClass 呼び出しをブートプロセスの早い段階に移動するには、通常、その呼び出しを pre-initializer に移動し、Discourse の ‘inject-discourse-objects’ initializer より前に実行されるように設定します。たとえば、次のようになります。

    // (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