当锚点定位遇到 Lazyload

421 阅读5分钟

当锚点定位遇到 Lazyload

在一个阳光明媚的下午,产品小姐姐突然召集我们开会,老板说我们做的页面很长,体验太差了,需要加上锚点才行;

后端说:我们这个页面访问量比较高,返回的数据多,接口速度也比较慢,接口调用的时候优化一下不要调用太多!

我说:锚点和懒加载我没听说过这样写的,这个需求可能做不了。

产品小姐姐一副央求的眼神说:这个需求是老板亲自提的,如果做不了我没法交差啊。

我心想:没法交差跟我有什么关系呢?不能实现就是不能实现嘛。但我碍于同事没这么说,我说:那我回去调研一下吧,能不能做尚不能下定论。

于是搜索引擎、chartGPT 各种工具一顿搜索,结果发现没有几个能用的答案,看来还是需要自己思考啊,于是我赶紧写了一个案例,案例如下:

stackblitz.com/edit/vitejs…

要解决问题必须先搞清楚问题双方都是什么,我是谁,我从哪里来,我到哪里去?首先是锚点定位的机制;

锚点定位的机制

锚点定位是利用 HTML 中 a 标签的 href 属性和 # 符号,指向页面中特定的元素,实现快速跳转到该元素的功能。

案例如下:developer.mozilla.org/zh-CN/play

也就是说锚点定位实现的前提是:元素已经渲染出来了,否则找不到这个特定 id 的元素,也就无法实现跳转。

lazyload 的机制

lazyload 的机制是延迟加载,只有当组件出现在可视区域内时才会加载。这能有效提高页面加载速度,减少初始加载时间。

lazyload 有两种实现方式,第一种:监听 scroll 事件,当到达目标组件时解锁 v-if 让目标组件开始加载;第二种:使用 IntersectionObserver 监听屏幕与元素的交集,当产生交集时解锁 v-if,关于IntersectionObserver的用法可以查看这里,题外话:另外还有三个 Observer 分别是 ResizeObserver(resize事件)、MutationObserver(DOM 树发生改变)、PerformanceObserver(性能监听)

核心问题: 当你点击锚点链接时,lazyload 组件还没有加载完成,导致页面无法定位到目标元素。即使组件加载完成,如果它本身是一个动态生成的元素,其 ID 可能还没有被注册到页面 DOM 中,同样会导致定位失败。

方案一:懒加载之后执行滚动

思路分析:翻阅 vue-lazyload 文档可以看到组件懒加载之后有一个 show 事件,show事件中可以更新当前组件的加载状态,为 Anchor 添加点击事件,点击事件中缓存点击的 id 元素,等到 show 事件触发时将记录的元素滚动到窗口中

代码实现:

const loaded = ref({
  '#HelloWorld1': false,
  '#HelloWorld2': false,
  '#HelloWorld3': false,
  '#HelloWorld4': false,
});
const cacheId = ref('');
function onShow(component, id) {
  if (id === cacheId.value) {
    const el = document.querySelector(id);
    if (el) {
      el.scrollIntoView();
    } else {
      setTimeout(() => {
        onShow(component, id);
      }, 100);
    }
  }
  loaded.value[id] = component;
}

function onJumpTo(id) {
  if (!loaded.value[id]) {
    cacheId.value = id;
    window.scrollTo(0, 10000);
  }
}

案例预览:stackblitz.com/edit/vitejs…

可以看到所有锚点均能准确定位,但是这段代码存在一个问题,请 jy 们思考一下到底有什么问题。

问题就出在这里:window.scrollTo(0, 10000);,我们这里滚动一次就能把所有资源加载完毕,但是假如页面非常长,滚动一次加载不完呢?这个时候应该怎么办?这个时候我们就需要不断执行滚动到底部的操作,直到加载出目标元素。这样的话页面就会滚动多次,体验不好,而且锚点定位的耗时较长;另外还有一个大问题就是:不该加载的元素也加载了,比如我就想看 4 对应的内容,1、2、3 的内容都不在意,按照现在的方案,要滚动到 4 则必须先加载 1、2、3。

方案一的优点:对于不长的页面能够通过简单的逻辑实现功能

缺点:比较长的页面定位体验差,性能差

此时我们通过脑海中的积累就可以想到一个神器:虚拟列表,虚拟列表又是何方神圣呢?虚拟列表就是我只渲染我看得到的内容,其他内容都不渲染,但是滚动条又滚动到了目标位置,其实就是个“障眼法”

方案二:虚拟列表(不可行)

d70a2d2da2d68de1.jpg

上面一张图就能够搞懂虚拟列表,虚拟列表其实就是只渲染可视区域+上下缓冲区的元素,是为了减少元素数量,缩小了 document 文档结构,自然提高了渲染效率和响应速度。然而虚拟列表渲染的前提是当前可视区域上面的元素都已经渲染过了,这样滚动条的滚动距离=已渲染元素高度 + 上缓冲区高度,而锚点的话它还不一定把目标元素上方渲染完成,因此虚拟列表好像完全没法帮上忙。

别灰心,我们可以尝试下通过锚点定位会触发 lazy-compoennt 的 show 事件吗?答案是不会,这样的话有一个思路就出来了:在点击 #4 时我们主动让 #4 对应的组件加载然后再次触发点击事件触发锚点定位

方案三:单独加载锚点跳转(不可行)

const lazyComponent4 = useTemplateRef('lazy-component4');
const HelloWorld4 = useTemplateRef('HelloWorld4');

function onJumpTo(id) {
  //   if (!loaded.value[id]) {
  //     cacheId.value = id;
  //     window.scrollTo(0, 10000);
  //   }
  if (id === '#HelloWorld4' && !lazyComponent4.show && lazyComponent4) {
    lazyComponent4.load();
    HelloWorld4.$el.click();
  }
}

然而很可惜并不能主动让 lazy-compoennt 展现出来,可以查看示例演示

综上所述

当锚点遇到 Lazyoload 基本是无解的,如果页面不是那么的长还是好解决的,这也从侧面提醒我们页面不应该设计的太长,长度需要有一个限定。

最后,我跟产品说了一句经典的话,那就是:这个需求我做不了

jym 如果有什么不同的想法可以一起讨论一下。