对于高级主题和插件,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-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(例如插件插口、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)。任何使用此语法的代码都应更新为使用上述原生类语法。通常,转换可以通过以下方式完成:
- 删除
pluginId- 此项不再需要 - 更新为上述现代原生类语法
- 测试您的更改
故障排除
类已初始化
当在初始化器中使用 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。