对于高级主题和插件,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-button、component:modal/login、controller:user、route: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)。使用此语法的任何代码都应更新为使用上述描述的原生类语法。通常,转换可以通过以下步骤完成:
- 移除
pluginId- 这不再需要 - 更新为上述描述的现代原生类语法
- 测试你的更改
故障排除
类已初始化
在初始化程序(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:user为service: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 上。