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
相关推荐
桂月二二39 分钟前
探索前端开发中的 Web Vitals —— 提升用户体验的关键技术
前端·ux
hunter2062062 小时前
ubuntu向一个pc主机通过web发送数据,pc端通过工具直接查看收到的数据
linux·前端·ubuntu
qzhqbb2 小时前
web服务器 网站部署的架构
服务器·前端·架构
刻刻帝的海角2 小时前
CSS 颜色
前端·css
浪浪山小白兔3 小时前
HTML5 新表单属性详解
前端·html·html5
lee5763 小时前
npm run dev 时直接打开Chrome浏览器
前端·chrome·npm
2401_897579653 小时前
AI赋能Flutter开发:ScriptEcho助你高效构建跨端应用
前端·人工智能·flutter
光头程序员4 小时前
grid 布局react组件可以循数据自定义渲染某个数据 ,或插入某些数据在某个索引下
javascript·react.js·ecmascript
limit for me4 小时前
react上增加错误边界 当存在错误时 不会显示白屏
前端·react.js·前端框架
浏览器爱好者4 小时前
如何构建一个简单的React应用?
前端·react.js·前端框架