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

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

modifyClass を使用するタイミング

modifyClass は、Discourse のより安定したカスタマイズ API (例: plugin-api メソッド、プラグインアウトレット、トランスフォーマー) を介してカスタマイズが行えない場合の最終手段として使用する必要があります。

コアのコードはいつでも変更される可能性があります。したがって、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"; // これは OK です
        }
    );
    
  • 元の実装の単純なクラスフィールドは、いかなる方法でもオーバーライドできません (ただし、上記のように、@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 呼び出しが失敗します。この場合、ルックアップを後で発生するように移動してみてください。たとえば、次のようなものを変更します。

    // 初期化子でサービスをルックアップし、実行時に使用する (悪い!)
    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) のメソッドをオーバーライドする場合や、アプリのブートプロセスで早期に初期化されるモデル (例: 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", {
          myNewUserFunction() {
            return "hello world";
          },
        });
      },
    
      initialize() {
        withPluginApi("0.12.1", 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