图片预览组件react-viewer的懒加载实现

1,614 阅读4分钟

这是我参与更文挑战的第1天,活动详情查看: 更文挑战

前情

项目中需要对一系列图片实现预览功能,经过调研,在github上找到一个React库react-viewer,非常简单易用,完美实现需求。

github地址:github.com/infeng/reac…

实现效果示例:

image.png

核心代码如下,参数images为传入图片的列表。

class PicViewer extends PureComponent {
  constructor(props) {
    super(props);
    this.state = {
      visible: false,
      images: [],
    };
  }

  closeViewer = () => {
    this.setState({ visible: false });
  };

  render() {
    const { visible, images } = this.state;
    return (
      <div>
        <Viewer
          visible={visible}
          onClose={this.closeViewer}
          images={images}
          downloadable
          noImgDetails
          zoomable={false}
          zoomSpeed={0.3}
          scalable={false}
          loop={false}
          showTotal={false}
          noNavbar
          customToolbar={toolbars =>
            toolbars.filter(item => ['rotateLeft', 'rotateRight', 'download'].includes(item.key))
          }
        />
      </div>
    );
  }
}

问题

但是后来发现,随着业务场景的丰富,images可能会非常大,多则由几百张图片,如果把几百张图片一次性传给images,则会导致批量的往服务器发送几百个获取图片的请求,给服务器造成的压力巨大。

解决思路

我想到的解决办法是去实现懒加载功能。尽管图片数量很大,但我控制初始是每次传入该组件中的图片总数固定,比如10张,这样最多只向服务器请求10张图片。

另外,随着不断翻阅图片,会通过onchange方法检测当前图片数目的变化。假设翻阅到了图片末尾,会去更新该组件的图片范围,慢慢的做扩展,控制每次图片加载的数目。

举个例子,比如有20张图片,编号1-20。

任意点击一张图片时,我会读该图片的前4张和后5张,共10张图片,并去加载它们。比如我点到了编号为10的图片,就会像预览组件传入6-15这10张图片。

翻阅图片时,会触发预览组件的函数,如果检测到图片到达了边界(比如6或15),会触发一次图片范围的扩展。具体扩展方式为,若为左下标,则左边界往左更新1,预览组件图片范围变成1-15,若为右下标,则右边界往右更新5,预览组件图片范围变成6-20。

这样可以很流畅的达成预览的效果,又不会对服务器带宽资源造成太多的浪费。

实现方式

经查阅文档,react-viewer组件提供了onChange方法,该方法将会用作图片移动的监测。

image.png

同时还用到的重要的属性有:

  • activeIndex:当前显示的图片的索引(注意这个索引是在当前传入预览组件中的图片列表的索引,因此我在写代码时会注意到要动态更新这个的值)
  • images:图片列表

直接上代码,我把原来的Viewer封装成了我自己的组件PictureViewer

import Viewer from 'react-viewer';
import 'react-photo-view/dist/index.css';
import React, { PureComponent } from 'react';

class PictureViewer extends PureComponent {
  constructor(props) {
    super(props);
    this.state = {
      start: 0, // 图片左边界
      end: 0, // 图片右边界
      cursorIndex: 0, // 组件内部指针
      useCursorIndex: false, // 是否使用组件指针
    };
  }

  // 初始范围,activeIndex为调用该组件的组件传入的
  componentWillReceiveProps(nextProps) {
    const { activeIndex, images } = nextProps;
    const { useCursorIndex } = this.state;
    if (!useCursorIndex && nextProps.visible) {
      if (activeIndex - 4 > 0) {
        this.setState({ start: activeIndex - 4 });
      } else {
        this.setState({ start: 0 });
      }
      if (activeIndex + 5 > images.length) {
        this.setState({ end: images.length });
      } else {
        this.setState({ end: activeIndex + 5 });
      }
    }
  }

  // 关闭预览,closeViewer的函数也来自于外层组件
  closeViewer = () => {
    const { closeViewer } = this.props;
    this.setState({ useCursorIndex: false, start: 0, end: 0 });
    closeViewer();
  };

  // 更新边界
  changePic = (activeImage, index) => {
    const { images } = this.props;
    const { start, end } = this.state;
    if (index === end - start - 1 && index < images.length - 1) {
      if (end + 5 < images.length) {
        this.setState({ end: end + 5, cursorIndex: index, useCursorIndex: true });
      } else {
        this.setState({ end: images.length, cursorIndex: index, useCursorIndex: true });
      }
    }
    if (index === 0 && start !== 0) {
      if (start - 5 > 0) {
        this.setState({ start: start - 5, cursorIndex: 5, useCursorIndex: true });
      } else {
        this.setState({ start: 0, cursorIndex: start, useCursorIndex: true });
      }
    }
  };

  render() {
    const { images, visible, activeIndex } = this.props;
    const { start, end, cursorIndex, useCursorIndex } = this.state;
    const showImages = images.slice(start, end);
    // 这几行可用于调试
    // console.log(useCursorIndex);
    // console.log(`cursorIndex:${cursorIndex}`);
    // console.log(`activeIndex:${activeIndex}`);
    // console.log(`start:${start}`);
    // console.log(`end:${end}`);
    return (
      <div>
        <Viewer
          visible={visible && end > 0}
          onClose={this.closeViewer}
          activeIndex={useCursorIndex ? cursorIndex : activeIndex - start}
          images={showImages}
          downloadable
          noImgDetails
          zoomable={false}
          scalable={false}
          loop={false}
          showTotal={false}
          onChange={(activeImage, index) => this.changePic(activeImage, index)}
        />
      </div>
    );
  }
}

接下来我围绕截图,具体阐释这个过程发生了什么。

首先,20张图片整整齐齐。

image.png

然后,我点击第一张图片。此时加载了1-5张图片,start=0,end=5,activeIndex=0,userCursorIndex=false

image.png

我不断移动向右按钮,图片组件正常轮播,当我翻到第5张时,奇迹出现了,一次性请求了6-10张图片。此时start=0,end=10,activeIndex=0,cursorIndex=4,userCursorIndex=true。

image.png

这里也解释了我为什么要引用变量userCursorIndex,因为在我更新图片组件的范围时,activeIndex不会更新,所以我要人为的告诉图片组件,此时的activeIndex应该是哪个值。

我继续向右移动按钮,效果和预期相符,当翻到第10张时,继续请求11-15张,这时候的图片边界为0-15,即start和end的值。当我继续翻到第15张时,继续请求16-20张,这时所有的图片都已经请求完毕。图片边界也已经变成0-20,不管我怎么翻阅,都不会再进行加载了。

image.png

最后,我来演示一下一个从中间请求的case。

我来点击第8张图片。发现这张图片的前4张和后4张也加载完毕。此时start=3,end=12,activeIndex=7,cursorIndex=0,userCursorIndex=false。

image.png

随着我向前翻阅到第3张,或者向后翻阅到第12张,都会触发边界更新。这里就不做演示了。