使用 modifyClass 更改核心行为

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

何时使用 modifyClass

当您的自定义需求无法通过 Discourse 更稳定的自定义 API(例如 plugin-api 方法、插件插口、transformer)实现时,modifyClass 应作为最后的手段。

核心代码可能随时更改。因此,通过 modifyClass 所做的自定义也可能随时中断。在使用此 API 时,您应确保已设置控制措施,以便在问题到达生产站点之前捕获这些问题。例如,您可以向主题/插件中添加自动化测试,或者使用暂存站点来测试传入的 Discourse 更新是否与您的主题/插件兼容。

基本用法

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

modifyClass 接受两个参数:

  • resolverName (string) - 通过使用类型(例如 component/controller/etc.),后跟冒号,再后跟类的(kebab-case 化的)文件名来构建。例如:component:d-buttoncomponent:modal/logincontroller:userroute:application 等。

  • callback (function) - 一个函数,它接收现有的类定义,然后返回一个扩展后的版本。

例如,要修改 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 系统仅检测类 JavaScript prototype 上的更改。实际上,这意味着:

  • 引入或修改 constructor() 不受支持

    api.modifyClass(
      "component:foo",
      (Superclass) =>
        class extends Superclass {
          constructor() {
            // This is not supported. The constructor will be ignored
          }
        }
    );
    
  • 引入或修改类字段不受支持(尽管一些带装饰器的类字段,如 @tracked 可以使用)

    api.modifyClass(
      "component:foo",
      (Superclass) =>
        class extends Superclass {
          someField = "foo"; // NOT SUPPORTED - do not copy
          @tracked someOtherField = "foo"; // This is ok
        }
    );
    
  • 原始实现上的简单类字段无法以任何方式覆盖(尽管如上所述,@tracked 字段可以通过在 modifyClass 调用中包含 @tracked someTrackedField = 来覆盖)

    // Core code:
    class Foo extends Component {
      // This core field cannot be overridden
      someField = "original";
    
      // This core tracked field can be overridden by including
      // `@tracked someTrackedField =` in the modifyClass call
      @tracked someTrackedField = "original";
    }
    

如果您发现自己想做这些事情,那么您的用例可能更适合通过向核心提交 PR 来引入新的 API(例如插件插口、transformer 或定制 API)。

升级旧版语法

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

// Outdated syntax - do not use
api.modifyClass("component:some-component", {
  someFunction() {
    const original = this._super();
    return original + " some change";
  }
  pluginId: "some-unique-id"
});

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

  1. 删除 pluginId - 此项不再需要
  2. 更新为上述现代原生类语法
  3. 测试您的更改

故障排除

类已初始化

当在初始化器中使用 modifyClass 时,您可能会在控制台中看到此警告:

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

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

  • 添加 lookup() 导致了错误

    如果在启动过程过早地 lookup() 了单例(singleton),将导致任何后续的 modifyClass 调用失败。在这种情况下,您应尝试将查找推迟到更晚的时间。例如,您会将类似以下内容更改为:

    // Lookup service in initializer, then use it at runtime (bad!)
    export default apiInitializer("0.8", (api) => {
      const composerService = api.container.lookup("service:composer");
      api.composerBeforeSave(async () => {
        composerService.doSomething();
      });
    });
    

    更改为:

    // 'Just in time' lookup of service (good!)
    export default apiInitializer("0.8", (api) => {
      api.composerBeforeSave(async () => {
        const composerService = api.container.lookup("service:composer");
        composerService.doSomething();
      });
    });
    
  • 添加新的 modifyClass 导致了错误

    如果错误是由您的主题/插件添加 modifyClass 调用引起的,那么您需要将其移至启动过程的更早阶段。这通常发生在覆盖服务(例如 topicTrackingState)上的方法,以及在应用启动过程早期初始化的模型(例如 model:user 是为 service:current-user初始化的)时。

    将 modifyClass 调用移至启动过程的更早阶段通常意味着将调用移至 pre-initializer,并配置它在 Discourse 的 ‘inject-discourse-objects’ 初始化器之前运行。例如:

    // (plugin)/assets/javascripts/discourse/pre-initializers/extend-user-for-my-plugin.js
    // or
    // (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);
      },
    };
    

    现在,对 user 模型的此修改应该可以成功运行而不会打印警告,并且新方法将在 currentUser 对象上可用。


This document is version controlled - suggest changes on github

16 个赞