modifyClass を使用してコアの動作を変更する

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

modifyClass を使用するタイミング

modifyClass は最終手段として使用するべきです。Discourse のより安定したカスタマイズ API(例: プラグイン API メソッド、プラグインアウトレット、トランスフォーマー)ではカスタマイズできない場合に限ります。

コアのコードはいつでも変更される可能性があります。そのため、modifyClass を通じたカスタマイズは、いつでも壊れる可能性があります。この API を使用する際は、本番環境に問題が到達する前にそれらを捕捉するための制御を講じておく必要があります。例えば、テーマ/プラグインに自動テストを追加したり、ステージングサイトで Discourse の更新をテーマ/プラグインに対してテストしたりすることができます。

基本的な使用方法

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

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

  • resolverName (文字列) - タイプ(例: component/controller など)を指定し、コロンを挟み、クラス名の(ダシャー化された)ファイル名を続けることで構築します。例: component:d-button, component:modal/login, controller:user, route: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";
    
      // このコアのトラッキングフィールドは、modifyClass 呼び出しに
      // `@tracked someTrackedField =` を含めることで上書きできます
      @tracked someTrackedField = "original";
    }
    

これらのことをやりたいと考える場合は、コアに新しい API(例: プラグインアウトレット、トランスフォーマー、または独自の 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. 変更のテスト

トラブルシューティング

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

イニシャライザー内で modifyClass を使用する場合、コンソールにこの警告が表示されることがあります:

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

テーマ/プラグインの開発において、このエラーが発生する通常の理由は2つあります:

  • lookup() の追加がエラーの原因

    ブートプロセスの初期段階でシングルトンを lookup() すると、その後の modifyClass 呼び出しが失敗する原因になります。この状況では、lookup を後に行うように変更する必要があります。例えば、以下のようなものを:

    // イニシャライザー内でサービスを lookup し、ランタイムで使用する(悪い例!)
    export default apiInitializer((api) => {
      const composerService = api.container.lookup("service:composer");
      api.composerBeforeSave(async () => {
        composerService.doSomething();
      });
    });
    

    以下のように変更します:

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

    テーマ/プラグインが modifyClass 呼び出しを追加することでエラーが発生した場合は、ブートプロセスのより早い段階に移動する必要があります。これは、サービス(例: topicTrackingState)や、アプリのブートプロセスの初期段階で初期化されるモデル(例: service:current-user に対して初期化される model: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 を使用しようとすることは、不可能であるか、少なくとも信頼性が低いと推測しますか?

コアに含まれるプラグイン(例: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

plugin-api.gjsapi.modifyClass の例がまだレガシー構文を使用しています。更新が必要かもしれません。

「いいね!」 4