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
相关推荐
Myli_ing17 分钟前
HTML的自动定义倒计时,这个配色存一下
前端·javascript·html
dr李四维34 分钟前
iOS构建版本以及Hbuilder打iOS的ipa包全流程
前端·笔记·ios·产品运营·产品经理·xcode
雯0609~1 小时前
网页F12:缓存的使用(设值、取值、删除)
前端·缓存
℘团子এ1 小时前
vue3中如何上传文件到腾讯云的桶(cosbrowser)
前端·javascript·腾讯云
学习前端的小z1 小时前
【前端】深入理解 JavaScript 逻辑运算符的优先级与短路求值机制
开发语言·前端·javascript
彭世瑜1 小时前
ts: TypeScript跳过检查/忽略类型检查
前端·javascript·typescript
FØund4041 小时前
antd form.setFieldsValue问题总结
前端·react.js·typescript·html
Backstroke fish1 小时前
Token刷新机制
前端·javascript·vue.js·typescript·vue
小五Five2 小时前
TypeScript项目中Axios的封装
开发语言·前端·javascript
小曲程序2 小时前
vue3 封装request请求
java·前端·typescript·vue