React.js 职场实战!图片 Infinite List

一天的开始

还记得吗?你是负责 Imager 的前端工程师,上次做了 Lazy Loading 改善了资源浪费的问题,公司对你的表现非常的满意。

但有干劲的你决定想想看,有没有其他方案也可以达到节约资源的效果,可以的话甚至可以和现在的 Lazy Loading 做结合!

喝杯咖啡,逛逛文章

一边品嚐着香醇浓郁的咖啡,一边滑着公司配给你的 MacBook Pro,一番寻寻觅觅之後,你找到了一个很酷的技术方案——

Infinite List 无限加载列表

你稍微把文章扫过一遍,归纳出几个实作重点:

  • 使用 Intersection Observer API
  • 一个触发拿取新资料的 Element
  • 将原本一次拿 100 张图(全部)改成分次拿取

上工了!开始 Coding

身为一名工程师,最快乐的时光就是动手写程序的时候啦。如果你喜欢直接看 Code 来理解的话可以直接前往完成的范例观看:

Edit 04-infinite-list

考量到 Lazy Loading 与 Infinite List 的结合较为复杂,这次会先实作只有 Infinite List 的版本,下一篇再将两者结合!

<InfiniteListTrigger />

首先我们需要一个 Element 搭配 Intersection Observer 来触发 Loading 的行为,也就是当这个 Element 出现在画面上时,就触发下载新的图片资料。

我们就将这个 Element 命名为 <InfiniteListTrigger /> ,接着确保 Loading 状态时不会显示它,目的是避免重复触发下载图片列表。

// App.js
import React, { useRef } from "react";
...
import {
  ...
  Loading,
  InfiniteListTrigger
} from "./style";

const App = () => {
  const triggerRef = useRef(null);
  ...

  return (
    <View>
      ...

      {/* 并且为了防止重复触发图片下载,在 Loading 时我们就不显示触发用的 Element */}
      {!isLoading && <InfiniteListTrigger ref={triggerRef} />}
    </View>
  );
};

export default App;

fetchNewImages()

接着我们来写一个模拟打 API 的 function,他做的事情很单纯,就是等一秒之後拿到新的图片列表。

// App.js
...

const App = () => {
  ...
  
  const fetchNewImages = () =>
    // 回传一个 Promise 是为了模拟打 API 时非同步的情境
    new Promise((resolve) => {
      const newImages = generateImages({ count: 10 });

      // 我们假定每次打 API 平均一秒後拿到新的图片列表
      setTimeout(() => {
        setImages((prev) => [...prev, ...newImages]);
        resolve();
      }, 1000);
    });
  
  ...
};
  
...

useInifiteList()

现在有了元件之後,我们需要一个 hook 将 <InifiniteListTrigger /> 交给 Intersection Observer 监听。

这次要做的是相对简单,我们需要 ref 和 onView 来完成无限加载的行为,我们看看它们对应的工作:

  • ref — 要监听的元件,也就是 <InfiniteListTrigger />
  • onView — 当元件出现在画面上时,触发的 function。
// useInfiniteList.js
import { useState, useEffect } from "react";

const useInfiniteList = ({ ref, onView }) => {
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    // 要监听的元件
    const trigger = ref.current;

    if (!trigger) return;
    if (!onView) return;

    // 这次我们的监听行为非常的简单
    // 如果元件出现在画面上就触发 onView 事件
    const callback = (entries) => {
      entries.forEach(async (entry) => {
        if (entry.isIntersecting) {
          setIsLoading(true);
          await onView();
          setIsLoading(false);
        }
      });
    };

    // 开始监听元件
    const observer = new IntersectionObserver(callback);
    observer.observe(trigger);

    // 别忘了取消监听元件哦!
    return () => {
      observer.unobserve(trigger);
    };
  }, [ref, isLoading, onView]);

  return { isLoading };
};

export default useInfiniteList;

准备完工

现在我们将上面的功能都组合在一起!

// App.js
import React, { useRef, useState } from "react";
import Image from "./components/Image";
import useCount from "./hooks/useCount";
import useInfiniteList from "./hooks/useInfiniteList";
import generateImages from "./utils/generateImages";
import {
  View,
  Title,
  Loading,
  ImageBlock,
  ImageCount,
  InfiniteListTrigger
} from "./style";

const App = () => {
  const triggerRef = useRef(null);
  const [images, setImages] = useState([]);
  const { count, addCount } = useCount();

  // 这里我们制作了一个下载图片列表的 function
  // 当使用者看到 <InfiniteListTrigger /> 的时候就触发下载图片列表
  const fetchNewImages = () =>
    // 回传一个 Promise 是为了模拟打 API 时非同步的情境
    new Promise((resolve) => {
      const newImages = generateImages({ count: 10 });

      // 我们假定每次打 API 平均一秒後拿到新的图片列表
      setTimeout(() => {
        setImages((prev) => [...prev, ...newImages]);
        resolve();
      }, 1000);
    });

  // ref   : 监听的元件
  // onView: 当元件出现在画面上时,执行此事件
  const { isLoading } = useInfiniteList({
    ref: triggerRef,
    onView: fetchNewImages
  });

  return (
    <View>
      <ImageCount>图片载入数量:{count}</ImageCount>

      <Title>Imager</Title>

      <ImageBlock>
        {images.map((image) => (
          <Image key={image.id} src={image.src} onLoad={addCount} />
        ))}
      </ImageBlock>

      {/* 当我们在下载新的图片列表时,显示 Loading 让使用者知道图片正在下载 */}
      {isLoading && <Loading>Loading...</Loading>}

      {/* 并且为了防止重复触发图片下载,在 Loading 时我们就不显示触发用的 Element */}
      {!isLoading && <InfiniteListTrigger ref={triggerRef} />}
    </View>
  );
};

export default App;

成果发表

你埋首在程序之中也经过了好一段时间,现在可以来看看最终成果了!

Edit 04-infinite-list

看起来运作的很顺利,当页面滑到最下方之後,我们才开始下载下面 10 笔的资料。看到这样的成果,你不禁露出了骄傲的神情。

咕噜咕噜…

快乐的时光总是过得特别快,从肚子传出的咕噜声将你的注意力拉回现实,居然已经是下班时间了!你缓缓的从座位站起身子,拿着钱包准备去吃晚餐啦!


<<:  day4 network simulator (雷)新不代表好

>>:  [Day03] Medium

管理 Deployments

Canary Deployments 当要使用一部分用户测试生产中的新部署时,请使用 Canary ...

成员 12 人:我真的不想教新人,除非他真的很可爱

「这是个性很急,却也快不了的一整个世代。」 那,我们该改用什麽样的态度,跟年轻同事相处? 没有, 绝...

Day23 - 中断...

开头,先跟追踪此系列的读者道歉, 我失败了。 是的,我决定在这天为我的系列划下一个不是很好的句点,却...

AI ninja project [day 28] QLattice --进阶回归

这一篇与[day 26]的差异是,我们对於对於训练模型上参数的调整。 可以先看一下原始资料集,是之前...

Firebase来帮忙资料上传 Day 12

将资料上传至Firebase上 使用UIImagePikerController来选取照片上传 使用...