大家好,
我正在为 Discourse 开发一个彩票插件,用户可以通过回复彩票主题来参与。坦白说:我不是程序员——我完全是在 AI 的帮助下(Claude Code)构建这个插件的,所以我可能缺少一些基本概念。 基本功能已经可以工作了,但在使用 MessageBus 进行实时更新时遇到了一些问题。我希望能得到一些关于正确方法的指导。
我想实现的目标
- 使用 BBCode
[lottery]...[/lottery]创建一个彩票帖子。 - 在帖子中显示一个彩票卡片,显示参与者数量。
- 当用户回复参与时,所有观看者都应该实时看到参与者数量更新,而无需刷新页面。
- 类似于 discourse-calendar 插件更新事件信息的方式。
当前实现
后端 (Ruby)
Lottery 模型 (app/models/lottery.rb):
def publish_update!
channel = "/lottery/#{self.post.topic_id}"
message = { id: self.id }
MessageBus.publish(channel, message)
end
LotteryParticipant 模型 (app/models/lottery_participant.rb):
after_commit :publish_lottery_update, on: [:create, :destroy]
private
def publish_lottery_update
self.lottery.publish_update!
end
后端日志确认 MessageBus.publish 已成功调用:
[Lottery] 📡 Publishing MessageBus update:
Channel: /lottery/27
Message: {:id=>52}
[Lottery] ✅ MessageBus.publish completed
前端 (JavaScript)
Decorator (discourse-lottery-decorator.gjs):
api.decorateCookedElement((cooked, helper) => {
const post = helper.getModel();
if (!post?.lottery_data) return;
// 检查是否已装饰(AI 建议使用 data 属性)
if (cooked.dataset.lotteryDecorated === "true") return;
const lotteryNode = cooked.querySelector(".discourse-lottery");
if (!lotteryNode) return;
const wrapper = document.createElement("div");
lotteryNode.before(wrapper);
const lottery = Lottery.create(post.lottery_data);
helper.renderGlimmer(
wrapper,
`<template><DiscourseLottery @lotteryData={{lottery}} /></template>`
);
lotteryNode.remove();
cooked.dataset.lotteryDecorated = "true";
}, { id: "discourse-lottery" });
Component (discourse-lottery/index.gjs):
export default class DiscourseLottery extends Component {
@service messageBus;
@service lotteryApi;
constructor() {
super(...arguments);
const { lotteryData } = this.args;
if (lotteryData?.topicId) {
this.lotteryPath = `/lottery/${lotteryData.topicId}`;
this.lotteryData = lotteryData;
// AI 建议手动绑定回调
this._boundOnLotteryUpdate = this._onLotteryUpdate.bind(this);
this.messageBus.subscribe(this.lotteryPath, this._boundOnLotteryUpdate);
registerDestructor(this, () => {
this.messageBus.unsubscribe(this.lotteryPath, this._boundOnLotteryUpdate);
});
}
}
async _onLotteryUpdate(msg) {
console.log('[Lottery Component] 🔔 MessageBus message received!');
const updatedData = await this.lotteryApi.lottery(this.lotteryData.topicId);
this.lotteryData.updateFromLottery(updatedData);
}
}
Model (models/lottery.js):
export default class Lottery {
@tracked _stats;
set stats(stats) {
this._stats = LotteryStats.create(stats || {});
}
updateFromLottery(lottery) {
// 更新所有属性,包括 stats
this.stats = lottery.stats || {};
// ... 其他属性
}
}
问题
只有参与者的页面会实时更新,而其他观看者的页面在不刷新页面的情况下不会更新。
有效的部分:
当用户回复参与时,他们自己的页面会实时显示参与者数量更新。
后端成功发布 MessageBus 消息。
参与者的页面收到了 MessageBus 消息(日志显示“
MessageBus message received!”)。
模型已更新为新的参与者数量(日志显示数量从 0 增加到 1)。
无效的部分:
其他观看彩票主题的用户看不到数量实时更新。
彩票创建者的页面不会更新。
打开了彩票主题的任何其他用户的页面都不会更新。- 他们都需要手动刷新页面才能看到更新的参与者数量。
此外,我在控制台中看到组件被多次销毁和重新创建:
[Lottery Component] ✅ MessageBus subscription established
[Lottery Component] 🧹 Cleaning up MessageBus for topic: 27
[Lottery Component] ✅ MessageBus subscription established
[Lottery Component] 🧹 Cleaning up MessageBus for topic: 27
这表明 decorateCookedElement 被触发了多次,导致组件被销毁和重新创建,这可能会破坏其他观看者的 MessageBus 订阅。
问题
-
使用已装饰元素上的
data-decorated属性是防止重复装饰的正确方法吗? AI 建议了这种方法,但我找不到官方文档确认这是推荐的模式。 -
为什么 UI 没有更新,即使跟踪的模型属性发生了变化?
@tracked _stats在通过this.stats = lottery.stats更新时应该会触发重新渲染,但显示的参与者数量保持不变。我使用@tracked的方式有什么问题吗? -
我应该完全采用不同的方法吗? 我让 AI 遵循 discourse-calendar 的模式,但也许我遗漏了 Discourse 处理实时更新的基本知识。
-
移除原始的
lotteryNode是否会导致问题? AI 最初尝试不移除它,但这会导致彩票卡片被 CSS 隐藏(.cooked > .discourse-lottery { display: none; })。有没有更好的模式?
由于我不是程序员,我可能会问一些基本问题——任何指向文档或示例的链接都会非常有帮助!谢谢!