React.lazy 的一些使用场景

React.lazy 主要用于实现组件的动态加载,从而减少初始加载时间并提高应用的性能。在 React 应用中有较大的组件需要加载时,使用 React.lazy 可以帮助我们优化性能,只在需要时才加载这些组件。这对于减少初始加载时间和减轻页面负担非常有帮助。

基本用法

React.lazy 接受一个函数作为参数,该函数应该返回一个动态 import() 调用,用于加载组件。例如:

jsx 复制代码
const MyComponent = React.lazy(() => import("./MyComponent"));

由于动态加载可能需要一些时间,需要使用 <Suspense> 组件来处理加载过程中的等待状态。在等待组件加载的过程中,可以在 <Suspense> 组件中设置一个加载中的 UI,例如:

jsx 复制代码
import React, { Suspense } from "react";

function App() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <MyComponent />
      </Suspense>
    </div>
  );
}

在使用 React.lazy 加载组件时,如果加载失败,React 将抛出一个错误。为了捕获和处理这些错误,可以使用错误边界(Error Boundaries)来包装 <Suspense> 组件:

jsx 复制代码
class ErrorBoundary extends React.Component {
  state = { hasError: false };

  componentDidCatch(error, errorInfo) {
    this.setState({ hasError: true });
    // You can also log the error to an error reporting service
  }

  render() {
    if (this.state.hasError) {
      return <div>Something went wrong.</div>;
    }

    return this.props.children;
  }
}

// Usage
<ErrorBoundary>
  <Suspense fallback={<div>Loading...</div>}>
    <MyComponent />
  </Suspense>
</ErrorBoundary>;

React.lazy 可以与 Webpack 等构建工具的代码分割功能一起使用,从而将应用代码分割为更小的块,只在需要时才会加载这些代码块,这有助于减小初始加载大小,提高性能。

失败重试

React.lazy 加载组件时有可能会加载失败,但是它不支持重试功能,需要我们额外写一些代码来实现这个功能。React.lazy 的参数是一个返回 Promise 的函数,也就是下面代码中的 factory,React.lazy 的类型定义如下所示:

typescript 复制代码
function lazy<T extends ComponentType<any>>(
  factory: () => Promise<{ default: T }>
): LazyExoticComponent<T>;

因此,我们可以用 Promise 的 catch 来捕获 factory 的错误,当 import 加载组件出现错误时,可以再次重新发起请求。

js 复制代码
const retry = (importer, count = 0) => {
  const _importer = () => {
    return importer().catch((error) => {
      if (!count) {
        throw error;
      }
      count = count - 1;
      return _importer();
    });
  };

  return _importer;
};

const MyComponent = React.lazy(retry(() => import("./MyComponent"), 3));

在上面的代码中,设置了重试次数之后,组件加载失败后会立即发起重试。在真实的业务场景中,要在重试时做更换 CDN,因为如果 CDN 出现问题,重试之后还是会失败。而 CDN 的切换需要做额外处理,这里就不做展开讨论。

自定义加载

上面介绍的是 React.lazy 最为常见的场景,通过 import 引入本地组件,Webpack 将组件进行打包,最终实现组件的按需加载。如果我们想直接从一个远程 CDN 上加载组件 bundle,又如何实现呢?

因此,我们需要实现一个自定义的 remoteImport。在 React.lazy 的类型定义中,factory 函数的返回值是一个 Promise,Promise resolve 的值是 { default: T },T 是一个具体的组件,可以是函数组件也可以是类组件。

typescript 复制代码
type ComponentType<P = {}> = ComponentClass<P> | FunctionComponent<P>;

function lazy<T extends ComponentType<any>>(
  factory: () => Promise<{ default: T }>
): LazyExoticComponent<T>;

首先,从最简单的开始,remoteImport 直接返回一个组件:

jsx 复制代码
const remoteImport = () =>
  Promise.resolve({
    default: () => <span>Hello</span>,
  });

const Hello = React.lazy(() => remoteImport());

接下来就要实现加载远程 bundle,这里介绍两种方法,一种是使用 script 标签加载,另外一种是使用 fetch 加载。但是无论哪种加载方式,都对 bundle 的打包构建有一定的要求,并不是随随便便的一个 bundle 我们都能加载成功。另外,这两种加载方式对应 bundle 的打包构建方式也略有差别。

使用 script 标签加载 bundle

script 标签加载加载 bundle 是一种较为常见的方案。在下面的代码中 loadBundle 动态创建 script 标签,并加载远程的 bundle:

javascript 复制代码
const loadBundle = (url) => {
  return new Promise((resolve, reject) => {
    const el = document.createElement("script");
    const remove = () => document.head.removeChild(el);

    el.addEventListener("load", () => {
      remove();
      resolve();
    });
    el.addEventListener("error", (ev) => {
      remove();
    });
    el.src = url;
    el.crossOrigin = "anonymous";
    document.head.appendChild(el);
  });
};

