react代码分割


2019-9-4 react

前言

随着你的应用的增长,你的包文件也会增长。特别是如果你引用了大型的第三方库。你需要密切关注包含在你包文件中的代码,这样你就不会意外地把它弄得太大,以至于你的应用程序需要很长时间才能加载完成。这是一段摘自官网的说明,详情点击官网说明

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


import()

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

注意:

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

如下面的例子:

export default class Home extends Component {
  state = {
    Load: null
  };

  showLoad = () => {
    const { Load } = this.state;
    if (!Load) {
      return <div>Loading123</div>;
    }
    return <Load />;
  }

  componentWillMount () {
    import('./load').then(load => {
      this.setState({ Load: load.default });
    });
  }

  render () {

    return (
      <div>
        {
          this.showLoad()
        }
      </div>
    )
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

四种方式

目前处理代码分割有四种比较好的处理方式:

  • 1.webpack
  • 2.React.lazy
  • 3.Loadable Components
  • 4.自定义AsyncComponent组件

那咱们一个一个的来看看是怎么处理的,let's go

在我们 react app 中,常见的路由配置可能是像下面一样的

export default () => (
  <Switch>
    <Route path="/" exact component={Home} />
    <Route path="/Home" exact component={Home} />
    <Route path="/my" exact component={My} />
    <Route path="/find" exact component={Find} />
  </Switch>
);
1
2
3
4
5
6
7
8

webpack的实现

webpack代码分割

在最开始使用Webpack的时候, 都是将所有的js文件全部打包到一个build.js文件中(文件名取决与在webpack.config.js文件中output.filename), 但是在大型项目中, build.js可能过大, 导致页面加载时间过长. 这个时候就需要code splitting, code splitting就是将文件分割成块(chunk), 我们可以定义一些分割点(split point), 根据这些分割点对文件进行分块, 并实现按需加载。

代码分割,也就是Code Splitting一般需要做这些事情:

1 为 Vendor 单独打包(Vendor 指第三方的库或者公共的基础组件,因为 Vendor 的变化比较少,单独打包利于缓存)
2 为 Manifest (Webpack 的 Runtime 代码)单独打包
3 为不同入口的公共业务代码打包(同理,也是为了缓存和加载速度) 4 为异步加载的代码打一个公共的包

Code Splitting 一般是通过配置 CommonsChunkPlugin 来完成的。一个典型的配置如下,分别为 vendor、manifest 和 vendor-async 配置了 CommonsChunkPlugin。

    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks (module) {
        return (
          module.resource &&
          /\.js$/.test(module.resource) &&
          module.resource.indexOf(
            path.join(__dirname, '../node_modules')
          ) === 0
        )
      }
    }),

    new webpack.optimize.CommonsChunkPlugin({
      name: 'manifest',
      minChunks: Infinity
    }),

    new webpack.optimize.CommonsChunkPlugin({
      name: 'app',
      async: 'vendor-async',
      children: true,
      minChunks: 3
    }),
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

CommonsChunkPlugin 的特点就是配置比较难懂,大家的配置往往是复制过来的,这些代码基本上成了模板代码(boilerplate)。如果 Code Splitting 的要求简单倒好,如果有比较特殊的要求,比如把不同入口的 vendor 打不同的包,那就很难配置了。总的来说配置 Code Splitting 是一个比较痛苦的事情。

而 Long-term caching 策略是这样的:给静态文件一个很长的缓存过期时间,比如一年。然后在给文件名里加上一个 hash,每次构建时,当文件内容改变时,文件名中的 hash 也会改变。浏览器在根据文件名作为文件的标识,所以当 hash 改变时,浏览器就会重新加载这个文件。

Webpack 的 Output 选项中可以配置文件名的 hash,比如这样:

output: {
  path: config.build.assetsRoot,
  filename: utils.assetsPath('js/[name].[chunkhash].js'),
  chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
},
1
2
3
4
5

正如上面说的,CommonsChunkPlugin相对比较难懂,所以在webpack4废弃了 CommonsChunkPlugin,引入了 optimization.splitChunks 这个选项。

optimization.splitChunks 默认是不用设置的。如果 mode 是 production,那 Webpack 4 就会开启 Code Splitting。

默认 Webpack 4 只会对按需加载的代码做分割。如果我们需要配置初始加载的代码也加入到代码分割中,可以设置 splitChunks.chunks 为 'all'。


React.lazy的实现

react 16.6 发布了新的功能 lazy ,和一个组件 Suspense

还是上面的例子;进行改装一下

const AsyncHome = lazy(() => import('../views/Home'));
const AsyncMy = lazy(() => import('../views/My'));
const AsyncFind = lazy(() => import('../views/Find'));

