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() {
            // これはサポートされていません。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(例:プラグインアウトレット、トランスフォーマー、またはカスタム 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((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:userservice: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