React丨用戶體驗丨hook版 lazy loading

咱們都知道隨着單頁應用 bundle 的體積不斷增大,會形成首次加載時間過長(白屏時間過長),過程當中會加載了咱們首頁沒有必要看到的一些 頁面/組件 js文件,因此咱們須要對 bundle 文件進行拆分來進行按需加載(懶加載),這裏須要用到 webpack 支持的代碼拆分(code splitting)。

React.lazy 和 React.Suspense 是一對好兄弟,它兩就能夠完成一個優雅的懶加載方案,讓咱們來看看它們在 hook 中的使用吧。css

文章篇幅有一丟丟長,但請耐心品它 ~前端

咱們先來看看 React.lazy

pages/page1.tsxreact

import React from "react";
const Index: React.FC = () => {
  return <div>my name is page1,i am very lazy</div>;
};
export default Index;

App.tsxwebpack

// ... 
const Page1 =   React.lazy(() => import("./pages/page1"));
// ...
export default function App() {
  return (
    <Router>
      <div id="app">
        <Switch>
          <Route path="/page1" component={Page1} />
        </Switch>
      </div>
    </Router>
  );
}

這裏咱們直接經過webpack支持的代碼拆分寫法 () => import("./pages/page1"),可是報了個錯:web

上面報錯說的是須要與 React.Suspense 配合作 loading 或者佔位符,因此說它們是好兄弟,如影隨行 。那咱們來知足一下它們吧~網絡

React.lazy + React.Suspense

App.tsxapp

const LazyPage1 = React.lazy(() => import("./pages/page1"));

const Page1: React.FC = () => (
  <React.Suspense fallback={<div>loading</div>}>
    <LazyPage1 />
  </React.Suspense>
);

注意:Suspense 組件須要包裹 React.lazy。異步

上面代碼咱們簡單用了 <div>loading</div> 做爲佔位符,效果以下:ide

爲了更加看得出效果,咱們能夠本身模擬網絡很卡的狀況下懶加載的效果,添加 slow 輔助方法測試

// ...
const slow = (comp: any, delay: number = 1000): Promise<any> => {
  return new Promise((resolve) => {
    setTimeout(() => resolve(comp), delay);
  });
};

const LazyPage1 = React.lazy(() => slow(import("./pages/page1"), 3000));
// ...

注意:這只是個模擬,測試完畢記得不須要調用slow哦 ~

讓咱們再來看看模擬延遲3s的效果:

小總結:這樣就能夠實現一個不錯的懶加載了,咱們如今已經掌握了這兩個方法在hook中的基本用法了,聰明的你是否嗅到了一股能夠抽象封裝的味道 ~

讓咱們來耐心的看下如何更優雅針對這"兩兄弟"進行封裝吧

前面說的話

咱們都知道,通常來講咱們會把組件分紅 頁面級組件真正的高可複用的組件
因此他們的 loading / 佔位符 是否也要作下區分,
我這裏想讓 異步頁面 加載的時候,用一個頁面絕對居中的loading做爲fallback,
讓 異步組件 加載的時候,用一個當前組件位置的loading做爲fallback。

咱們來封裝一下,show you my code

HOCLazy/index.tsx

import React from "react";
import Loading from "./Loading";
import "./index.css";
interface IProps {
  isComponent?: boolean;
  content?: string;
}
const PH: React.FC<IProps> = ({ isComponent = false, content = "" }) => {
  return (
    <>
      <div className={`ph ${!isComponent ? "page" : ""}`}>
        <div className="loading-wrapper">
          <Loading />
          <div>{content}</div>
        </div>
      </div>
    </>
  );
};
function HOCLazy<T = {}>(chunk: any): React.FC<IProps & T> {
  const WrappedComponent: React.FC = React.lazy(chunk);
  const Comp: React.FC<any> = ({
    isComponent = false,
    content = "",
    ...props
  }) => {
    return (
      <React.Suspense
        fallback={<PH isComponent={isComponent} content={content} />}
      >
        <WrappedComponent {...props} />
      </React.Suspense>
    );
  };
  return Comp;
}
export default HOCLazy;

解釋:

  • 咱們定義了一個」PH「佔位符組件,它的內容是一個loading和loading下的一段可選文字,接收一個 isComponent 屬性用來區分當前lazy對象而用不一樣的樣式。
  • 核心方法 HOCLazy,爲了保證在ts下組件props的「完整性」,咱們用了 泛型 <T> 來彌補這樣的「完整性」。

接下來,咱們就能夠更加方便的 lazy loading 了

頁面級 lazy loading

App.tsx

// ...
const Page1 = HOCLazy(() => slow(import("./pages/page1"), 3000));
// ...

組件級 lazy loading

pages/page1.tsx

import React from "react";
import HOCLazy from "../HOCLazy";
import slow from "../HOCLazy/slow";
import { IProps } from "../components/Comp1";
const Comp1 = HOCLazy<IProps>(() => slow(import("../components/Comp1"), 3000));
const Index: React.FC = () => {
  return (
    <>
      <div>my name is page1,i am very lazy</div>
      <Comp1 isComponent content="Comp1組件加載中..." suffix="!!!" />
      <Comp1 isComponent content="Comp1組件加載中..." suffix="!!" />
      <Comp1 isComponent content="Comp1組件加載中..." suffix="!" />
    </>
  );
};
export default Index;

最終效果圖

更優化

若是咱們在頁面/組件每次 lazy loading 前都須要顯示「佔位符」,那麼可能會形成在網絡不錯的狀況下,頁面閃過「佔位符」,致使總體不美觀。
作lazy loading的目前是爲了讓頁面呈現更流暢順滑,這違背了咱們的初衷,
我想在網絡很好的狀況下,不讓用戶看到這個「佔位符」。

咱們須要改造一下

HOCLazy/index.tsx

// ...
const PH: React.FC<IProps> = ({ isComponent = false, content = "" }) => {
  const [show, setShow] = useState<boolean>(false);
  const timeoutRef = useRef<number>();
  useEffect(() => {
    const id = setTimeout(() => {
      setShow(true);
      // 延遲時間可本身拿捏
    }, 500);
    timeoutRef.current = id;
    return () => {
      clearTimeout(timeoutRef.current);
    };
  }, []);

  return (
    <>
      {show ? (
        <div className={`ph ${!isComponent ? "page" : ""}`}>
          <div className="loading-wrapper">
            <Loading />
            <div>{content}</div>
          </div>
        </div>
      ) : null}
    </>
  );
};
// ...

解釋:這裏經過延遲 500 毫秒顯示「佔位符」的方式,讓頁面在網絡良好的狀況下,不會出現「佔位符」一閃而過。

打包在同個異步塊 (chunk) 中

有了懶加載,咱們就能夠更靈活的利用 webpack 去分包,更細膩的去處理包的加載。如合併幾個頁面爲一個異步包:

const Page1 = HOCLazy(() =>
  import(/* webpackChunkName: "page" */ "./pages/page1")
);
const Page2 = HOCLazy(() =>
  import(/* webpackChunkName: "page" */ "./pages/page2")
);

執行build以後的包名,如:page.e6075659.chunk.js

總結

SPA應用必不可少的就是懶加載,本篇提供了一個較爲合理的封裝思路。喜歡封裝的小夥伴,能夠繼續深究,更細節的去處理組件與loading/佔位符 樣式。

若是有更好的idea也能夠與我一塊兒討論,關注公衆號:前端精 ,讓咱們一塊兒進步吧 ~

戳我看完整例子

相關文章
相關標籤/搜索