单页面代码分割
目前单页应用的 bundle.js 一般都会很大,这样的话会对启动时间造成影响。为此我们需要合理拆分 js 文件,然后在需要使用的时候才动态记在。
拆分 js 文件,我们通常会采用手段:
- 基于业务逻辑和依赖库分割
- 基于路由分割
- 基于组件分割
左图所示是基于路由进行分割,有图为基于组件进行分割。基于路由分割的力度比较大,一个路由里会嵌套很多组件。每个路由 react 组件还是有很多,但并不是每个组件都会被立刻使用。同时,多个路由之间还有可能会产生很多重复的组件代码。这显然不如基于组件进行分割效果较好。
代码分割案例
假如,我们有个页面,页面的额外信息 extra 组件是不需要的,只有点击查看额外信息按钮后才会出现。我们对 extra 进行惰性加载。代码如下:
|
|
demo 中我们利用了 webpack 分包加载功能,当点击按钮时利用 import 来加载 extra 组件,而不是在开头直接引入。
动态导入(dynamic imports)
上述案例,我们借助 import 函数来引入 extra 包。webpack 在打包的时候会自动检测这样的 import 引入的文件,为其单独生成一个打包 js 文件。如图所示:
当我们点击加载按钮后,红线处的 js 包会被异步载入
webpack 实现动态导如的代码如下:
|
|
注意,使用 import 导入这种形式的话,我们需要使用 dynamic-import 包:
npm install –save-dev @babel/plugin-syntax-dynamic-import
|
|
或者你也可以使用 webpack 提供的 require.ensure 来代替。
react-loadable
上述代码,我们粗略的实现了一个分包加载的demo,业务上我们还要有很多问题来处理。比如:import 失败怎么办?加载前需要 loading 占位怎么做?服务端渲染怎么做?
这里介绍一个库 react-loadable。他会把我们要异步加载的组件封装成一个高阶组件供我们使用
例如,我们异步加载 extra 组件,并且引入一个 loading 作为加载占位,代码如下:
|
|
使用的时候我们直接在恰当的时机渲染 LoadableComponent 组件即可。
除此之外,react-loadable 还提供了预加载,服务端渲染等功能。这里就不详细介绍使用了,主要我们看一下源码实现的过程。
源码解析
函数入口
react-loadable
本质上就是一个高阶组件封装的过程。代码入口如下:
|
|
Loadable 接受我们传入的参数 opts, 再调用了createLoadableComponent函数,传入了 load
函数和 opts。
load 函数
这里我们先来看一下 load
函数:
|
|
load
函数里初始化了一个state对象,执行 loader 方法(即我们传入的 loader 参数 (import(…))) 来加载异步组件。之后将返回结果保存在 state 的 loaded 属性中,加载状态保存在其他属性上。返回这个 state 供后续使用。
createLoadableComponent
接下来,开始分析整个代码的主体部分 createLoadableComponent
函数。其作用就是包装加载后的异步组件,返回一个高阶组件供我们使用。代码如下:
首先检测必须含有 loading 组件,给参数添加默认值等。然后初始化一个 init 方法,用于执行上文的 load
函数以加载异步组件,并记录状态给 res 变量。之后就是返回 LoadableComponent 组件的过程。
在 LoadableComponent 中,发生如下情况:
constructor
阶段:调用 init 方法,保存状态到 state 上componentWillMount
阶段:- 设置 this._mounted 状态默认为 ture 表示加载完成
- 进行一些判断,如果 res.loading 为 false,说明之前的 init 执行出错,直接return
- 如果opts.delay 和 opts.timeout有值,且为number属性的话,就加个定时器用来延迟显示 loading 组件(避免闪烁)
- 声明 update 函数,用来根据异步组件加载的结果更新 state 值
- 如果 this.state.loading 或者 this.state.error 为 true,就是整体状态是正在加载或者出错了,就用 React.createElement 生成出loading的过渡组件
- 如果 this.state.loaded 有值了,说明传入的loader的promise异步操作执行完成,就开始渲染真正的组件,调用opts.render方法
大致过程就是这样,不贴代码了。还有一些细节,对 webpack 等处理,preload 静态方法等,也就一两句话,看看就知道了。
React.lazy
在 React 16.0.0 中,加入了 React.lazy 方法渲染动态导入为常规组件(尚不能用在服务端)。
主要用法如下:
|
|
当渲染此组件时,这将自动加载包含 OtherComponent 包。如果我们需要加载时的 Loading 组件做为
过度使用。我们需要调用 Suspense 组件。
|
|
fallback prop(属性) 接受在等待加载组件时要渲染的任何 React 元素。
异步加载时机
想这么一个问题,如果 extra 包很大,我们点击打开 open 按钮后,要花很长一段时间才能看到组件内容。这样同样也不是什么好的体验。甚至,如果我们加载的包里面逻辑复杂,js 运行时间过长会阻塞掉用户的交互。这样要如何处理呢?
Idle Until Urgent (空闲等待 & 紧急优先)
点击 extra 肯定是延后操作,我们希望在主要任务都完成后,如果浏览器有空闲时间就来预加载组件(空闲等待)。如果我们点击打开按钮,这时候组件还没有加载完毕,那么我们就立刻加载组件(紧急优先)。
实现该操作,我们可以借助 requestIdleCallback 函数。改造代码如下:
|
|
当然,你也可以用 react-loadable 来实现该例子,简要改动如下:
|
|
这种空闲执行的方案,同样使用于执行那些不是很紧急但是耗时很长的函数。例如处理大量数据集,localStorage 中获取数据等等。合理利用 requestIdleCallback 可以减少网页的阻塞,提高效果。
空闲任务队列
上面技术适用于可以通过单个函数计算出来的属性,但在某些情况下,逻辑可能无法写到单个函数里,或者,即使技术上可行,您仍然希望将其拆分为更小的一些函数,以免其长时间阻塞主线程。 这里提供一个 IdleQueue 你可以在里面找到利用 requestIdleCallback 的姿势。
mobx 是否需要拆分
通常我们在写业务的时候,会把 store 写在 <Provider>
上直直接注入。那我们需要对 mobx 的 store 也异步加载呢?理论上没有这个必要,因为 store 里面的内容确实一般不会很大并不是造成包体积过大的原因。优化效果不太明显。
当然如果非要异步加载的话,我这里有个想法,我们将 store 和 action 拆分出来。store 里面只包含数据:
|
|
store 依然在全局在 Provider 注入,每个业务的 action 单独抽离成一个文件:
|
|
action 们单独在一个全局变量中维护,需要异步加载的时候如下:
|
|