前端开发者学堂 - fedev.cn

使用Intersection Observer API构建无限滚动组件

发布于 大漠

特别声明:本文根据@Alex Jover Morales的《Build an Infinite Scroll component using Intersection Observer API》一文所整理。

在开发过程中,经常会遇到要处理大量数据的情况,比如列表、搜索记录等,因此你需要一种方法让用户以块状显示这些数据,以便保持应用程序性能和数据的有序性。

你可能会使用分页组件来处理,它可以轻松跳转,甚至一次跳转几个页面。

当然,元素滚动是分页组件的另一种替代方案,它可以提供更好的用户体验,特别是在移动端和可触摸设备上。当用记在页面上滚动时,它提供了一个透明的分页,给人一种没有结尾列表的感觉。

自从Intersection Observer API出现之后,构建无限滚动组件变得更简单。让我们看看如何通过这个API来构建无限滚动组件。

Intersection Observer API

Intersection Observer API提供了一个可订阅的模型,可以观察该模型,以便在元素进入视窗时得到通知。

创建一个观察者实例很简单,我们只需要创建一个IntersectionObserve的新实例并调用observe方法,传递一个DOM元素:

const observer = new IntersectionObserver()

const coolElement = document.querySelector('#coolElement')
observer.observe(coolElement)

但是,当观察者进入coolElement视窗时,我们如何得到通知呢?IntersectionObserver构造函数接受一个回调作为参数,我们可以这样使用它:

const observer = new IntersectionObserver(entries => {
    const firstEntry = entries[0]

    if (firstEntry.isIntersecting) {
        // Handle intersection here...
    }
})

const coolDiv = document.querySelector('#coolDiv')
observer.observe(coolDiv)

如你所见,回调将接收entries作为其参数。它是一个数组,因为当你使用阈值时,你可以有几个entries,但事实并非如此,所以我们只会得到第一个元素。然后我们可以使用firstEntry.isIntersection属性来检查它是否相交。这是进行异步请求并检索下一个页面数据的好方法。

IntersectionObserver构造函数使用下面的方法接受选项对象(options)为其第二个参数:

const options = {
    root: document.querySelector('#scrollArea')
    rootMargin: '0px'
    threshold: 1.0
}

const observer = new IntersectionObserver(callback, options)

rootMargin对于我们的示例非常有用,因为它提供了一种定义margin的方法,观察者可以使用它来查找交集。默认情况下,它是0,表示观察者一进入视窗就会触发交叉事件(Intersect Event)。但是设置一个400pxrootMargin意味着交叉回调将在元素进入视窗之前400px位置处触发。

因为rootthreshold对于这种呢况没有什么意义(因此超出了范围),有关于这方面可以查阅文档进行了解。

知道如何使用交点观察器,我们可以在列表末尾放置一个Observable组件,以便在用户到达列表底部时添加更多数据。

Observer组件

前面的例子很酷,对吧?但是对于我们来说有一个Vue组件是很方便的,所以我们可以在我们的Vue应用程序中使用它。

我们可以使用一个mounted钩子来创建我们需要保存在组件状态变量中的观察者。使用mounted钩子而不是created钩子很重要,因为我们需要一个DOM元素来观察,而在created钩子中我们没有它:

// Observer.vue
export default {
    data: () => ({
        observer: null
    }),

    mounted() {
        this.observer = new IntersectionObserver(([entry]) => {
            if (entry && entry.isIntersecting) {
                // ...
            }
        })

        this.observer.observe(this.$el)
    }
}

注意:我们在[entry] 参数上使用数组解构。这是一种速记方式,相当于获取entries数组并将第一个元素作为entries[0]访问。

正如你所见,我们使用this.$el作为root元素以便观察DOM元素。

为了使其可重用,我们需要让父组件(使用Observer组件的组件)处理交叉的事件。为此,我们在它相交时发出一个自定义事件intersect

export default {
    mounted() {
        this.observer = new intersectionObserver(([entry]) => {
            if (entry && entry.isIntersecting) {
                this.$emit('intersect')
            }
        })

        this.observer.observe(this.$el)
    }
    // ...
}

根据组件的模板,我们只需要任何元素,所以我们可以使用一个没有任何大小的<div>

<template>
    <div class="observer"/>
</template>

最后,在组件被销毁时清理观察者很重要,否则,我们将在应用程序中会造成内存泄漏,因为事件监听器不会被清除。我们可以在destroyed钩子中来调用observerdisconnect方法:

export default {
    destroyed() {
        this.observer.disconnect()
    }
    // ...
}

