代码拆分(Code-Splitting)

打包(Bundling)

大多数的 React 应用程序使用WebpackBrowserify 等工具来 “打包” 文件。打包(Bundling) 是一个处理过程,跟踪导入的文件并将其合并到单个文件:“包” 。然后,这个包文件可以包含到网页上,这样可以一次性加载整个应用程序。

示例

App:

// app.js
import { add } from './math.js';

console.log(add(16, 26)); // 42
// math.js
export function add(a, b) {
  return a + b;
}

打包后的包文件:

function add(a, b) {
  return a + b;
}

console.log(add(16, 26)); // 42

注意:

你的最终包文件,看起来不同于这个。

如果你使用的是Create React AppNext.jsGatsby 或类似的工具,您将拥有一个开箱即用的 Webpack 安装程序来打包你的应用程序。

如果你没用使用这些工具,你需要自己安装 Webpack 。 例如,请参阅Webpack文档上的安装入门 指南。

代码拆分

打包工具非常强大,但是随着你的应用的增长,你的包文件也会增长。特别是如果你引用了大型的第三方库。你需要密切关注包含在你包文件中的代码,这样你就不会意外地把它弄得太大,以至于你的应用程序需要很长时间才能加载完成。

为了避免生成一个很大的包文件,最好事先解读这个问题,开始拆分你的包文件。Code-Splitting 是很多打包工具都支持的特性,比如 Webpack 和 Browserify (通过 factor-bundle ),它可以创建多个包文件,可以在运行时动态加载。

代码拆分 你的应用程序可以帮助你 “懒加载(lazy-load)” 用户当前需要的东西,这可以显着提高您的应用程序的性能。虽然你没有减少应用程序中的代码总量,但是你已经避免了加载用户可能不需要的代码,并且减少了初始加载过程中的代码量。

import()

将代码拆分引入到应用程序中的最好方法是通过动态 import() 语法。

之前:

import { add } from './math';

console.log(add(16, 26));

以后:

import("./math").then(math => {
  console.log(math.add(16, 26));
});

注意:

动态 import() 语法是ECMAScript(JavaScript)提案,目前不是语言标准的一部分。预计在不远的将来会被接受。

当 Webpack 遇到这个语法时,它会自动启动 代码拆分 来拆分你的应用程序。 如果您正在使用 Create React App ,则已经为你配置好了,你可以立即开始使用它。 在 Next.js 中,它也支持开箱即用。

如果你是自己设置 Webpack ,你可能需要阅读 Webpack 关于 代码拆分的指南。 你的 Webpack 配置应该看起来类似于 这样

在使用 Babel 时,你需要确保 Babel 能够解析动态导入语法,但目前 Babel 还不能直接对其进行转换。 为此,你需要使用 babel-plugin-syntax-dynamic-import

React.lazy

注意:

React.lazy 和 Suspense 尚不可用于服务器端渲染。如果要在服务器渲染的应用程序中进行代码拆分,我们建议使用 Loadable Components 。它有一个很好的服务器端渲染打包拆分指南

React.lazy 函数允许您渲染动态导入为常规组件。

之前:

import OtherComponent from './OtherComponent';

function MyComponent() {
  return (
    <div>
      <OtherComponent />
    </div>
  );
}

之后:

const OtherComponent = React.lazy(() => import('./OtherComponent'));

function MyComponent() {
  return (
    <div>
      <OtherComponent />
    </div>
  );
}

当渲染此组件时,这将自动加载包含 OtherComponent 包。

React.lazy 接受一个函数,必须调用动态 import() 。 这必须返回一个 Promise ,它 resolves 为一个带有包含 React 组件的 default 导出的模块。

Suspense

如果在 MyComponent 渲染时尚未加载包含 OtherComponent 的模块,我们必须在等待加载时显示一些后备内容 - 例如加载指示符。 这是使用Suspense 组件完成的。

const OtherComponent = React.lazy(() => import('./OtherComponent'));

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

fallback prop(属性) 接受在等待加载组件时要渲染的任何 React 元素。 您可以将 Suspense 组件放在惰性组件上方的任何位置。 您甚至可以使用一个 Suspense 组件包装多个惰性组件。

const OtherComponent = React.lazy(() => import('./OtherComponent'));
const AnotherComponent = React.lazy(() => import('./AnotherComponent'));

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

错误边界

如果其他模块无法加载(例如,由于网络故障),则会触发错误。 您可以使用 错误边界 处理这些错误以显示良好的用户体验并管理恢复。 一旦创建了错误边界,就可以在惰性组件上方的任何位置使用它,以便在出现网络错误时显示错误状态。

import MyErrorBoundary from './MyErrorBoundary';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
const AnotherComponent = React.lazy(() => import('./AnotherComponent'));

const MyComponent = () => (
  <div>
    <MyErrorBoundary>
      <Suspense fallback={<div>Loading...</div>}>
        <section>
          <OtherComponent />
          <AnotherComponent />
        </section>
      </Suspense>
    </MyErrorBoundary>
  </div>
);

基于路由的代码拆分

决定在应用程序中引入 代码拆分 的位置可能有点棘手。您需要确保选择的位置可以将 拆分包 均匀分配,但不会破坏用户体验。

路由开始处是一个好的地方。网上的大多数人都习惯于页面过渡,知道页面需要花费一定的时间来加载。你也倾向于一次重新渲染整个页面,因此你的用户不可能同时与页面上的其他元素进行交互。

以下是如何使用 React RouterReact.lazy 之类的库设置基于路由的 代码拆分 来拆分你的应用程序的示例。

import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import React, { Suspense, lazy } from 'react';

const Home = lazy(() => import('./routes/Home'));
const About = lazy(() => import('./routes/About'));

const App = () => (
  <Router>
    <Suspense fallback={<div>Loading...</div>}>
      <Switch>
        <Route exact path="/" component={Home}/>
        <Route path="/about" component={About}/>
      </Switch>
    </Suspense>
  </Router>
);

命名导出(Exports)

React.lazy 目前仅支持默认导出。 如果想要导入的模块使用命名导出,则可以创建一个中间模块,将其重新导出为默认模块。 这可确保 treeshak 继续工作,并且您不会引入未使用的组件。

// ManyComponents.js
export const MyComponent = /* ... */;
export const MyUnusedComponent = /* ... */;
// MyComponent.js
export { MyComponent as default } from "./ManyComponents.js";
// MyApp.js
import React, { lazy } from 'react';
const MyComponent = lazy(() => import("./MyComponent.js"));