对于高级主题和插件,Discourse 提供了 modifyClass 系统。这允许您扩展和覆盖核心许多 JavaScript 类中的功能。
何时使用 modifyClass
当您的自定义无法通过 Discourse 更稳定的自定义 API(例如 plugin-api 方法、插件出口、转换器)实现时,modifyClass 应作为最后的手段。
核心代码可能随时更改。因此,通过 modifyClass 所做的自定义随时可能中断。在使用此 API 时,您应确保已设置控件以在到达生产站点之前捕获这些问题。例如,您可以向主题/插件中添加自动化测试,或者使用暂存站点来针对您的主题/插件测试传入的 Discourse 更新。
基本用法
api.modifyClass 可用于修改 Ember 解决器可访问的任何类的函数和属性。这包括 Discourse 的路由、控制器、服务和组件。
modifyClass 接受两个参数:
-
resolverName(string) - 通过使用类型(例如 component/controller/etc.),后跟冒号,后跟(短横线命名法)类的文件名名称来构造此名称。例如:component:d-button、component:modal/login、controller:user、route: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(例如插件出口、转换器或定制 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)。任何使用此语法的代码都应更新为使用上面描述的本机类语法。通常,转换可以通过以下方式完成:
- 删除
pluginId- 此项不再需要 - 更新为上面描述的现代本机类语法
- 测试您的更改
故障排除
类已初始化
在初始化程序中使用 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((api) => { const composerService = api.container.lookup("service:composer"); api.composerBeforeSave(async () => { composerService.doSomething(); }); });改为:
// 'Just in time' lookup of service (good!) export default apiInitializer((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); }, };现在,对用户模型的此修改应该可以成功运行而不会打印警告,并且新方法将在
currentUser对象上可用。
This document is version controlled - suggest changes on github。