export default () => (
  <Switch>
    <Suspense fallback={<div>Loading...</div>}>
      <Route path="/" exact component={AsyncHome} />
      <Route path="/Home" exact component={AsyncHome} />
      <Route path="/my" exact component={AsyncMy} />
      <Route path="/find" exact component={AsyncFind} />
    </Suspense>
  </Switch>
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14

这种用法同样可以使用在小组件上;

const InputSeach = lazy(() => import('./InputSeach'));

export default class Home extends Component {

  fallback = () => {
    return (
      <div>Loading...</div>
    );
  }

  render () {
    return (
      <div>
        <Suspense fallback={this.fallback()}>
          thomas small train
          <InputSeach />
        </Suspense>
      </div>
    )
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

通过操作来看,其实api来说很简单,就两句代码,这就实现了懒加载;

import React, { lazy, Suspense } from 'react';
const Child = lazy(() => import('./Child'))

但是还是需要注意的是,lazy非要搭配Suspense使用,

可以试想一下,既然是懒加载,那么当Child还未加载完成之前,这个视图怎么办?

bingo!Suspense就是为了处理这个的,让视图更友好,为懒加载组件做优雅降级,它叫加载指示器

Suspense 可以放在懒加载的组件外层的任意位置,但是必须要添加fallback属性,不然会报错,fallback是懒加载组件载入过程中的一个过渡,可以放一些过渡效果或方法。或者放一个loading的组件提示;

注意:React.lazy和 Suspense 尚不可用于服务器端,如果做服务端渲染的同学官方还是建议使用 React Loadable 或者下面这家伙


Loadable Components的实现

这个种方式比较简单,就是安装一个包即可,里面已经做了相关个配置;详情请看官网;他的文档做的非常的详细,这里,我们就引用官网的例子即可:

npm install @loadable/component

const OtherComponent = loadable(() => import('./OtherComponent'))

function MyComponent() {
  return (
    <div>
      <OtherComponent />
    </div>
  )
}
1
2
3
4
5
6
7
8
9

自定义AsyncComponent组件的实现

回到开始的那个路由引入的例子,我们一开始引入这些组件,然后定义好的路径,会根据我们的路由去匹配这些组件。

但是,我们静态地在顶部导入路由中的所有组件。这意味着,不管哪个路由匹配,所有这些组件都被加载。我们只想加载对匹配路由的时候才加载响应的组件。下面我们一步步来完成这个使命。

创建一个js 文件,如:src/components/AsyncComponent.js,代码如下:

export default function asyncComponent (importComponent) {
  class AsyncComponent extends Component {
    constructor(props) {
      super(props);

      this.state = {
        component: null
      };
    }

    async componentDidMount () {
      const { default: component } = await importComponent();

      this.setState({
        component: component
      });
    }

    render () {
      const C = this.state.component;

      return C ? <C {...this.props} /> : null;
    }
  }

  return AsyncComponent;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

我们在这里做了一些事情:

  • 这个asyncComponent 函数接受一个importComponent 的参数,importComponent 调用时候将动态引入给定的组件。
  • 在componentDidMount 我们只是简单地调用importComponent 函数,并将动态加载的组件保存在状态中。
  • 最后,如果完成渲染,我们有条件地提供组件。在这里我们如果不写null的话,也可提供一个菊花图,代表着组件正在渲染。

现在让我们使用我们的异步组件,而不是像开始的静态去引入。

import Home from '../views/Home';(这是静态引入)

我们要用asyncComponent组件来动态引入我们需要的组件。

import asyncComponent from '../components/AsyncComponent
const AsyncHome = asyncComponent(() => import('../views/Home'));

我们将要使用 AsyncHome 这个组件在我们的路由里面

< Route path="/" exact component={AsyncHome} />

修改一开始的例子:

const AsyncHome = AsyncComponent(() => import('../views/Home'))
const AsyncMy = AsyncComponent(() => import('../views/My'))
const AsyncFind = AsyncComponent(() => import('../views/Find'))

export default () => (
  <Switch>
    <Route path="/" exact component={AsyncHome} />
    <Route path="/Home" exact component={AsyncHome} />
    <Route path="/my" exact component={AsyncMy} />
    <Route path="/find" exact component={AsyncFind} />
  </Switch>
);
1
2
3
4
5
6
7
8
9
10
11
12

现在你运行npm run build 您将看到代码已经被分割成一个个小文件。

关于自定义组件,更多请看原文


关于react的代码分割部分,就先到此。

感谢蚂蚁金服数据体验技术;

Thomas: 10/15/2019, 11:12:42 AM