对于高级主题和插件,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-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() { // 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)。任何使用此语法的代码都应更新为使用上面描述的原生类语法。通常,转换可以按以下方式进行:
- 删除
pluginId- 此项不再需要 - 更新为上面描述的现代原生类语法
- 测试您的更改
故障排除
类已初始化
在 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 上建议更改。