Svelte 中的懒加载图片

Avatar of Donovan Hutchinson
Donovan Hutchinson

DigitalOcean 提供适用于旅程每个阶段的云产品。立即开始使用 200 美元的免费额度!

提高网站速度的一种简单方法是仅在需要时下载图片,也就是图片进入视窗时。这种“懒加载”技术已经存在一段时间了,并且有很多优秀 教程介绍如何实现它。

但是即使有了所有这些资源,懒加载的实现方式也可能因你正在使用的项目或框架而异。在本文中,我将使用 Intersection Observer API 以及 onLoad 事件来使用 Svelte JavaScript 框架懒加载图片。

如果你不熟悉该框架,请查看Tristram Tolliday 对 Svelte 的介绍

让我们使用一个真实的例子

我在测试我正在进行的 Svelte 和 Sapper 应用程序 Shop Ireland 的速度时,将这种方法整合在一起。我们的目标之一是尽可能快地完成这项工作。我们发现主页的性能受到了影响,因为浏览器下载了大量不在屏幕上的图片,因此我们自然而然地转向了懒加载。

Svelte 已经非常快,因为所有代码都是预先编译的。但是一旦我们为图片添加了懒加载,速度就大大提升了。

这就是我们要一起努力的内容。请随时从 GitHub 获取此演示的最终代码,并继续阅读了解其工作原理。

这是最终的结果

让我们快速启动 Svelte

你可能已经有一个你想使用的 Svelte 应用程序,但如果没有,让我们从命令行开始一个新的 Svelte 项目并在本地进行操作

npx degit sveltejs/template my-svelte-project
cd my-svelte-project
npm install
npm run dev

你现在应该有一个在 http://localhost:5000 上运行的入门应用程序。

添加 components 文件夹

最初的 Svelte 演示有一个 App.svelte 文件,但还没有组件。让我们设置此演示所需的组件。目前还没有 components 文件夹,所以让我们在 src 文件夹中创建一个。在这个文件夹里,创建一个 Image 文件夹——它将容纳我们此演示的组件。

我们将让我们的组件做两件事。首先,他们将检查图片何时进入视窗。然后,当图片进入时,组件将等待图片文件加载完毕再显示它。

第一个组件将是一个 <IntersectionObserver>,它将包装第二个组件,即 <ImageLoader>。我喜欢这种设置的原因是它允许每个组件专注于做一件事,而不是试图在一个组件中打包一堆操作。

让我们从 <IntersectionObserver> 组件开始。

观察交集

我们的第一个组件将是 Intersection Observer API 的一个工作实现。Intersection Observer 非常复杂,但其本质是它监视子元素,并在子元素进入其父元素的边界框时通知我们。因此,对于图片来说:它们可以是某个父元素的子元素,当它们滚动到视图中时,我们可以得到提示。

虽然深入了解 Intersection Observer API 的进进出出是一个好主意——Travis Almand 对其有一个优秀的介绍——但我们将使用一个方便的 Svelte 组件,它是 Rich Harris 为svelte.dev 准备的。

我们将先设置它,然后再深入研究它的具体工作原理。创建一个新的 IntersectionObserver.svelte 文件,并将其放入 src/components/Image 文件夹中。我们将在这里定义具有以下代码的组件

<script>
  import { onMount } from 'svelte';


  export let once = false;
  export let top = 0;
  export let bottom = 0;
  export let left = 0;
  export let right = 0;


  let intersecting = false;
  let container;


  onMount(() => {
    if (typeof IntersectionObserver !== 'undefined') {
      const rootMargin = `${bottom}px ${left}px ${top}px ${right}px`;


      const observer = new IntersectionObserver(entries => {
        intersecting = entries[0].isIntersecting;
        if (intersecting && once) {
          observer.unobserve(container);
        }
      }, {
        rootMargin
      });


      observer.observe(container);
      return () => observer.unobserve(container);
    }


    // The following is a fallback for older browsers
    function handler() {
      const bcr = container.getBoundingClientRect();


      intersecting = (
        (bcr.bottom + bottom) > 0 &&
        (bcr.right + right) > 0 &&
        (bcr.top - top) < window.innerHeight &&
        (bcr.left - left) < window.innerWidth
      );


      if (intersecting && once) {
        window.removeEventListener('scroll', handler);
      }
    }


    window.addEventListener('scroll', handler);
    return () => window.removeEventListener('scroll', handler);
  });
