如何在React.js中预加载图片?

61
如何在React.js中预加载图像? 我有一个下拉选择组件,它像菜单一样工作,但我必须预加载项目的图像图标,因为有时它们在第一次打开时不可见。
我已经尝试过:

https://github.com/sambernard/react-preload

https://github.com/wizardzloy/react-img-preload

第一个API易于理解和使用,但是即使图片已经加载,它仍会在控制台上产生警告信息。 第二个API有些奇怪,但我尝试了它的示例,它没有预加载任何内容。
所以我可能需要自己实现一些东西,但不知道从哪里开始。另一种可能性是使用webpack加载它们。

这些图标是不是有点小呢?它们能使用数据URI吗?或者甚至可以使用内联SVG吗? - silvenon
我们现在正在处理什么类型的图像? - Abin Thaha
18个回答

86

假设您在组件属性中有一个名为 pictures: string[]; 的图片URL数组。 您应该在组件中定义 componentDidMount() 方法,然后只需为要预加载的每张图片创建新的 Image 对象:

componentDidMount() {
    this.props.pictures.forEach((picture) => {
        const img = new Image();
        img.src = picture.fileName;
    });
}

它强制浏览器加载所有图片。


4
如果您在使用JS,请将“let”替换为“const”,不要在此情况下使用“var”。 - Daniel Reina
15
使用一行代码 new Image().src = picture.fileName; 代替两行代码。 - Maxim Mazurok
4
TL;DR:如果你知道所声明的变量不会改变,就使用“const”,这样可以获得一些额外的好处。其中一些优点是:使用“const”可以让编译器进行一些优化,因为类型和值不会发生变化。同时它还可以提高代码的可读性,因为你知道该值不会改变。如果你在代码的另一个地方更改了值,它会给你一个 TypeError,这可能有助于避免更改不应更改的值而导致的错误。 - Daniel Reina
4
你如何在标记语言中使用通过 new Image() 创建的图像? - leto
5
除非您进行某些不安全的操作,否则无法在React中实现。它会将图像加载到缓存中,因此您可以像使用相同源的普通img标记一样使用它,并且它将立即加载。 - Charles
显示剩余5条评论

16

这是使用React Hooks(带有)预加载组件中图像的半完整示例。您还可以将其制作为钩子。

在这些示例中,我们在此处使用和,但实际的预加载工作在我们拥有的函数内部进行。还请注意,导入图像会得到一个字符串。

直接添加到组件中

import React, { useEffect, useState } from 'react'

import Image1 from 'images/image1.png'
import Image2 from 'images/image2.jpg'
import Image3 from 'images/image3.svg'

const preloadSrcList: string[] = [
  Image1,
  Image2,
  Image3,
]

function preloadImage (src: string) {
  return new Promise((resolve, reject) => {
    const img = new Image()
    img.onload = function() {
      resolve(img)
    }
    img.onerror = img.onabort = function() {
      reject(src)
    }
    img.src = src
  })
}

export default function Component() {
  const [assetsLoaded, setAssetsLoaded] = useState<boolean>(false)

  useEffect(() => {
    let isCancelled = false

    async function effect() {
      if (isCancelled) {
        return
      }

      const imagesPromiseList: Promise<any>[] = []
      for (const i of preloadSrcList) {
        imagesPromiseList.push(preloadImage(i))
      }
  
      await Promise.all(imagesPromiseList)

      if (isCancelled) {
        return
      }

      setAssetsLoaded(true)
    }

    effect()

    return () => {
      isCancelled = true
    }
  }, [])

  if (!assetsLoaded) {
    return <p>Preloading Assets</p>
  }

  return <p>Assets Finished Preloading</p>
}

更好的方式是使用钩子:useImagePreloader()

import { useEffect, useState } from 'react'

function preloadImage (src: string) {
  return new Promise((resolve, reject) => {
    const img = new Image()
    img.onload = function() {
      resolve(img)
    }
    img.onerror = img.onabort = function() {
      reject(src)
    }
    img.src = src
  })
}

export default function useImagePreloader(imageList: string[]) {
  const [imagesPreloaded, setImagesPreloaded] = useState<boolean>(false)

  useEffect(() => {
    let isCancelled = false

    async function effect() {
      console.log('PRELOAD')

      if (isCancelled) {
        return
      }

      const imagesPromiseList: Promise<any>[] = []
      for (const i of imageList) {
        imagesPromiseList.push(preloadImage(i))
      }
  
      await Promise.all(imagesPromiseList)

      if (isCancelled) {
        return
      }

      setImagesPreloaded(true)
    }

    effect()

    return () => {
      isCancelled = true
    }
  }, [imageList])

  return { imagesPreloaded }
}

在组件中使用 useImagePreloader()

import React, { useEffect, useState } from 'react'
import useImagePreloader from 'hooks/useImagePreloader'

import Image1 from 'images/image1.png'
import Image2 from 'images/image2.jpg'
import Image3 from 'images/image3.svg'

const preloadSrcList: string[] = [
  Image1,
  Image2,
  Image3,
]

export default function Component() {
  const { imagesPreloaded } = useImagePreloader(preloadSrcList)

  if (!imagesPreloaded) {
    return <p>Preloading Assets</p>
  }

  return <p>Assets Finished Preloading</p>
}

