我们正逐步弃用旧的“小部件”(widget)渲染系统,转而采用现代的 Glimmer 组件。
我们最近已使用 Glimmer 组件对帖子流进行了现代化改造。本指南将引导您将插件和主题从旧的基于小部件的系统迁移到新的 Glimmer 实现。
请放心——这次迁移比乍一看要简单得多。我们设计的新系统比旧的小部件系统更直观、更强大,本指南将帮助您完成整个过程。
时间线
这些为预估时间,可能会有变动
2025 年第二季度:
核心实现已完成
开始升级官方插件和主题组件
已在 Meta 站点启用
已发布升级建议(即本指南)
2025 年第三季度:
完成官方插件和主题组件的升级
将 glimmer_post_stream_mode默认值设置为auto,并启用控制台弃用消息
启用带有管理员警告横幅的弃用消息(计划于 2025 年 7 月实施)- 第三方插件和主题应进行更新
2025 年第四季度:
默认启用新的帖子流
移除功能标志设置和遗留代码
这对我意味着什么?
如果您的任何插件或主题使用了任何“小部件”API 来自定义帖子流,则需要更新它们以适配新版本。
我如何尝试新的帖子流?
要尝试新的帖子流,只需在站点设置中将 glimmer_post_stream_mode 设置更改为 auto。如果您没有安装不兼容的插件或主题,这将启用新的帖子流。
当 glimmer_post_stream_mode 设置为 auto 时,Discourse 会自动检测不兼容的插件和主题。如果发现任何不兼容项,您将在浏览器控制台中看到有帮助的警告消息,明确指出哪些插件或主题需要更新,并附带堆栈跟踪以帮助您定位相关代码。
这些消息将帮助您准确识别插件或主题中需要更新以兼容新的 Glimmer 帖子流的部分。
如果您在使用新帖子流时遇到任何问题,请不用担心。目前,您仍可以将该设置改为 disabled 以返回旧系统。如果您安装了不兼容的扩展但仍想尝试,作为管理员,您可以将选项设置为 enabled 以强制启用新帖子流。但请务必谨慎使用——根据您的自定义内容,您的站点可能无法正常工作。
我安装了自定义插件或主题。我需要更新它们吗?
如果您的插件或主题执行了以下任何自定义操作,则需要进行更新:
-
在以下小部件上使用
decorateWidget、changeWidgetSetting、reopenWidget或attachWidgetAction:actions-summaryavatar-flairembedded-postexpand-hiddenexpand-post-buttonfilter-jump-to-postfilter-show-allpost-articlepost-avatar-user-infopost-avatarpost-bodypost-contentspost-datepost-edits-indicatorpost-email-indicatorpost-gappost-group-requestpost-linkspost-locked-indicatorpost-meta-datapost-noticepost-placeholderpost-streampostposter-nameposter-name-titleposts-filtered-noticereply-to-tabselect-posttopic-post-visited-line
-
使用了以下 API 方法之一:
addPostTransformCallbackincludePostAttributes
如果您有扩展使用了上述任何自定义操作,当您访问主题页面时,控制台将显示警告,指出需要升级的插件或组件。
弃用 ID 为:
discourse.post-stream-widget-overrides
如果您在实例中使用了多个主题,请务必检查所有主题,因为警告仅会出现在活动的插件和当前使用的主题及主题组件上。
替代方案是什么?
新的 Glimmer 帖子流为您提供了几种自定义帖子显示方式:
- 插件出口(Plugin Outlets):用于在帖子流的特定点添加内容。
- 转换器(Transformers):用于自定义项目、修改数据结构或更改组件行为。
用 addTrackedPostProperties 替换 includePostAttributes
如果您的插件使用 includePostAttributes 向帖子模型添加属性,则需要更新为使用 addTrackedPostProperties。
之前:
api.includePostAttributes('can_accept_answer', 'accepted_answer', 'topic_accepted_answer');
之后:
api.addTrackedPostProperties('can_accept_answer', 'accepted_answer', 'topic_accepted_answer');
addTrackedPostProperties 函数将属性标记为跟踪属性,以便在帖子更新时生效。如果您的插件向帖子添加属性并在渲染时使用它们,这一点至关重要。它确保 UI 在这些属性更改时自动更新。
常见迁移模式
使用插件出口
插件出口是您在帖子流特定点添加内容的主要工具。可以将它们想象为您可以注入自定义内容的指定位置。
它们是迁移脱离小部件装饰的关键。
Glimmer 帖子流将经常自定义的内容封装在出口中。使用插件 API 函数 renderBeforeWrapperOutlet 和 renderAfterWrapperOutlet 可以在它们之前或之后插入内容。
1. 用插件出口替换小部件装饰
最常见的自定义是向帖子添加内容。在小部件系统中,您会使用 decorateWidget。使用 Glimmer 时,您将改用插件出口。
之前:
// 插件初始化器的一部分
import { withPluginApi } from "discourse/lib/plugin-api";
// ... 其他导入
function customizeWidgetPost(api) {
api.decorateWidget("post-contents:after-cooked", (helper) => {
const post = helper.getModel();
if (post.post_number === 1 && post.topic.accepted_answer) {
return helper.attach("solved-accepted-answer", { post });
}
});
}
export default {
name: "extend-for-solved-button",
initialize() {
withPluginApi((api) => {
// ... 其他自定义
customizeWidgetPost(api);
});
}
};
之后:
// 插件 .gjs 初始化器的一部分
import Component from "@glimmer/component";
import { withPluginApi } from "discourse/lib/plugin-api";
import SolvedAcceptedAnswer from "../components/solved-accepted-answer";
// ... 其他导入
function customizePost(api) {
api.renderAfterWrapperOutlet(
"post-content-cooked-html",
class extends Component {
static shouldRender(args) {
return args.post?.post_number === 1 && args.post?.topic?.accepted_answer;
}
<template>
<SolvedAcceptedAnswer
@post={{@post}}
/>
</template>
}
);
}
export default {
name: "extend-for-solved-button",
initialize() {
withPluginApi((api) => {
// ... 其他自定义
customizePost(api);
});
}
};
2. 在发布者名称后添加内容
如果您的插件在发布者名称后添加内容,您将使用 renderAfterWrapperOutlet API 和 post-meta-data-poster-name 出口。
之前:
// 插件初始化器的一部分
import { withPluginApi } from "discourse/lib/plugin-api";
// ... 其他导入
function customizeWidgetPost(api) {
api.decorateWidget(`poster-name:after`, (dec) => {
if (!isGPTBot(dec.attrs.user)) {
return;
}
return dec.widget.attach("persona-flair", {
personaName: dec.model?.topic?.ai_persona_name,
});
});
}
export default {
name: "ai-bot-replies",
initialize() {
withPluginApi((api) => {
// ... 其他自定义
customizeWidgetPost(api);
});
}
};
之后:
// 插件 .gjs 初始化器的一部分
import Component from "@glimmer/component";
import { withPluginApi } from "discourse/lib/plugin-api";
// ... 其他导入
function customizePost(api) {
api.renderAfterWrapperOutlet(
"post-meta-data-poster-name",
class extends Component {
static shouldRender(args) {
return isGPTBot(args.post?.user);
}
<template>
<span class="persona-flair">{{@post.topic.ai_persona_name}}</span>
</template>
}
);
}
export default {
name: "ai-bot-replies",
initialize() {
withPluginApi((api) => {
// ... 其他自定义
customizePost(api);
});
}
};
3. 在帖子内容前添加内容
// 主题 .gjs 初始化器的一部分
import Component from "@glimmer/component";
import { withPluginApi } from "discourse/lib/plugin-api";
// ... 其他导入
function customizePost(api) {
api.renderBeforeWrapperOutlet(
"post-article",
class extends Component {
static shouldRender(args) {
return args.post?.topic?.pinned;
}
<template>
<div class="pinned-post-notice">
这是一个置顶主题
</div>
</template>
}
);
}
export default {
name: "pinned-topic-notice",
initialize() {
withPluginApi((api) => {
// ... 其他自定义
customizePost(api);
});
}
};
4. 在帖子内容后添加内容
// 主题 .gjs 初始化器的一部分
import Component from "@glimmer/component";
import { withPluginApi } from "discourse/lib/plugin-api";
// ... 其他导入
function customizePost(api) {
api.renderAfterWrapperOutlet(
"post-article",
class extends Component {
static shouldRender(args) {
return args.post?.wiki;
}
// 在实际组件中,您将使用如下模板:
<template>
<div class="wiki-post-notice">
此帖子是维基帖
</div>
</template>
}
);
}
export default {
name: "wiki-post-notice",
initialize() {
withPluginApi((api) => {
customizePost(api);
// ... 其他自定义
});
}
};
使用转换器
转换器是自定义 Discourse 组件的强大方式。它们允许您修改数据或组件行为,而无需覆盖整个组件。
以下是与帖子流自定义最相关的值转换器:
| 转换器名称 | 描述 | 上下文 |
|---|---|---|
post-class |
自定义应用于主帖子元素的 CSS 类。 | { post } |
post-meta-data-infos |
自定义为帖子显示的元数据组件列表。这允许添加、删除或重新排序项目,如帖子日期、编辑指示器等。 | { post, metaDataInfoKeys } |
post-meta-data-poster-name-suppress-similar-name |
决定是否在用户名与全名相似时抑制显示全名。返回 true 以抑制。 |
{ post, name } |
post-notice-component |
自定义或替换用于渲染帖子通知的组件。 | { post, type } |
post-show-topic-map |
控制第一个帖子上主题地图组件的可见性。 | { post, isPM, isRegular, showWithoutReplies } |
post-small-action-class |
向小型操作帖子添加自定义 CSS 类。 | { post, actionCode } |
post-small-action-custom-component |
用自定义 Glimmer 组件替换标准的小型操作帖子。 | { post, actionCode } |
post-small-action-icon |
自定义小型操作帖子使用的图标。 | { post, actionCode } |
poster-name-class |
向发布者名称容器添加自定义 CSS 类。 | { user } |
1. 向帖子添加自定义类
// 插件初始化器的一部分
import { withPluginApi } from "discourse/lib/plugin-api";
function customizePostClasses(api) {
api.registerValueTransformer(
"post-class",
({ value, context }) => {
const { post } = context;
// 为特定用户的帖子添加自定义类
if (post.user_id === 1) {
return [...value, "special-user-post"];
}
return value;
}
);
}
export default {
name: "custom-post-classes",
initialize() {
withPluginApi((api) => {
// ... 其他自定义
customizePostClasses(api);
});
}
};
2. 添加自定义帖子元数据
post-meta-data-infos 转换器允许您向帖子元数据部分添加自定义组件。
// 插件初始化器的一部分
import { withPluginApi } from "discourse/lib/plugin-api";
function customizePostMetadata(api) {
// 定义要在元数据部分使用的组件
// 组件应在转换器回调之外创建,
// 否则可能导致内存问题。
const CustomMetadataComponent = <template>...</template>;
api.registerValueTransformer(
"post-meta-data-infos",
({ value: metadata, context: { post, metaDataInfoKeys } }) => {
// 仅为特定帖子添加组件
if (post.some_custom_property) {
metadata.add(
"custom-metadata-key",
CustomMetadataComponent,
{
// 将其放置在日期之前
before: metaDataInfoKeys.DATE,
// 并在回复标签之后
after: metaDataInfoKeys.REPLY_TO_TAB,
}
);
}
}
);
}
export default {
name: "custom-post-metadata",
initialize() {
withPluginApi((api) => {
// ... 其他自定义
customizePostMetadata(api);
});
}
};
以下是来自 discourse-activity-pub 插件的真实示例:
// discourse-activity-pub 插件初始化器的一部分
import { withPluginApi } from "discourse/lib/plugin-api";
import ActivityPubPostStatus from "../components/activity-pub-post-status";
import {
activityPubPostStatus,
showStatusToUser,
} from "../lib/activity-pub-utilities";
function customizePost(api, container) {
const currentUser = api.getCurrentUser();
const PostMetadataActivityPubStatus = <template>
<div class="post-info activity-pub">
<ActivityPubPostStatus @post={{@post}} />
</div>
</template>;
api.registerValueTransformer(
"post-meta-data-infos",
({ value: metadata, context: { post, metaDataInfoKeys } }) => {
const site = container.lookup("service:site");
const siteSettings = container.lookup("service:site-settings");
if (
site.activity_pub_enabled &&
post.activity_pub_enabled &&
post.post_number !== 1 &&
showStatusToUser(currentUser, siteSettings)
) {
const status = activityPubPostStatus(post);
if (status) {
metadata.add(
"activity-pub-indicator",
PostMetadataActivityPubStatus,
{
before: metaDataInfoKeys.DATE,
after: metaDataInfoKeys.REPLY_TO_TAB,
}
);
}
}
}
);
}
export default {
name: "activity-pub",
initialize(container) {
withPluginApi((api) => {
customizePost(api, container);
// ... 其他自定义
});
}
};
5. 在帖子渲染内容前后插入内容
如果您的插件在帖子文本后添加内容,您将使用 renderAfterWrapperOutlet API 和 post-content-cooked-html 出口。
之前:
// 插件初始化器的一部分
import { withPluginApi } from "discourse/lib/plugin-api";
function customizeCooked(api) {
api.decorateWidget("post-contents:after-cooked", (helper) => {
const post = helper.getModel();
if (post.wiki) {
const banner = document.createElement("div");
banner.classList.add("wiki-footer");
banner.textContent = "此帖子是维基帖";
element.prepend(banner);
}
});
}
export default {
name: "wiki-footer",
initialize() {
withPluginApi((api) => {
// ... 其他自定义
customizeCooked(api);
});
}
};
之后:
// 插件初始化器的一部分 (.gjs)
import Component from "@glimmer/component";
import { withPluginApi } from "discourse/lib/plugin-api";
// 定义要在插件出口中使用的组件
class WikiBanner extends Component {
static shouldRender(args) {
return args.post.wiki;
}
<template>
<div class="wiki-footer">此帖子是维基帖</div>
</template>
}
function customizePost(api) {
// 使用 renderBeforeWrapperOutlet 在帖子内容前添加内容
api.renderAfterWrapperOutlet(
"post-content-cooked-html",
WikiBanner
);
}
export default {
name: "wiki-footer",
initialize() {
withPluginApi((api) => {
customizePost(api);
// ... 其他自定义
});
}
};
在过渡期间同时支持旧系统和新系统
作为插件或主题作者,您可能希望在过渡期间同时支持两个系统,以确保您的扩展同时兼容旧版和新版帖子流。
以下是许多官方插件使用的模式:
// solved-button.js
import Component from "@glimmer/component";
import { withSilencedDeprecations } from "discourse/lib/deprecated";
import { withPluginApi } from "discourse/lib/plugin-api";
import RenderGlimmer from "discourse/widgets/render-glimmer";
import SolvedAcceptedAnswer from "../components/solved-accepted-answer";
function customizePost(api) {
// glimmer 帖子流自定义
api.renderAfterWrapperOutlet(
"post-content-cooked-html",
class extends Component {
static shouldRender(args) {
return args.post?.post_number === 1 && args.post?.topic?.accepted_answer;
}
<template>
<SolvedAcceptedAnswer
@post={{@post}}
@decoratorState={{@decoratorState}}
/>
</template>
}
);
// ...
// 包装旧的 widget 代码并静默弃用警告
withSilencedDeprecations("discourse.post-stream-widget-overrides", () =>
customizeWidgetPost(api)
);
}
// 旧的 widget 代码
function customizeWidgetPost(api) {
api.decorateWidget("post-contents:after-cooked", (helper) => {
let post = helper.getModel();
if (helper.attrs.post_number === 1 && post?.topic?.accepted_answer) {
// 使用 RenderGlimmer 在 widget 系统中渲染 Glimmer 组件
return new RenderGlimmer(
helper.widget,
"div",
<template><SolvedAcceptedAnswer @post={{@data.post}} /></template>
null,
{ post }
);
}
});
}
export default {
name: "extend-for-solved-button",
initialize(container) {
const siteSettings = container.lookup("service:site-settings");
if (siteSettings.solved_enabled) {
withPluginApi((api) => {
customizePost(api);
// ... 其他自定义
});
}
},
};
真实示例
以下是来自我们官方插件的实际迁移拉取请求链接。这些展示了我们如何更新每种类型的自定义:
- discourse-ai
- discourse-assign
- discourse-cakeday
- discourse-post-voting
- discourse-reactions
- discourse-shared-edits
- discourse-solved
- discourse-topic-voting
- discourse-user-notes
- discourse-activity-pub
故障排除
启用新帖子流后我的站点显示异常
- 将
glimmer_post_stream_mode改回disabled - 检查控制台中的具体错误消息
- 在再次尝试之前更新有问题的插件/主题
我没有看到任何警告,但我的自定义不起作用
- 检查您的自定义是否针对上述列出的小部件
- 确保您在自定义可见的主题页面上进行测试
需要帮助?
如果您发现了错误,或者无法使用我们引入的新 API 实现您的自定义,请在下方告诉我们。