</script>


<style>
  div {
    width: 100%;
    height: 100%;
  }
</style>


<div bind:this={container}>
  <slot {intersecting}></slot>
</div>

我们可以使用此组件作为其他组件的包装器,它将为我们确定包装的组件是否与视窗相交。

如果你熟悉 Svelte 组件的结构,你会发现它遵循一个模式,从脚本开始,然后进入样式,最后是标记。它设置了一些我们可以传递的选项,包括 once 属性,以及屏幕边缘的顶部、右侧、底部和左侧距离的数值,这些数值定义了交集开始的点。

我们将忽略这些距离,而是使用 once 属性。这将确保图片仅在它们第一次进入视窗时加载。

组件的主要逻辑位于 onMount 部分。这将设置我们的观察器,用于检查我们的元素以确定它是否与屏幕的可见区域“相交”。

对于旧浏览器,它还会附加一个滚动事件,以检查我们在滚动时元素是否可见,然后如果我们确定它可用且 oncetrue,它将移除这个侦听器。

加载图片

让我们使用 <IntersectionObserver> 组件通过将其包装在 <ImageLoader> 组件周围来有条件地加载图片。同样,这个组件会收到来自 <IntersectionOberserver> 的通知,以便它知道何时该加载图片。

这意味着我们需要在 components/Image 中创建一个新的组件文件。让我们把它命名为 ImageLoader.svelte。以下是我们要在其中写入的代码

<script>
  export let src
  export let alt


  import IntersectionObserver from './IntersectionObserver.svelte'
  import Image from './Image.svelte'
  
</script>


