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-buttonclick() アクションを変更するには:

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 呼び出しが失敗する原因となります。この状況では、ルックアップが後で発生するように試みる必要があります。例えば、次のような変更を行います。

    // initializer でサービスをルックアップし、実行時に使用する (悪い!)
    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: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

上記で言及された合法的なユースケース内で、同じインストール環境にある別のプラグインのコンポーネントに対してプラグイン内で modifyClass を使用しようとすることは、不可能であるか、少なくとも信頼性が低いと推測しますか?

コアに含まれるプラグイン(例:ChatやPoll)であってもですか?

「いいね!」 1

両方のプラグインがインストールされて有効になっていれば、問題なく動作するはずです。ターゲットがインストール/有効化されていない場合は、コンソールに警告が表示されます。しかし、ignoreMissing パラメータ を使用してそれを抑制することができます。

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

もちろん、標準的な modifyClass のアドバイスも引き続き適用されます。これは最終手段であるべきで、いつでも壊れる可能性があるため、問題を迅速に特定できる十分なテストを実施する必要があります。トランスフォーマー を使用する方がはるかに安全な戦略です。

「いいね!」 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