cover

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 是一个很便利的工具,但对于它的实现有必要更深入了解。希望这篇文章能让大家避免遇到同样错误。

3