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