<IntersectionObserver once={true} let:intersecting={intersecting}>
  {#if intersecting}
    <Image {alt} {src} />
  {/if}
</IntersectionObserver>

这个组件接受一些与图片相关的道具——srcalt——我们将在其中使用它们来创建图片的实际标记。请注意,我们在脚本部分导入了两个组件,包括我们刚刚创建的 <IntersectionObserver> 和另一个我们尚未创建的组件,名为 <Image>,我们稍后会介绍。

<IntersectionObserver> 通过作为即将创建的 <Image> 组件的包装器来发挥作用。请查看其上的那些属性。我们正在将 once 设置为 true,因此图片仅在第一次看到时加载。

然后我们使用 Svelte 的插槽道具。它们是什么?让我们接下来介绍它们。

插槽属性值

包装组件(比如我们的 <IntersectionObserver>)非常适合将道具传递给它包含的子元素。Svelte 为我们提供了名为“插槽道具”的东西,以实现这一点。

在我们的 <IntersectionObserver> 组件中,你可能已经注意到这一行

<slot {intersecting}></slot>

它将 intersecting 道具传递到我们给它的任何组件。在这种情况下,当 <ImageLoader> 组件使用包装器时,它会接收到该道具。我们可以使用 let:intersecting={intersecting} 来访问该道具,就像这样

<IntersectionObserver once={true} let:intersecting={intersecting}>

然后我们可以使用 intersecting 值来确定何时该加载 <Image> 组件。在本例中,我们使用一个 if 条件来检查何时该执行。

<IntersectionObserver once={true} let:intersecting={intersecting}>
  {#if intersecting}
    <Image {alt} {src} />
  {/if}
</IntersectionObserver> 

如果发生了交集,<Image> 将被加载并接收 altsrc 道具。你可以在这篇Svelte 教程 中了解更多关于插槽道具的信息。

我们现在已经到位,可以在图片滚动到屏幕上时显示 <Image> 组件。最后,让我们来构建这个组件。

加载时显示图片

是的,你猜对了:让我们将 Image.svelte 文件添加到 components/Image 文件夹中,用于我们的 <Image> 组件。这个组件会接收我们的 altsrc 道具,并将它们设置在 <img> 元素上。

以下是组件代码

<script>
  export let src
  export let alt


  import { onMount } from 'svelte'


  let loaded = false
  let thisImage


  onMount(() => {
    thisImage.onload = () => {
      loaded = true
    }
  }) 


</script>


<style>
  img {
    height: 200px;
    opacity: 0;
    transition: opacity 1200ms ease-out;
  }
  img.loaded {
    opacity: 1;
  }
</style>


<img {src} {alt} class:loaded bind:this={thisImage} />

首先,我们在定义两个新变量之前接收了altsrc 属性:loaded 用于存储图像是否已加载,thisImage 用于存储对 img DOM 元素本身的引用。

我们还使用了一个名为 onMount 的 Svelte 有用方法。这为我们提供了一种在组件在 DOM 中呈现后调用函数的方法。在这种情况下,我们为 thisImage.onload 设置了一个回调函数。简单地说,这意味着它在图像加载完成后执行,并将 loaded 变量设置为 true 值。

我们将使用 CSS 来显示图像并将其淡入视图。让我们为图像设置 opacity: 0,以便它们最初不可见,尽管从技术上讲它们在页面上。然后,当它们与视窗相交并且 <ImageLoader> 允许加载图像时,我们将图像设置为完全不透明。我们可以通过为图像设置 transition 属性来使其平滑过渡。演示将过渡时间设置为 1200 毫秒,但您可以根据需要加快或减慢速度。

这将我们引向文件中的最后一行,它是 <img> 元素的标记。

<img {src} {alt} class:loaded bind:this={thisImage} />

这使用 class:loaded 来有条件地应用 .loaded 类,如果 loaded 变量为 true。它还使用 bind:this 方法将此 DOM 元素与 thisImage 变量关联。

原生延迟加载

虽然对浏览器中原生延迟加载的支持即将到来,但它还没有在所有当前稳定版本中得到支持。我们仍然可以使用简单的功能检查来添加对它的支持。

在我们的 ImageLoader.svelte 文件中,我们可以引入 onMount 函数,并在其中检查我们的浏览器是否支持延迟加载。

import { onMount } from 'svelte'

let nativeLoading = false
// Determine whether to bypass our intersecting check
onMount(() => {
  if ('loading' in HTMLImageElement.prototype) {
    nativeLoading = true
  }
})

然后,我们调整 if 条件以包含此 nativeLoading 布尔值。

{#if intersecting || nativeLoading}
  <Image {alt} {src} />
{/if}

最后,在 Image.svelte 中,我们告诉浏览器使用延迟加载,方法是在 <img> 元素中添加 loading="lazy"

<img {src} {alt} class:loaded bind:this={thisImage} loading="lazy" />

这使现代和未来的浏览器能够绕过我们的代码并原生处理延迟加载。

让我们将它们全部连接起来!

好了,现在是实际使用我们组件的时候了。打开 App.svelte 文件,并将以下代码放入其中以导入我们的组件并使用它

<script>
  import ImageLoader from './components/Image/ImageLoader.svelte';
</script>


<ImageLoader src="OUR_IMAGE_URL" alt="Our image"></ImageLoader>

这是演示的再次展示

请记住,您可以在 GitHub 上下载此演示的完整代码。如果您想在生产网站上看到它,请查看我的Shop Ireland 项目。延迟加载在首页、类别页面和搜索页面上使用,以帮助加快速度。我希望您能将它用于您自己的 Svelte 项目!