在插件中覆盖现有的Discourse方法

最近我经常遇到需要覆盖插件中现有 Ruby 方法的情况,因此我想在此分享我的最佳实践。

覆盖实例方法

class ::TopicQuery
  module BabbleDefaultResults
    def default_results(options={})
      super(options).where('archetype <> ?', Archetype.chat)
    end
  end
  prepend BabbleDefaultResults
end
  • 在这里,我从一个返回主题列表的实例方法中移除了聊天主题。
  • 模块名 BabbleDefaultResults 可以是任何你喜欢的名称;我通常将其命名为方法名加上插件名,以尽量减少命名冲突的风险(尽管这种风险本身已经很低)。
  • Module#prepend 非常酷,如果你正在为任何 Ruby 项目编写插件,你应该了解它。请注意,正是因为我们使用了 prepend 模块,才使得我们能够在覆盖的方法中调用 super
  • 附注,务必调用 super 这使得你的插件在底层实现发生变化时更不容易出错。除非你_非常非常确定_你的功能完全替代了底层方法中的_所有内容_,否则你应该调用 super 并在此基础上修改结果,这样当 Discourse 核心中的该方法发生变化时,你的插件才不会因此失效。
  • ::TopicQuery 中的 :: 确保我引用的是顶层的 TopicQuery 类进行覆盖,而不是某个模块化版本(例如 Babble::TopicQuery)。
  • 这段代码可以直接放入 plugin.rb 中,如果你的插件较大,也可以考虑将每个覆盖操作分离到单独的文件中。

覆盖类方法

class ::Topic
  module BabbleForDigest
    def for_digest(user)
      super(user).where('archetype <> ?', Archetype.chat)
    end
  end
  singleton_class.prepend BabbleForDigest
end
  • 在这里,我获取了 Topic 类上现有的 self.for_digest 方法,并从结果中移除了聊天主题。
  • 这与覆盖实例方法非常相似,区别在于我们调用的是 singleton_class.prepend 而不是单纯的 prependsingleton_class 是一种稍微有些奇怪的说法,意思是“我想将此附加到类级别,而不是实例级别”。如果你想深入了解 Ruby 相关的知识,可以阅读 进一步资料

覆盖作用域(Scope)

class ::Topic
  @@babble_listable_topics = method(:listable_topics).clone
  scope :listable_topics, ->(user) {
    @@babble_listable_topics.call(user).where('archetype <> ?', Archetype.chat)
  }
end
  • 这个稍微有点棘手,因为作用域与 super 不太兼容(至少我没能成功让它们配合工作)。因此,我们采取的方法是:获取现有的方法定义,将其克隆并存储,然后在需要时调用它。
  • 同样,@@babble_listable_topics 可以是任何你喜欢的名称,但使用你的插件名作为命名空间可能是一个好主意。
  • 关于 method 函数 的更多信息,它也_非常_酷,不过真正需要用到它的场景并不多。顺便提一个相关的小知识:在调试时,如果你难以确定某个方法调用实际执行了哪些代码(通常是“是哪个 gem 定义了这个方法?”),你可以使用 source_location 来获取定义该方法的确切源代码行。
[7] pry(main)> Topic.new.method(:best_post).source_location
=> ["/Users/gdpelican/workspace/discourse/app/models/topic.rb", 282]

(^^ 这表示新主题上的 best_post 方法定义在 <root>/app/models/topic.rb 的第 282 行)

好了,这就是我所知道的全部内容。如果有任何需要纠正、扩展或澄清的地方,请告诉我 :slight_smile:

35 个赞

This is great, thanks for sharing James!

Overriding instance and class methods

My fav resource on this is: https://stackoverflow.com/a/4471202

I use basically the same structure, except I tend to seperate out the module and the prepend.

As you pointed out, this pattern is “super” :wink: useful when trying to avoid overriding core logic.

module InviteMailerEventExtension
  def send_invite(invite)
     ## stuff
     super(invite)
  end
end

require_dependency 'invite_mailer'
class ::InviteMailer
  prepend InviteMailerEventExtension
end

One small tip here is that when overriding private or protected methods, your overriding method also needs to be private or protected, e.g.

module UserNotificationsEventExtension
  protected def send_notification_email(opts)
    ## stuff
    super(opts)
  end
end
20 个赞

@angus @gdpelican Thanks for this. This is great stuff. :smiley: . This would be really essential all the (especially newbies like me) plugin developers out there.

This is what I really really needed to be aware of. I use to think that if you override a method only to make a few changes to it, you’d probably copy the code to your new method and make changes to it which by the very thought of it sounded hacky.

3 个赞

你好,我想请教一下这段代码:为什么需要这个 require_dependency?看起来没有它代码也能正常运行。

是的,确实如此。自那篇帖子发布以来,Discourse 对 Rails 的更新使得 require_dependency 变得不再必要。我无法编辑该帖子以说明这一点。详见:

3 个赞

有什么覆盖模块类的技巧吗?我想对 GroupGuardian 做一些更改(为特殊类型的组添加一些特殊条件)。

谢谢。

1 个赞

您可以重新定义模块并重新定义函数。如果您想保留对旧方法的访问权限,请使用 alias_method,如下所示:RubyDoc.info: Method: Module#alias_method – Documentation for core (3.4.3) – RubyDoc.info

3 个赞

刚开始接触 discourse 和 Rails 开发。我正在本地使用 Dev Container 环境(在 VS Code 中)。指南和文档很有帮助。

我想知道是否有人能提供关于如何覆盖核心 discourse 类的方法,特别是在本地开发环境中使其持久化。

在我的插件中,我试图覆盖核心 discourse TopicEmbed 类中的一个方法。(使用 @angus 上面很好地记录的通用方法。)它在我重建和重新加载 VS Code 时工作一次,但在后续的 http 请求中,我的覆盖从未被调用。

我的覆盖定义在 /plugins/my-plugin/app/models/override.rb 中,我使用 require_relative 将此文件包含在我的 plugin.rb 中。

#override.rb:
class ::TopicEmbed

  # 一个将预置到 TopicEmbed.singleton_class 的模块
  module TopicEmbedOverrideModule
    # TopicEmbed 中的方法
    def first_paragraph_from(html)
      Rails.logger.info(“我的覆盖正在发生! ”)

      # 继续执行 TopicEmbed 提供的原始实现。
      super

    end
  end

  # 在此处进行预置
  singleton_class.prepend TopicEmbedOverrideModule
end

我怀疑我的持久化挑战可能与我的开发环境以及 Ruby 代码的编译/缓存方式有关。
我也尝试了 rm -rf tmp; bin/ember-cli -ubundle exec rake tmp:cache:clear

我通过以下方式使其在单例类中生效:

# my overrides.rb

# 一个将被预置到 TopicEmbed.singleton_class 中的模块
module TopicEmbedOverrides

  # 覆盖 parse_html 方法
  def parse_html(html, url) # 注意:我在这里没有使用 self.
    # 我的新内容在这里
    # 然后运行原始实现
    super
  end

end

# 在这里进行覆盖
class ::TopicEmbed
  singleton_class.prepend TopicEmbedOverrides
end