返回

Vue 实现容器内元素滚动控制:解决多按钮无效问题

vue.js

实现容器内元素的滚动控制:方法与实践

概述

在网页开发过程中,让用户能够方便地浏览长列表或横向布局的内容是常见的需求。特别是在类似Netflix的媒体展示界面上,水平滚动的电影或剧集列表提供了一种直观的交互方式。要控制指定容器内的元素水平滚动,按钮常常被选用。这篇文章主要解决实现多个滚动控制按钮,让每个按钮只控制对应的容器内元素的滚动问题。以下分析可能的原因以及如何提供可靠的解决方案。

问题原因分析

从代码中可以看出,“电影” 和 “电视剧” 各有一个列表容器以及其各自控制滚动按钮。但是发现,点击电影列表容器的按钮是有效的,而点击电视列表的无效

造成该问题的核心在于 scroll 函数的设计。尽管该函数接受了一个参数来指明滚动操作的目标容器ID,但直接将 ref 的字符串传入该方法中会导致 Vue 组件引用不正确。每个具有 ref 属性的元素都会添加到其父组件的 $refs 对象中。问题根源就在于 $refs[id] 这句话。组件会正确创建和更新它的 $refs 对象,但是 movieScrollContainer 只注册一次,导致 “电视列表容器” 获取不到它的 $refs 元素。Vue 通过 refs 对象引用其模板中的一个 DOM 元素。如果 id 参数不是引用了一个有效的 $refs,则无法正常操作 DOM 元素进行滚动

解决方案:调整获取 DOM 元素的方式

要处理不同容器之间的滚动,需要确保 scroll 函数每次都能找到正确的DOM元素,也就是相应的滚动容器。通过稍微调整对元素引用和使用它们的方式可以避免以上问题的出现。可以使用以下策略改进代码:

  1. 利用 event.target 。可以通过获取点击按钮所在的目标来判断按钮处于哪个滚动列表区域,然后遍历目标元素的 parentNode 来查找到相应的容器元素进行处理即可。

  2. 修改为动态创建 $refs 。让 $refs 和你的数据 store 进行联动,根据数据来动态创建 $refs

解决方案1:利用 event.target 获取滚动容器

这种方法可以使 scroll 函数从触发事件的元素本身出发,向上查找DOM树以找到正确的滚动容器。利用的是父节点的树状关系来进行实现,通过查找事件按钮来找到 movie 的节点或者 tv 的节点,然后根据其子元素,获取到该节点下唯一的列表容器元素 movieScrollContainer。然后执行后续操作。这种方法有很好的鲁棒性。

操作步骤:

  1. 更新 scroll 方法的签名,使其接收一个额外的参数,用于获取事件对象 event

  2. 修改调用 scroll 函数时传递事件对象。

  3. 调整 scroll 方法内部逻辑,用它来寻找并使用正确的滚动容器。

代码修改:

首先, 修改两个模板中对于 scroll 函数的调用。因为多了一个 event 参数需要传递。

<button @click="scroll($event, -600, 'movieScrollContainer')">
    scroll left
</button>
<button @click="scroll($event, 600, 'movieScrollContainer')">
    scroll right
</button>

<button @click="scroll($event, -600, 'tvScrollContainer')">
    scroll left
</button>
<button @click="scroll($event, 600, 'tvScrollContainer')">
    scroll right
</button>

然后,在方法 scroll 内部通过获取事件按钮的节点对象进行遍历:

methods: {
    scroll(event, distance, id) {
        // 先找到 event 按钮节点的父元素,因为按钮外面有个父元素
        let parentNode = event.target.parentNode;

        // 然后, 判断该父节点下面是否有所需的容器
        let container = parentNode.previousElementSibling;

        // 有才继续操作
        if (container && container.getAttribute('ref') === id) {
            container.scrollBy({
                left: distance,
                behavior: 'smooth'
            });
        }
    }
}

通过上述修改,scroll 方法根据事件对象的当前位置在页面中,查找它最近的具备对应 ref 属性值的父元素,并对该父元素进行滚动操作。该代码不需要预先知道滚动容器 ref 的名称,即可对其操作。代码也更健壮,无需担心添加多少个按钮和列表而引发故障。

解决方案2:修改为动态创建 $refs

通过动态创建 $refs 可以将多个重复的 $refs 组件关联上一个数据,例如将 movie 的节点下的列表容器关联到 movieScrollContainer[index]。具体做法可以根据 movietv 两个数组进行关联,数组的每一个元素对应一个列表组件 ref。需要注意的是,这里只修改了电影列表和电视剧列表,并没有对其内部的 CardComponent 子组件做改动。这里可以自己根据需求将这些改动应用到更细粒度的元素中。

