我一直在折腾,试图让网站呈现出与众不同的外观,并制作了一个 Facebook 风格的帖子模态框。对我来说,它的加载速度似乎更快,在桌面端上的表现也更流畅。
大家觉得怎么样?
测试链接是 https://www.carptalk-online.co.uk/ (仅在桌面端有效)
我一直在折腾,试图让网站呈现出与众不同的外观,并制作了一个 Facebook 风格的帖子模态框。对我来说,它的加载速度似乎更快,在桌面端上的表现也更流畅。
大家觉得怎么样?
测试链接是 https://www.carptalk-online.co.uk/ (仅在桌面端有效)
干得漂亮!虽然我不是 Facebook 用户,但我挺喜欢这个的。你的网站很酷。![]()
另外,我没想到鲤鱼能长这么大!![]()
太酷了!你在回复数超过20的主题中测试过吗?在滚动加载更多“页”帖子时,根据我的经验,这是这种替代视图最大的复杂之处之一。
不,我现在就去做,然后回来汇报
运行正常 - URL 已删除
正如您所说,我可以看到页面正在加载,但它的表现符合预期
点击直接链接时,主题不会在模态框中显示。但从主题列表中可以正常显示。
嗯,那是我接下来需要研究的事情,但我一直在思考这个问题:这真的重要吗?这和 Facebook 的行为是一样的,直接链接指向实际帖子,如果在浏览时点击,则会打开模态窗口。
概念不错,在加载旧消息时,你能否保持与消息总线的稳定连接以接收新消息?
是的,因为模态框是通过 iframe 加载常规的 Discourse 主题应用,而不是渲染主题的自定义副本。该 iframe 会建立自己的 MessageBus 连接,Discourse 会继续以常规方式处理帖子流,包括加载旧帖子和接收新发布的帖子。
父页面在模态框背后仍然保持活动状态,因此从技术上讲,当模态框打开时,存在两个 Discourse 实例和两个 MessageBus 连接。这种方式可行,尽管比完全原生的实现稍微重一些。关闭模态框会移除 iframe 及其连接。
我不知道这是否有关系,而且我也不用 Facebook ![]()
我只是感到惊讶,点击你的链接并没有在模态框中显示该主题,而你发布它正是为了展示它能在模态框中正常工作。
我最初的帖子是主页的链接,你点击了我回复 Kris 测试主题的那个帖子吗,哈哈。不管怎样,最近怎么样?好久没见你了。
刚刚完成了移动版的开发,对目前的运行效果挺满意的。下一步是寻找更好的实现方式,以优化加载速度。大家有什么建议吗?
经过更多测试,我使用了 Google 看到的非 JavaScript 版本,它在模态框中加载速度极快。如果我能以某种方式利用该内容,就能实现快速响应。
在电脑端看起来非常不错,
在移动端的加载速度有点缓慢,点击后只能看到一个无内容的页面和右上角的关闭按钮,似乎需要面对一个空白页面等待很久。
这就是我现在正在做的项目
目前这还只是一个我想实现的概念。不过,在我的设备上,它通过 Discourse 的加载图标在移动端只需 2 秒就能加载完毕,所以我不太确定具体出了什么问题。可能是因为服务器位于英国伦敦,而且目前还没有使用 CDN。
那确实距离是太远了,所以网络需要时间传输
我现在已经让主题在模态框中即时加载,接下来需要找到一种方法,以便对回复等使用首次加载的 Discourse 实例。(这是在测试服务器上)
我已在主站关闭了此功能,因为现在有人注册并带来了流量,所以它已被移至测试服务器。如果大家对模态框感兴趣,希望能在八月中旬前完成。
你好 ![]()
虽然采用了与 @Damian_Boon 不同的方法:不使用 iframe,所有内容都作为原生 Glimmer 组件运行,并复用 Discourse 自身的 Post/post-stream 机制。由于在讨论串中出现了一些具体问题,以下是我最终解决每个问题的方法。
为什么不用 iframe
iframe 无疑是实现这一目标最快的方法,完整的话题路由、编辑器、 moderation 功能,全部 免费 提供。代价(正如 @nicolsdennis 关于 MessageBus 的问题所指出的)是页面上最终会出现两个活跃的 Discourse 实例:两个 post-stream、两个 MessageBus 连接、两个编辑器。
我选择了另一条路。模态框是一个 DModal,它针对一个由 store.createRecord("topic", …) 构建的 postStream 渲染真实的 Post/PostSmallAction 组件,使用同一个应用、同一个 MessageBus 连接、同一个编辑器。这意味着只要模态框处于打开状态,就需要将少数核心单例服务(modal、bookmarkApi、DiscourseURL.routeTo、环境 controller:topic)指向模态框的话题:
// 核心组件(例如 PostBookmarkManager)从 controller:topic 读取/写入 topic.bookmarks
// 在模态框打开期间,将其指向我们的 topicModel。
this.topicController = getOwner(this).lookup("controller:topic");
if (this.topicController) {
this.originalTopicControllerModel = this.topicController.model;
}
每个补丁在关闭、销毁或导航离开时,都会通过幂等的 restoreServicePatches() 进行还原,因此一旦模态框消失,就不会有任何内容泄漏到应用的其余部分。
直接链接与内部路由
分为两部分。首先,模态框应该在读者实际停留的位置打开,而不是始终在第 1 楼:
const lastRead = this.topic.last_read_post_number ?? 0;
const highestPostNumber = this.topic.highest_post_number ?? 1;
const initialPostNumber = Math.max(
1,
Math.min(lastRead + 1, highestPostNumber)
);
其次,帖子 内部 指向同一话题的链接不应跳出模态框。DiscourseURL.routeTo 在其打开期间会被修补:
this.originalRouteTo = DiscourseURL.routeTo.bind(DiscourseURL);
DiscourseURL.routeTo = (path, opts) => {
if (typeof path === "string") {
const match = path.match(/^\/t\/(?:[^/]+\/)?(\d+)(?:\/(\d+))?/);
if (match && parseInt(match[1], 10) === this.topicId) {
this.jumpToPost(match[2] ? parseInt(match[2], 10) : 1);
return Promise.resolve();
}
}
this.restoreServicePatches();
return this.originalRouteTo(path, opts);
};
任何指向其他地方的链接都会先还原补丁,然后回退到正常导航,因此离开模态框时不会留下过期的路由状态。
回复情况(20+ 条回复)
通过哨兵元素 + IntersectionObserver 实现下方的无限滚动,上方对称的“加载更早”功能(预览可以在中途打开),两者都驱动真实话题路由所使用的确切 postStream.appendMore() / prependMore():
sentinel = modifier((element) => {
const obs = new IntersectionObserver(
(entries) => {
if (entries.some((e) => e.isIntersecting)) {
this.loadBelow();
}
},
{
root: document.querySelector(".topic-preview-modal .d-modal__body"),
rootMargin: "200px",
}
);
obs.observe(element);
return () => obs.disconnect();
});
首次渲染
骨架屏(头像 + 线条占位符,闪烁效果)在首次渲染时填充,而不是使用加载旋转器,帖子列表本身分阶段渲染而不是一次性全部渲染,首先渲染足够多的帖子以到达目标帖子,然后通过 requestIdleCallback 每次再渲染五个,直到其余部分跟上:
const renderRemainingPosts = () => {
const totalPosts = this.postStream?.posts?.length || 0;
if (this.renderLimit < totalPosts) {
this.renderLimit = Math.min(this.renderLimit + 5, totalPosts);
if (this.renderLimit < totalPosts) {
if (window.requestIdleCallback) {
window.requestIdleCallback(renderRemainingPosts, { timeout: 300 });
} else {
setTimeout(renderRemainingPosts, 30);
}
return;
}
}
this.renderLimit = Number.MAX_SAFE_INTEGER;
};
每个帖子包装器还获得了 content-visibility: auto 和一个大致的 contain-intrinsic-size,因此浏览器完全跳过对屏幕外帖子的布局计算:
.topic-post {
contain: layout;
content-visibility: auto;
contain-intrinsic-size: 1px 180px;
}
这是我会最强烈推动的部分。与其在点击后重用实例,我在点击 之前 进行预取。每个话题列表行监控自身的可见性,防抖 200ms 以防止快速滚动触发十几个请求,并交由一个小型共享队列处理,该队列限制为最多 2 个并发获取:
function createTopicPrefetchQueue(maxConcurrent) {
const pending = [];
const activeIds = new Set();
function drain() {
while (activeIds.size < maxConcurrent && pending.length) {
const { id, task } = pending.shift();
activeIds.add(id);
Promise.resolve()
.then(task)
.finally(() => {
activeIds.delete(id);
drain();
});
}
}
return {
schedule(id, task) {
if (activeIds.has(id) || pending.some((item) => item.id === id)) {
return;
}
pending.push({ id, task });
drain();
},
cancel(id) {
const index = pending.findIndex((item) => item.id === id);
if (index !== -1) {
pending.splice(index, 1);
}
},
};
}
预取结果存储在私有 PreloadStore 键下,故意 不 使用核心 topic_${id} 键,因为在那里写入会将作用域为 last_read+1 的部分 JSON 泄漏到真实的话题完整导航中。在实际点击时,它会被提升为模态框加载器期望的键:
promoteTopicPrefetch(topic) {
if (!topic?.id) {
return;
}
try {
const key = this.prefetchStoreKey(topic.id);
const prefetched = PreloadStore.get?.(key);
if (prefetched != null) {
PreloadStore.remove?.(key);
PreloadStore.store(`topic_${topic.id}`, prefetched);
}
} catch {
// 尽力而为 - 模态框回退到全新加载
}
}
如果某行在点击前滚出视图,待处理的获取会被取消,任何缓存结果都会被丢弃,因此它只保留当前屏幕上实际存在的话题数据。
还有一个,讨论串中尚未提及的:嵌套模态框
一些模态框流程(核心自身打开的,而不是通过连接到 showSubModal() 的显式回调属性打开的)直接调用 modal 服务。为了防止这些操作从读者下方关闭 预览 模态框,modal.show() 被修补为选择性地将目标模态框重定向到一个小型本地子模态框插槽,只要预览处于打开状态:
this.originalModalShow = this.modal.show.bind(this.modal);
this.modal.show = (component, opts = {}) => {
return this.showSubModal(component, opts.model);
};
因此,这些目标流程会叠加在预览之上,而不是替换它,同时阅读跟踪(/topics/timings)通过定期刷新由 IntersectionObserver 跟踪的可见帖子编号集合在后台继续运行,因此关闭后的未读计数仍然正确,与正常访问话题相同。
这是跨越模态框、话题列表触发器和样式的大量代码(总共约 1700 行),这仅覆盖了其中的三分之一。迄今为止最棘手的部分是服务修补以及让 content-visibility 在图片加载完成后帖子高度发生变化时表现正常。如果感兴趣,我很乐意就其中任何部分提供更多细节,或将其开源。