React 组件代码分割和加载
当你的应用足够庞大时,把所有代码简单地打成一个 bundle,启动时间会很长。你需要将 app 分割成几个 bundle,按需加载。

A single giant bundle vs. multiple smaller bundles
Browserify 和Webpack 等工具可以很好地解决如何将一个大 bundle 分割的问题。
那么你就需要决定在哪儿可以分离出另一个 bundle 进行异步加载。App 还需要在加载时给用户提示。
基于路由的分割 vs 基于组件的分割
通常的建议是将 app 分成独立的路径,然后每个异步加载。这对大多 app 都适用,点击链接然后加载一个新的页面,这种体验还可以。
但是我们可以做得更好。
React 的多数路由工具都是一个路径就是一个组件。没什么特别的。如果我们在组件上进行优化而不是让路径来负责这个任务会怎样呢?

Route vs. component centric code splitting
显然组件的方式更好些。你可以轻松地在更多地方分割 app,Modals、tabs以及很多用户触发才展示内容的 UI 组件等,而不仅是路径。
更不用说那些延迟加载直到高优先级的内容加载完的地方。页面底部的组件加载一堆库:为什么在顶部时就要加载那些库呢?
你也可以简单地按路由分割,因为它们也是组件。看哪种方式更适合你的 app 了。
但是我们需要让组件级分割和路由一样简单。新的分割应该改几行代码就可以了,其它都会自动完成。
React Loadable 介绍
大家都说组件分割很难实现,然后我就写了一个小库——React Loadable。
Loadable 是一款可以轻松分割组件级 bundle 的高阶组件(创建组件的函数)。
假设有两个组件,其中一个引入并渲染另一个。
1
2
3
4
5
6
7
|
import
AnotherComponent
from
'./another-component'
;
class
MyComponent
extends
React
.
Component
{
render
(
)
{
return
<
AnotherComponent
/
>
;
}
}
|
目前通过 import
同步引入 AnotherComponent
这个依赖。我们需要一种可以异步加载的方式。
dynamic import(目前处于第 3 阶段的 tc39 提议)可以使组件异步加载 AnotherComponent
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
class
MyComponent
extends
React
.
Component
{
state
=
{
AnotherComponent
:
null
}
;
componentWillMount
(
)
{
import
(
'./another-component'
)
.
then
(
AnotherComponent
=
>
{
this
.
setState
(
{
AnotherComponent
}
)
;
}
)
;
}
render
(
)
{
let
{
AnotherComponent
}
=
this
.
state
;
if
(
!
AnotherComponent
)
{
return
<
div
>
Loading
.
.
.
<
/
div
>
;
}
else
{
return
<
AnotherComponent
/
>
;
}
;
}
}
|
但是这需要一系列的人为操作,而且有许多不同的场景无法适用。import()
失败了怎么办呢?服务端渲染呢?
这个问题可以用 Loadable
进行抽象。Loadable 使用起来很简单,只要传入加载组件的函数和加载组件过程中展示的“Loading”组件就可以了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
import
Loadable
from
'react-loadable'
;
function
MyLoadingComponent
(
)
{
return
<
div
>
Loading
.
.
.
<
/
div
>
;
}
const
LoadableAnotherComponent
=
Loadable
(
{
loader
:
(
)
=
>
import
(
'./another-component'
)
,
LoadingComponent
:
MyLoadingComponent
}
)
;
class
MyComponent
extends
React
.
Component
{
render
(
)
{
return
<
LoadableAnotherComponent
/
>
;
}
}
|
但是如果组件加载失败了呢?我们还需要有 error 状态。
为了给你最大的控制权,决定什么时候展示什么,error
只会简单地作为 LoadingComponent
的属性抛出。
1
2
3
4
5
6
7
|
function
MyLoadingComponent
(
{
error
}
)
{
if
(
error
)
{
return
<
div
>
Error
!
<
/
div
>
;
}
else
{
return
<
div
>
Loading
.
.
.
<
/
div
>
;
}
}
|
import() 自动分割代码
import()
的一个优点是增加新代码时,Webpack 2 可以自动分割代码。
也就是说你只要使用 React Loadable、改用 import()
,就可以轻松地用新的代码分割点进行试验,来看看哪种方法最适合你的应用。
此处可以查看示例项目,或者查阅 Webpack 2 文档(注:一些相关文档位于 require.ensure() 章节)。
Loading 组件避免一闪而过
有时组件加载很快(<200ms),loading 屏只在屏幕上一闪而过。
一些用户研究已证实这会导致用户花更长的时间接受内容。如果不展示任何 loading 内容,用户会接受得更快。
所以 loading 组件有一个 pastDelay
属性,仅在组件加载时间超过设置的 delay
时值为 true
。
1
2
3
4
5
6
7
8
9
|
export
default
function
MyLoadingComponent
(
{
error
,
pastDelay
}
)
{
if
(
error
)
{
return
<
div
>
Error
!
<
/
div
>
;
}
else
if
(
pastDelay
)
{
return
<
div
>
Loading
.
.
.
<
/
div
>
;
}
else
{
return
null
;
}
}
|
delay
默认是 200ms,可以向 Loadable
传递第 3 个参数自定义 delay
。
1
2
3
4
5
|
Loadable
(
{
loader
:
(
)
=
>
import
(
'./another-component'
)
,
LoadingComponent
:
MyLoadingComponent
,
delay
:
300
}
)
;
|
预加载
你也可以在组件渲染前预加载进行优化。
例如需要在点击按钮时加载新的组件,就可以在用户悬浮在按钮上时预加载组件。
Loadable
创建的组件会暴露一个 preload
静态方法用来实现上述效果。
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
|
let
LoadableMyComponent
=
Loadable
(
{
loader
:
(
)
=
>
import
(
'./another-component'
)
,
LoadingComponent
:
MyLoadingComponent
,
}
)
;
class
MyComponent
extends
React
.
Component
{
state
=
{
showComponent
:
false
}
;
onClick
=
(
)
=
>
{
this
.
setState
(
{
showComponent
:
true
}
)
;
}
;
onMouseOver
=
(
)
=
>
{
LoadableMyComponent
.
preload
(
)
;
}
;
render
(
)
{
return
(
<
div
>
<
button
onClick
=
{
this
.
onClick
}
onMouseOver
=
{
this
.
onMouseOver
}
>
Show
loadable
component
<
/
button
>
{
this
.
state
.
showComponent
&&
<
LoadableMyComponent
/
>
}
<
/
div
>
)
}
}
|
服务端渲染
Loader 通过最后一个参数支持服务端渲染。
向正在异步加载的模块传递精确路径,Loader 就会在服务端运行时同步地 require()
模块。
1
2
3
4
5
6
7
8
|
import
path
from
'path'
;
const
LoadableAnotherComponent
=
Loadable
(
{
loader
:
(
)
=
>
import
(
'./another-component'
)
,
LoadingComponent
:
MyLoadingComponent
,
delay
:
200
,
serverSideRequirePath
:
path
.
join
(
__dirname
,
'./another-component'
)
}
)
;
|
也就是说经过异步加载、代码分割的 bundle 可以在服务端同步渲染。这样客户端获取备份会有问题。我们可以在服务端渲染全部应用,但是在客户端需要一个个加载 bundle。
但是如果我们可以指定哪些 bundle 需要加入服务端的 bundle 进程呢?那么我们就可以一次性向客户端装载那些 bundle了,客户端就可以准确获取服务端渲染的状态了。
现在离实现已经很接近了。
因为在 Loadable
中我们能够拿到服务端需要的全部路径,所以我们可以增加一个 flushServerSideRequires
函数,返回最后在服务端渲染的所有路径。然后通过 webpack --json
命令可以匹配文件和该文件结束所在的 bundle(此处查看代码)。
仅存的问题是让 Webpack 在客户端完美运行。我会在发布这个 Sean 后等着你们的消息。
可以完美整合的话,我们可以构建各种各样的炫酷工具。React Fiber 让我们更了解哪些 bundle 是想要立即装载的,哪些是等待高优先级的任务完成后再装载的。
最后请安装 Loadable,给仓库颗星星吧。
1
2
3
4
5
|
yarn
add
react
-
loadable
# or
npm
install
--
save
react
-
loadable
|