操作步骤:

  1. 修改组件结构体,为循环添加动态 ref 索引。

  2. 更新 scroll 方法,让其接收循环的下标值,可以更加便捷地计算当前列表的 ref

代码修改:

先看 <template> 部分。针对电影容器进行调整:

<!-- 电影容器 -->
<div
  v-for="(container, index) in [store.movie]"
  :key="'movieContainer' + index"
  class="container pt-5 pb-5"
>
  <!-- 原来的逻辑,创建唯一的 ref -->
  <!-- <div ref="movieScrollContainer" class="dflex"> -->

  <!-- 根据传入的索引计算 ref 值,注意数组的写法。传入的 ref 值是 "movieScrollContainer",其获取方式就是通过 [0][0][index] -->
  <div :ref="'movieScrollContainer[' + index + ']'" class="dflex">
    <div
      class="ls-col"
      v-for="movie in container"
      :key="'movie' + movie.id"
    >
      <CardComponent
        :img="store.ImageUrl + movie.poster_path"
        :name="movie.title"
        :originalName="movie.original_title"
        :language="movie.original_language"
        :vote="movie.vote_average"
        :overview="movie.overview"
      />
    </div>
  </div>

  <div class="d-flex justify-content-between">
    <!-- 针对当前 movie 的 ref 的列表添加下标 index 即可 -->
    <button @click="scroll(-600, 'movieScrollContainer', index)">
      scroll left
    </button>

    <button @click="scroll(600, 'movieScrollContainer', index)">
      scroll right
    </button>
  </div>
</div>

<!-- 电视剧容器 -->
<div
  v-for="(container, index) in [store.tv]"
  :key="'tvContainer' + index"
  class="container pt-5 pb-5"
>
  <!-- 根据传入的索引计算 ref 值,注意数组的写法。传入的 ref 值是 "tvScrollContainer",其获取方式就是通过 [0][0][index] -->
  <div :ref="'tvScrollContainer[' + index + ']'" class="dflex">
    <div
      class="ls-col"
      v-for="series in container"
      :key="'series' + series.id"
    >
      <CardComponent
        :img="store.ImageUrl + series.poster_path"
        :name="series.name"
        :originalName="series.original_name"
        :language="series.original_language"
        :vote="series.vote_average"
        :overview="series.overview"
      />
    </div>
  </div>

  <div class="d-flex justify-content-between">
    <!-- 针对当前 tv 的 ref 的列表添加下标 index 即可 -->
    <button @click="scroll(-600, 'tvScrollContainer', index)">
      scroll left
    </button>

    <button @click="scroll(600, 'tvScrollContainer', index)">
      scroll right
    </button>
  </div>
</div>

这段代码针对列表容器动态添加了索引。v-for 循环用于遍历两个数据,store.moviestore.tv[store.movie] 的写法使其返回的 index 从 0 开始递增。该方法使循环中所有的节点对象添加上唯一标识 movieContainer 后面带上当前是第几个元素。列表 div 的动态 ref 保证每个滚动列表对象都有正确的 ref 进行引用。movieScrollContainer[index] 这是一种动态属性的写法,注意其中的写法即可。最后就是更新对应的点击按钮,加入下标 index 的参数进行区分了。

最后修改 <script> 部分的 scroll 函数即可:

methods: {
  scroll(distance, id, index) {
    // 先根据参数计算正确的 refs 索引
    const refIndex = id + '[' + index + ']';

    // 通过 $refs[refIndex] 获取当前滚动列表的对象,注意这里的下标是 [0]
    const container = this.$refs[refIndex][0];

    // 执行容器的滚动即可
    container.scrollBy({
      left: distance,
      behavior: 'smooth'
    });
  }
}

scroll 函数添加额外的下标参数进行识别。并在代码中使用 refIndex 来操作 ref 元素。这段代码根据传入的循环 index 计算其对应的 $refs 的值即可。然后使用 this.$refs[refIndex][0] 即可获取到正确列表元素的 DOM 对象了。通过动态 refs,避免对 DOM 树中列表容器的查找。相比上一种更加高效和简便。

安全建议

  • 限制滚动距离。 为了避免潜在的注入攻击,可以检查滚动操作中的距离值。例如,如果只接受特定的值,可以设置验证条件:if (distance === 600 || distance === -600)

  • 对动态 $refs 中的下标添加有效判断。 在获取当前数组或者列表的长度 arr 前,必须先保证该数组或者列表非空。并且循环变量的取值必须保证:0<=index<arr,这对于防止数组越界异常有效。可以避免访问一些超出预期范围之外的引用。