使用 modifyClass 改变核心行为

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

何时使用 modifyClass

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

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

基本用法

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

modifyClass 接受两个参数:

  • resolverName (字符串) - 通过类型(例如 component/controller/etc.)加上冒号,再加上类的(dasherized)文件名名称来构建它。例如: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() {
            // 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 字段可以被另一个 @tracked 字段覆盖)

    // 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(例如 plugin outlets、transformers 或定制 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. 测试您的更改

故障排除

类已初始化

在 initializer 中使用 modifyClass 时,您可能会在控制台中看到此警告:

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

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

  • 添加 lookup() 导致错误

    如果您在启动过程过早地 lookup() 单例,将导致任何后续的 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)上的方法时,以及在应用程序启动过程中早期初始化的模型上(例如 service:current-user 会初始化 model:user)。

    将 modifyClass 调用移到启动过程的更早阶段通常意味着将调用移到 pre-initializer,并将其配置为在 Discourse 的“inject-discourse-objects” initializer 之前运行。例如:

    // (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", {
          myNewUserFunction() {
            return "hello world";
          },
        });
      },
    
      initialize() {
        withPluginApi("0.12.1", this.initializeWithApi);
      },
    };
    

    现在,对 user 模型的此修改应该可以正常工作而不会打印警告,并且新方法将在 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 个赞