在React中动态加载模块
起因
在 React 项目中常会遇到打包出来的 bundle 过大而引发的页面加载速度过慢或是性能问题。因此,如果能够将使用的模块按需在使用时动态加载,就可以很大程度地减少资源浪费并优化首次加载速度。
例如,在使用 highlight.js 时,官方为我们提供了 176 种语言的支持,每种语言支持包的大小从 1K 到几十 K 不等,一次性全部加载就需要 load 1MB 的资源。而如果采用按需加载的方式,每次就只需要 load 几 K 的资源。想想就很酷。
在 webpack v2 之后的版本中,可以使用 import()
来完成代码分割和动态载入。借助这个 feature ,可以实现上文预想的功能。
关于 import()
JavaScript 中的模块是纯静态的。使用import
和export
的语句必须放在模块的顶层,它们会在 编译 时执行(而不是运行时)。因此,如果你在 JS 代码编译之后修改了 module 中的内容,即使还没有用到它,也无法对运行中的程序造成影响。另外,如果想把import
语句放在if
代码块中或函数也是不行的,这么做会报语法错误。
这个 feature 带来的好处是可以提高编译器的执行效率,但是无法在运行时加载模块。
为了解决这个问题,有一个提案建议引入import()
方法来进行动态加载。例如:
const specifier = './module.js'
import(specifier).then(someModule => someModule.foo())
import()
方法和import
语句能使用的参数是一样的,它使用起来类似于一个函数。在 ES6 中, import()
方法会返回一个Promise
对象。当模块加载完成时,实现这个Promise
。
通过import()
方法,可以按需求加载模块,例如:
button.addEventListener('click', () => {
import('./dialogBox.js')
.then(dialogBox => {
dialogBox.open()
})
.catch(error => {
/* Error handling */
})
})
或者将需要加载的模块放在判断中:
if (condition) {
import('module')
}
还可以配合 Promise 的特性来加载模块:
Promise.all([
import('./module1.js'),
import('./module2.js'),
import('./module3.js'),
])
在 webpack 中,如果使用import()
函数来加载模块,webpack 在打包时会自动将模块拆分,并仅在需要使用时才载入这个模块。
在 React 中动态加载模块
在 React 中使用import()
对模块动态加载,需要合理地搭配生命周期函数。
以上文所说的应用场景为例,它的基础用法大致如下:
import React from 'react'
import Lowlight from 'react-lowlight'
import PropTypes from 'prop-types'
import js from 'highlight.js/lib/languages/javascript'
// 使用之前要先对模块进行注册
Lowlight.registerLanguage('js', js)
class CodeRenderer extends React.Component {
static propTypes = {
value: PropTypes.string,
language: PropTypes.string,
}
static defaultProps = {
value: '',
language: 'js',
}
render() {
return <Lowlight value={this.props.value} language={this.props.language} />
}
}
export default CodeRenderer
为了动态加载模块,需要修改成如下的样子:
import React from 'react'
import Lowlight from 'react-lowlight'
import PropTypes from 'prop-types'
const supportLanguages = {
js: () => import('highlight.js/lib/languages/javascript'),
}
class CodeRenderer extends React.Component {
static propTypes = {
value: PropTypes.string,
language: PropTypes.string,
}
static defaultProps = {
value: '',
language: 'js',
}
state = {
displayLanguage: '',
}
static getDerivedStateFromProps(nextProps, prevState) {
const language = nextProps.language,
displayLanguage = prevState.displayLanguage
if (language === displayLanguage) return null
// 如果新的language 已被注册, 直接在下一次render 中使用该language渲染
if (Lowlight.hasLanguage(language)) {
return { displayLanguage: language }
}
// 否则按没有指定language 进行渲染
return { displayLanguage: '' }
}
loadLanguageSupport = () => {
// 如果没有指定language 并且highlightjs的库中有对这个language提供支持
if (
!this.state.displayLanguage &&
supportLanguages.hasOwnProperty(this.props.language)
) {
const syntaxBundle = supportLanguages[this.props.language]
syntaxBundle().then(bundle => {
// 注册新的 language
Lowlight.registerLanguage(this.props.language, bundle)
// 更新state
this.setState({ displayLanguage: this.props.language })
})
}
}
componentDidMount() {
this.loadLanguageSupport()
}
componentDidUpdate() {
this.loadLanguageSupport()
}
render() {
if (!this.state.displayLanguage)
return (
<pre>
<code>{this.props.value}</code>
</pre>
)
return (
<Lowlight
value={this.props.value}
language={this.state.displayLanguage}
/>
)
}
}
export default CodeRenderer
这里主要做了几件事:
- 将语言支持的模块变成了一个 function (
import('highlight.js/xxx')
) , webpack 会将它自动拆分; - 当 state 中的
displayLanguage
为空时,视为没有载入这个语言的支持模块; - 在
componentDidMount
中和componentDidUpdate
中,载入所需要的语言支持模块并将其注册,然后再更新state
并重新 render。 - 在
render
时,如果没有载入当前语言的支持模块,则按照普通的文本进行渲染。当然也可以在shouldComponentUpdate
中设置减少重新渲染的次数以提升性能。
由于将没有注册的 language 作为 props 传入Lowlight
组件时会报错,因此更新 state(包括setState
或在getDerivedStateFromProps
中返回新的值)这个动作必须发生在Lowlight.registerLanguage
之后。由于无法预测何时可以加载完成并注册语言的支持包,因此不能将import()
的异步动作放在getDerivedStateFromProps
中。
使用 Chrome 调试台查看一下效果。
这是首次打开时加载的内容。其中 chunk6 是当前页面的模块。
然后手动修改 markdown 中代码块的语言为java
,可以看到载入了一个新的 chunk
查看调用栅可以看到是通过上述代码异步加载的模块。
最后
在 React 中动态加载模块可以让你的 Application 运行起来更有效率。其实动态加载模块还可以用在页面拆分、组件异步加载等功能上。可以参考这篇文章 。同时,在 npm 上还有很多用于组件异步加载的 package 可以直接使用。
另外,如果你使用的是 webpack v1, 可能需要使用babel-plugin-dynamic-import-webpack 这个 package 将import()
转为require.ensure。