但是这种方法加载 bundle 之后,需要将对应的组件挂在 window 对象上,否则我们没有办法获取到 bundle 里的组件。因此,这就要求在构建 bundle 时做一些处理,在 Webpack 配置中要对 output.library 字段进行设置。在 Webpack 中,output.library 字段用于指定打包生成的 JavaScript 资源作为一个库(library)暴露给外部使用时的名称。

javascript 复制代码
// webpack 配置
{
  output: {
    library: 'MyComponent',
    filename: 'bundle.js'
  }
  external: {
    react: 'react',
  }
}

这样可以通过 window.MyComponent 来访问到 bundle 内部的组件。与此同时,在当前的 bundle 中不要引入 React 的代码。所以,要将 React 设置为 external,并且把 React 挂到 window 对象上。这样,bundle 中才能正常引入 React。如果 bundle 中是使用 export default 导出的组件,那么 window.MyComponent 对应的是对象也是 { default: T }。我们可以直接在 remoteImport 函数中将其作为返回值:

jsx 复制代码
import React from "react";

window.react = React;

const remoteImport = (url, library) => {
  return loadBundle(url)
    .then(() => window[library])
    .catch(() => ({
      default: () => <></>,
    }));
};

这样即可加载远程的 bundle,比如:

jsx 复制代码
const MyComponent = React.lazy(() =>
  remoteImport("http://example.com/MyComponent.js", "MyComponent")
);

使用 fetch 加载 bundle

另外一种加载方式可以通过 fetch 来取远程 bundle,在请求成功之后可以通过函数来解析出 bundle 中的组件:

jsx 复制代码
const fetchBundle = (url, _requires) => {
  return fetch(url)
    .then((response) => response.text())
    .then((data) => {
      const exports = {};
      const module = { exports };
      const func = new Function("require", "module", "exports", data);
      func(_requires, module, exports);
      return module.exports;
    });
};

而为了能够让上面的 fetchBundle 正常使用,需要以 commonjs 的形式构建 bundle,在 Webpack 中将 libraryTarget 设置为 commonjs:

javascript 复制代码
// webpack 配置
{
  output: {
    libraryTarget: 'commonjs',
    filename: 'bundle.js'
  }
  external: {
    react: 'react',
  }
}

同样由于不能在 bundle 中包含 React 代码,所以要对 bundle 中的 require 做处理,也就是要告诉 bundle 去哪里取 React。fetchBundle 的第二个参数 _requires 就是来解决这个问题。

jsx 复制代码
import React from "react";

const external = {
  react: React,
};

const remoteImport = (url) => {
  const _requires = (id) => {
    return external[id];
  };

  return fetchBundle(url, _requires).catch(() => ({
    default: () => <></>,
  }));
};

这样即可加载远程的 bundle,比如:

jsx 复制代码
const MyComponent = React.lazy(() =>
  remoteImport("http://example.com/MyComponent.js")
);

在真实的业务中,使用 React.lazy 加载远程 bundle 场景并不多见,大多数情况下正常使用 React.lazy 即可满足需求。但是,在一些微前端场景中,可能会用到这种方案来加载模块,进而来实现不同系统或项目之间的业务模块的复用。

总结

本文主要介绍了 React.lazy 的一些基本用法:

  • React.lazy 主要用于代码的按需加载,进而提升页面的性能;使用 <Suspense> 来设置加载状态;使用 ErrorBoundary 来进行错误处理;
  • 通过自定义 import 可以实现重试功能,以应对加载失败的场景
  • 也可以通过自定义 import 来加载远端的 bundle,实现的方案主要有两种:
    • 使用 script 标签加载 bundle
    • 使用 fetch 加载 bundle
相关推荐
jump68010 分钟前
url输入到网页展示会发生什么?
前端
诸葛韩信13 分钟前
我们需要了解的Web Workers
前端
brzhang19 分钟前
我觉得可以试试 TOON —— 一个为 LLM 而生的极致压缩数据格式
前端·后端·架构
yivifu38 分钟前
JavaScript Selection API详解
java·前端·javascript
这儿有一堆花40 分钟前
告别 Class 组件:拥抱 React Hooks 带来的函数式新范式
前端·javascript·react.js
十二春秋1 小时前
场景模拟:基础路由配置
前端
六月的可乐1 小时前
实战干货-Vue实现AI聊天助手全流程解析
前端·vue.js·ai编程
一 乐1 小时前
智慧党建|党务学习|基于SprinBoot+vue的智慧党建学习平台(源码+数据库+文档)
java·前端·数据库·vue.js·spring boot·学习
BBB努力学习程序设计2 小时前
CSS Sprite技术:用“雪碧图”提升网站性能的魔法
前端·html
BBB努力学习程序设计2 小时前
CSS3渐变:用代码描绘色彩的流动之美
前端·html