2
这是一个很好的用例,说明useEffect相对于生命周期方法而言是一个巨大的退步。以前很简单,componentDidMount,完成。现在变得冗长、hacky混乱。这不是Jo E的错,而是React的问题。 - Matt West
Jo,在预加载图像后,如何显示它们? - Rylan Schaeffer
1
@RylanSchaeffer 在显示上没有区别。只需确保它在imagesPreloaded检查之后即可。https://dev59.com/E1cP5IYBdhLWcg3wl7CY - Jo E.
@MattWest 有些是Jo的“错”!Jo添加了额外的功能来跟踪图像何时加载完成(并在用户离开页面时取消加载)。如果有人想要预加载图像,但不在意它们何时完成,useEffect代码在行数上与componentDidMount代码相当。 - undefined

8

React Hook 的方式

useEffect(() => {
  //preloading image
  faceArray.forEach((face) => {
    const img = new Image();
    img.src = face;
  });
}, []);

这只适用于你有src的情况,如果你使用image-loader导入,那么你就没有它了,那么你该怎么办? - Dimitri Kopriwa
1
当我这样做时,当图像组件被渲染时,React dom 通过另一个网络请求重新加载图像。 - coler-j
您可以尝试在呈现图像之前将图像存储到状态中,并在图像标签上使用id属性。 @coler-j - Anil Kumar

8
如果您的一些图片在被缓存后仍然重新加载,请尝试将它们存储在window对象中:
componentDidMount() {
    const imagesPreload = [image1, image2, image3];
    imagesPreload.forEach((image) => {
        const newImage = new Image();
        newImage.src = image;
        window[image] = newImage;
    });
}

你不需要从window对象调用图片


5
谢谢!这个帮了我,但为什么需要它? - Ryan Warner
这对我来说也是必需的,而不仅仅是推荐答案。我同意@RyanWarner的问题。有人知道为什么需要这个,并且似乎只在某些情况下? - Eric Russell

4
这是一种将此功能放入自定义钩子中的方法,您可以在任何功能组件中使用它。
一些答案中缺少的重要部分是将预加载图像的引用存储在类似于window对象的地方。如果不这样做,您可能会发现浏览器在实际需要时会重新请求这些图像,尽管它们已经被预加载过了。
第一次使用此钩子时,将在window对象中添加一个名为usePreloadImagesData的键,并将其初始化为{}。
每次在组件中使用该钩子时,都会向window.usePreloadImagesData添加一个新的键。键的名称是随机生成的。它被初始化为[]。
该钩子接受一个图像源字符串数组。对于每个图像源,都会创建一个new Image()并将其添加到window.usePreloadImagesData[key]数组中。
当组件卸载或传递给自定义钩子的数组发生更改时,将删除window.usePreloadImagesData[key]以避免内存泄漏。
此示例是用Typescript编写的。
import { useEffect } from 'react';

declare global {
  interface Window {
    usePreloadImagesData?: Record<symbol, unknown[]>;
  }
}

export const usePreloadImages = (imageSrcs: string[]): void => {
  useEffect(() => {
    const key = Symbol();
    window.usePreloadImagesData = window.usePreloadImagesData ?? {};
    window.usePreloadImagesData[key] = [];
    for (const src of imageSrcs) {
      // preload the image
      const img = new Image();
      img.src = src;
      // keep a reference to the image
      window.usePreloadImagesData[key].push(img); 
    }
    return () => {
      delete window.usePreloadImagesData?.[key];
    };
  }, [ imageSrcs ]);
};

使用此钩子时,请确保传递给钩子的图像源字符串数组是常量;否则会导致不必要的重新渲染。即,要么在组件外部创建它,要么使用类似于React.useMemo的东西。
例如:
import React from 'react';
import { usePreloadImages } from '../hooks/usePreloadImages';

import img1 from './image-1.jpg';
import img2 from './image-2.jpg';

const preload = [ img1, img2 ]; // create constant array here, outside the component

export const MyComponent: React.FC = () => {

  usePreloadImages(preload); // correct
  // usePreloadImages([ img1, img2 ]); // incorrect

  return (
    // ...
  );
};

4

这个可以运行:

import im1 from 'img/im1.png'
import im2 from 'img/im2.png'
import im3 from 'img/im3.png'

componentDidMount() {
    imageList = [im1, im2, im3]
    imageList.forEach((image) => {
        new Image().src = image
    });
}

3
如果只是要传递一些小的“图标” -(为什么不使用 字体?)- 并且服务器提供了gzipped文件,你可以使用base64。否则,如果选择不立即可见,您还可以在先前的HTML中添加img标签(带有display: none)。另一种方法是将Image对象附加到DOM并等待.onload,然后显示组件(这种方法由您提到的库使用)。就我所知,webpack或react在这里无法做任何特殊的事情。这是客户端的问题,这些只是实现自己的预加载API(甚至使用JS / TS、React、Angular等现有的API)的工具。

4
使用 SVG 而不是字体有很多原因。 - filoxo
你也可以添加内联SVG选项(React使这样的图标系统易于实现)。此外,如果作者决定使用数据URI,webpack也很容易实现。但是,问题不够具体。 - silvenon
考虑过与SVG相关的字体吗?但还是感谢你的提示。当然,有许多工具可以帮助你实现这样的东西。 - Tim
这些图标已经以png格式存储在服务器上,因此我无法将它们转换为svg而不失去质量。我不能设置display:none,因为对于下拉菜单,我正在使用react-select库,该库会自行处理附加或可见的内容。 - Ľubomír

1

所有这些重复的答案在Firefox中都不起作用(截至109版本)。您需要将图像实际挂载到DOM中:

const preloadImage = src => new Promise(resolve => {
  const image = new Image()
  image.src = src
  image.style.display = 'none'
  image.onload = () => {
    document.body.removeChild(image)
    resolve()
  }
  image.onerror = reject
  document.body.appendChild(image)
})

0

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接