使用 modifyClass 修改核心行为

对于高级主题和插件,Discourse 提供了 modifyClass 系统。它允许你扩展和覆盖核心 JavaScript 类中的许多功能。

何时使用 modifyClass

modifyClass 应作为最后的手段,仅当无法通过 Discourse 更稳定的定制 API(例如 plugin-api 方法、plugin outlets、transformers)进行定制时使用。

核心代码可能会随时发生变化。因此,通过 modifyClass 进行的定制也可能会随时失效。在使用此 API 时,你应确保已采取措施,以便在这些问题到达生产站点之前捕获它们。例如,你可以为主题/插件添加自动化测试,或者使用预发布站点来测试即将发布的 Discourse 更新是否会影响你的主题/插件。

基本用法

api.modifyClass 可用于修改任何可通过 Ember 解析器访问的类的函数和属性。这包括 Discourse 的路由(routes)、控制器(controllers)、服务(services)和组件(components)。

modifyClass 接受两个参数:

  • resolverName (字符串) - 通过类型(例如 component/controller 等),后跟一个冒号,再后跟类的(短横线分隔的)文件名来构造此字符串。例如: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";
    }
    

如果你发现自己想要执行上述操作,那么你的使用场景可能更适合通过向核心提交 PR 来引入新的 API(例如 plugin outlets、transformers 或自定义 API)。

升级旧版语法

过去,modifyClass 是使用对象字面量语法调用的,如下所示:

// 过时的语法 - 请勿使用
api.modifyClass("component:some-component", {
  someFunction() {
    const original = this._super();
    return original + " some change";
  }
  pluginId: "some-unique-id"
});

不再推荐此语法,并且它存在已知的 bug(例如覆盖 getters 或 @actions)。使用此语法的任何代码都应更新为使用上述描述的原生类语法。通常,转换可以通过以下步骤完成:

  1. 移除 pluginId - 这不再需要
  2. 更新为上述描述的现代原生类语法
  3. 测试你的更改

故障排除

类已初始化

在初始化程序(initializer)中使用 modifyClass 时,你可能会在控制台中看到以下警告:

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

在主题/插件开发中,通常有两种方式会导致此错误:

  • 添加 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 个赞

我推测,在同一个安装中,尝试在插件中使用 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:
Neither:
api.modifyClass("component:create-channel", :cross_mark:
Nor even:
api.modifyClass("component:modal/create-channel", :cross_mark:
都不够!

5 个赞

plugin-api.gjsapi.modifyClass 的示例仍在使用旧版语法。也许需要更新一下?

4 个赞