🍬用 React.Suspense 实现组件预加载效果

3,372 阅读4分钟

要实现的效果

先看看最终要实现效果吧

首先显示预加载组件直到获取到了数据后真正要显示的组件才会显示出来

24.gif

基础概念

官网中的介绍:

React.Suspense 可以指定加载指示器(loading indicator),以防其组件树中的某些子组件尚未具备渲染条件。

也就是说,将你想要展示的组件放在 React.Suspense 中,并指示加载指示器。在将展示的组件没有准备好之前都将显示加载指示器。(前提是你想要展示的组件需要使用 lazy 来加载)

这里的没准备好是指比如没获取到数据、组件还没加载好之类的

官网例子:

// 该组件是动态加载的
const OtherComponent = React.lazy(() => import('./OtherComponent'));

function MyComponent() {
  return (
    // 显示 <Spinner> 组件直至 OtherComponent 加载完成
    <React.Suspense fallback={<Spinner />}>
      <div>
        <OtherComponent />
      </div>
    </React.Suspense>
  );
}

实现

背景

为什么写这个例子,因为在写 50 Projects in 50 Days 这个第 24 个 Demo 的时候,想起记得在官网看到过的 React.Suspense 这个功能,就尝试实现了一下。故记录一下。

实现原理

你可以先将 React.Suspense 理解成允许 React 在满足某些条件之前暂停组件的呈现

在这个例子中就是获取数据,在这个组件中抛出了 Promise,在这个 Promise 被处理之前都会暂停组件的显示,在暂停期间就会显示指定的加载指示器

准备

因为我在实现中使用了 styled-components 这个库

所以没了解过的没事,你只需要知道它是做什么的就行了(看得懂的例外)

首先是 Main 这个样式块,就是为了更好的查看效果而已

export const Main = styled.main`
  height: 100vh;
  background-color: steelblue;
  display: flex;
  justify-content: center;
  align-items: center;
`;

再就是 Loading 这个样式块,这个就是上面所说的加载指示器,其实也就是一个显示加载的效果

const animationBg = keyframes`
  0% {
    background-position: 50% 0;
  }

  100% {
    background-position: -150% 0;
  }
`;

export const Loading = styled.div`
  width: 350px;
  height: 400px;
  box-shadow: 0 0 15px rgba(0, 0, 0, 0.7);
  background-image: linear-gradient(
    to right,
    #f6f7f8 0%,
    #edeef1 10%,
    #f6f7f8 20%,
    #f6f7f8 100%
  );
  background-size: 200% 100%;
  animation: ${animationBg} 1s linear infinite;
`;

开始实现

首先开始实现真正将要显示的组件,上面也说了,需要抛出一个 Promise,所以先自定义一个获取“数据”的钩子

/**
 * 自定义一个简易的 获取“数据”的钩子
 */
import {useState, useEffect} from 'react'

function useCustomFetch(fetcher) { // 传入一个真正获取数据的函数
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  
  useEffect(() => {
    setLoading(true);
    // 处理数据
    fetcher()
      .then((res) => {
        console.log('fetch', res);
        setData(res);
      })
      .finally(() => {
        setLoading(false);
      });
  }, [fetcher]);

  // 这里会在获取到数据之后才会置为 false,也就是才会返回数据
  if (loading) {
    throw Promise.resolve(null); // 这里抛出一个 Promise
  } else {
    return data;
  }
}

export default useCustomFetch;

需要在组件抛出的 Promise 的钩子实现了,接下来就是在组件中使用这个钩子了

import React, {useState, useEffect} from "react";
import styled from "styled-components";
import useCustomFetch from "./useCustomFetch";

// 这个就是为了展示信息的 div
const CardDiv = styled.div`
  width: 350px;
  height: 400px;
  background-color: #e3e3e3;
  box-shadow: 0 0 15px rgba(0, 0, 0, 0.7);
  display: flex;
  flex-direction: column;
  img {
    object-fit: cover;
    height: 100%;
    width: 100%;
  }
  .info {
    padding: 10px;
    h3 {
      text-align: center;
    }
    p {
      text-align: center;
    }
  }
`;

// 传入 useCustomFetch 的回调函数
// 模拟发送数据
function fetcher() {
 return new Promise((resolve) => {
   setTimeout(() => {
     resolve({
       imgUrl: "https://source.unsplash.com/random/350x200",
       title: "Lorem ipsum dolor sit amet",
       desc: "Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolore perferendis",
     });
   }, 2000);
 });
}

// 初始数据
const initialState = {
  imgUrl: "",
  title: "",
  desc: ""
};

function Card() {

  const [data, setData] = useState(initialState);
  const res = useCustomFetch(fetcher); // 这里使用了自定义钩子

  useEffect(() => {
    // 避免无意义的重复渲染
    if(res !== null) {
      setData(res);
    }
  }, [res]);

  return (
    <CardDiv>
      <img src={data.imgUrl} alt="" />

      <div className="info">
        <h3>{data.title}</h3>
        <p>{data.desc}</p>
      </div>
    </CardDiv>
  );
}

export default Card;

需要的钩子和组件都实现了,最后在使用 React.Suspense

import React from 'react'
import { Main, Loading } from "./styled"; // Main 和 Loading 都是上面定义的样式 div

// 这里记得用 lazy 来加载 Card 组件
const Card = React.lazy(() => import("./card.js"));

function ContentPlaceholder() {
  return (
    <Main>
      {/* 这里的 fallback 指定加载指示器 */}
      <React.Suspense fallback={<Loading />}>
        <Card />
      </React.Suspense>
    </Main>
  );
}

export default ContentPlaceholder

最后

这篇博客的完整源码在 这里

我将 50 Projects in 50 Days 中的例子全部用 React Hooks 实现了一遍。

github地址在这里:50-mini-projects-with-react

若有不当之处,欢迎评论指出