你会发现还有unobserve方法。主要区别是:

  • unobserve:停止观察一个元素
  • disconnect:停止观察所有元素

在我们的示例中,因为我们只有一个元素,所以它们都可以工作。

我们还可以添加一个options属性,以便在需要使用rootMargin的情奖品下将IntersectionObserver选项传递给它。

将所有内容放在Observer.vue组件中:

<!-- Observer.vue -->
<template>
    <div class="observer" />
</template>

<script>
    export default {
        props: ['options'],
        data: () => ({
            observer: null
        }),
        mounted() {
            const options = this.options || {}
            this.observer = new IntersectionObserver(([entry]) => {
                if (entry && entry.isIntersecting) {
                    this.$emit('intersect')
                }
            }, options)

            this.observer.observe(this.$el)
        },
        destroyed() {
            this.observer.disconnect()
        }
    }
</script>

构建元限滚动

假设你有一个列表组件,类似以下内容:

<template>
    <div>
        <ul>
            <li class="list-item" v-for="item in items" :key="item.id">{{ item.name }}</li>
        </ul>
    </div>
</template>

<script>
    export default {
        data: () => ({
            items: []
        }),
        async mounted() {
            const res = await fetch('https://jsonplaceholder.typicode.com/comment')
            this.items = await res.json()
        }
    }
</script>

请注意,代码中使用了asyncawait语法,使异步代码看起来很漂亮。有关于这方面的信息可以阅读这篇文章

这个组件使用v-foritems渲染到列表中。在mounted钩子中,它使用Fetch APIjsonplaceholder.typicode.com中获取一些模拟数据,用于填充items变量。

添加分页

它可以正常工作,但没有分页功能。为此,jsonplaceholder.typicode.com的端点允许我们使用_page_limit来控制返回的数据。此外,我们需要一个page变量来跟踪,它从1开始。

我来改变上面的代码,以便进行分页:

export default {
    data: () => ({
        page: 1,
        items: []
    }),
    async mounted() {
        const res = await fetch(
            `https://jsonplaceholder.typicode.com/comments?_page=${this.page}&_limit=50`
        )

        this.items = await res.json()
    }
}

现在我们有分页功能了,每个页限制50个元素。

添加Observer组件

我们仍然需要创建无限滚动组件,接下来把Observer组件引入进来。我们将在列表底部使用它,当它到达视窗时,它将获取下一页并增加该页。

首先,导入Observer组件,并将其添加到InfiniteScroll组件中:

<template>
    <div>
        <ul>
            <li class="list-item" v-for="item in items" :key="item.id">{{ item.name }}</li>
        </ul>
        <Observer @intersect="intersected" />
    </div>
</template>

<script>
    import Observer from './components/Observer'

    export default {
        // ...
        components: {
            Observer
        }
    }
</script>

最后,我们可以在mounted钩子上的代码移动intersected方法中。在Observer组件的intersect定制事件。

export default {
    data: () => ({
        page: 1,
        items: []
    }),
    methods: {
        async intersected() {
            const res = await fetch(`https://jsonplaceholder.typicode.com/comments?_page=${this.page}&_limit=50`)
        }

        this.page++

        const items = await res.json()
        this.items = [...this.items, ...items]
    }
}

请记信,我们必须增加页面。此外,现在我们必须将项添加到现有的this.items数组。我们通过在this.items=[...this.items, ...items]来实现。这基本上和老办法this.items = this.items.concat(items)是一样的。

无限滚动组件,全部代码看起来像下面这样:

<!-- InfiniteScroll.vue -->
<template>
    <div>
        <ul>
            <li class="list-item" v-for="item in items" :key="item.id">{{ item.name }}</li>
        </ul>
        <Observer @intersect="intersected" />
    </div>
</template>

<script>
    import Observer from './components/Observer'

    export default {
        data: () => ({
            page: 1,
            items: []
        }),
        methods: {
            async intersected() {
                const res = await fetch(`https://jsonplaceholder.typicode.com/comments?_page=${this.page}&_limit=50`)

                this.page++

                const items = await res.json()
                this.items = [...this.items, ...items]
            }
        },
        components: {
            Observer
        }
    }
</script>

总结

无限滚动组件是数据分页的一种很好的展示方式,特别是在移动设备和可触摸设备上。通过添加IntersectionObserver API,它变得更加容易。在本文中,我们已经完成了自己构建一个无限滚动组件所有步骤。

请记住,如果你需要支持旧的浏览器,你可能需要W3C的IntersectionObserverGithub上Fetch Polyfill

最终Demo效果如下:

NikeLab ACG.07.KMTR