Turbo frame 的 lazy loading 会在什么时候执行
最近 GeekNote 发现了一个 Bug,所有未登录用户在访问文章页面时会跳转到登陆页面。这看起来就像那些封闭花园式的发布平台那样,导致流失了很多潜在用户。
经过调试,我发现 Bug 是由这段代码引起的(已简化):
<div class="dialog">
<turbo-frame loading="lazy" src="...">
</turbo-frame>
</div>
Dialog 是我自己实现的一个 CSS 组件,默认情况下不可见(visibility: hidden;
),当触发显示逻辑的时候则加上 visibility: visible;
使其可见。
而 turbo-frame 的 loading="lazy"
的文档描述是:
Like an eager-loaded frame, but the content is not loaded from src until the frame is visible.
所以理想情况下,只有用户触发了 dialog 时,turbo-frame 才会载入 src 的内容。但实际上,用户一进入页面就触发了 loading,而加载这个资源触发了需要登录的过滤器,导致页面重定向。
延迟加载的原理
要修复这个问题,有必要理解 turbo frame 的延迟加载是怎么实现的。在 turbo 的源码里搜索 lazy
很快便找到这段代码(frame_controller.ts#L82-L86):
if (this.loadingStyle == FrameLoadingStyle.lazy) {
this.appearanceObserver.start()
} else {
this.loadSourceURL()
}
接着深入,AppearanceObserver
类中有这样一行:
this.intersectionObserver = new IntersectionObserver(this.intersect)
所以,延迟加载是依赖 IntersectionObserver
实现的。IntersectionObserver 是浏览器的原生接口,根据文档描述:
Intersection Observer API 提供了一种异步检测目标元素与祖先元素或 viewport 相交情况变化的方法。
这里关注它的描述:检测目标元素与祖先元素或 viewport 相交情况变化。也就是说,这个 API 检测的是元素是否与可视区域相交,而不是元素是否视觉可见。如果元素进入了可视区域,即使它是 visibility: hidden;
也会触发 callback。
恰好我实现的 Dialog 默认情况下是与可视区域相交的,只是视觉不可见,这导致了 turbo frame 错误触发加载。
针对这个问题,有一个 IntersectionObserver V2 的提案出现,在 API 中添加检测元素是否可视的属性,但目前只有 Chrome 实现。
解决方案
要解决这个问题,只要让 dialog 未打开时 turbo frame 不与可视区域相交就行了,于是我用 CSS 打了个补丁:
.dialog turbo-frame[loading="lazy"] {
display: none;
}
.dialog--open turbo-frame[loading="lazy"] {
display: block;
}
后来为了稳妥起见,干脆让这个 dialog 在用户未登录时不渲染:
<% if logined? %>
<div class="dialog">
<turbo-frame loading="lazy" src="...">
</turbo-frame>
</div>
<% end %>
经过以上修改后,问题解决。
讨论
Turbo frame 是一个很便利的工具,但对于它的实现有必要更深入了解。希望这篇文章能让大家避免遇到同样错误。