Dynamic import 提出已有一段时间,围绕着它也产生了许多的解决方案。今天我分享一下最近在React工程中使用它的一些心得。
Dynamic import的基本使用形式,在Github中有案例:
<!DOCTYPE html>
<nav>
<a href="books.html" data-entry-module="books">Books</a>
<a href="movies.html" data-entry-module="movies">Movies</a>
<a href="video-games.html" data-entry-module="video-games">Video Games</a>
</nav>
<main>Content will load here!</main>
<script>
const main = document.querySelector("main");
for (const link of document.querySelectorAll("nav > a")) {
link.addEventListener("click", e => {
e.preventDefault();
import(`./section-modules/${link.dataset.entryModule}.js`)
.then(module => {
module.loadPageInto(main);
})
.catch(err => {
main.textContent = err.message;
});
});
}
</script>
可以看出,它的用法是 import('./section-module/xxx').then()
这种方式。
即:import可以作为函数来使用,参数为路径,返回的结果是Promise。
对于普通的模块,要使用dynamic import只需要修改其引入方式,并合理地处理promise即可。
举例来说,假设原来有这样一段代码:
import zhCN from './zhCN'
import enUS from './enUS'
export const translate = function (word, lang = 'zhCN') {
switch (lang) {
case 'zhCN': return zhCN[word];
case 'enUS': return enUS[word];
}
return word;
}
像这样和语言有关的场景,因为一般只需要使用一种语言,所以懒加载就很容易变成不加载,效果更好。
要把它改成 dynamic import的话,一是要修改import的方式,二是要把translate方法修改成async方法,当然相应的调用处也需要更新。
改造之后的代码如下:
export const translate = async function (word, lang = 'zhCN') {
switch (lang) {
case 'zhCN': return (await import('./zhCN'))[word];
case 'enUS': return (await import('./enUS'))[word];
}
return word;
}
有条件的话,可以使用Webpack bundle analyzer等工具来检查改造前后输出Bundle的差异。
在上面的例子中,我们可以看到dynamic import的一个副作用,就是对应的方法必须是异步的。
这对于React组件来说是个挑战,因为React组件的render方法中不应该有异步处理,自然也不能去处理这些因dynamic import页产生的async方法/promise。
但是这个问题并不是没有解决办法。
要处理React组件的异步获取,原理上也并不复杂。
stateDiagram
componentDidMount --> renderLoading
componentDidMount --> renderLoaded
state componentDidMount {
[*] --> import
import --> [*]
}
state renderLoading {
[*] --> 加载中
加载中 --> [*]
}
state renderLoaded {
[*] --> 组件
组件 --> [*]
}
大概的代码如下:
export class Loadable extends React.Component {
state = {
Comp: null
}
async componentDidMount() {
const Comp = await import('./component-a')
setState({ Comp })
}
render() {
const { Comp } = this.state
return Comp ? <Comp {...this.props} /> : <div>loading</div>
}
}
基本这种原理,也出现了几种解决方案:
- react-loadable
- React.lazy
- @component/loadable
三者的简单对比如下:
- react-loadable不再活跃维护,不再推荐使用
- React.lazy是React官方推出的解决方案,不支持SSR
- @component/loadable是一个在活跃维护的开源项目,支持SSR,也得到React官方的推荐。
参见React官方文档:https://zh-hans.reactjs.org/docs/code-splitting.html
我采用的是@component/loadable这一方案,它的使用方式如下:
// import Button from 'antd/lib/button'
import loadable from '@loadable/component'
const Button = loadable(async () => import('antd/lib/button'))
ReactDOM.render(
<div>
<Button type="primary">Primary</Button>
</div>,
mountNode,
);
上面举例的button只是个简单的组件,直接使用 loadable 将其包一层,即可实现dynamic import。
但是有的组件是比较复杂的,其复杂性主要体现在两个方面:
- 某些组件带有子组件。如antd中的 Select.Option
- 某些组件带有方法,如antd中的Form.create
这里先解释下原因:
在上面的示意代码中,我们也可以看到 loadable 本身是一个Component,它内部会处理Promise得到真正的组件。在Promise解析完成之前,它没办法知道此组件上有哪些静态属性和方法。
因此如果使用这种方法包裹Select组件
import loadable from '@loadable/component'
const Select = loadable(async () => import('antd/lib/select'))
那么这个Select组件上就必定会丢失掉 Option
这个静态属性,Form.create
方法同理。
那么如何处理这种复杂组件呢?分为三种情况:
- 如果丢失的属性还是React组件,则可以使用 loadable 二次处理下子属性。
- 如果丢失的静态方法是异步的,也可以使用 loadable 二次处理下。
- 如果丢失的静态方法是同步的,只能放弃dynamic import了。
以Select举例,在改成dynamic import之前,它一般是这样使用的:
import Select from 'antd/lib/select'
ReactDOM.render(
<Select defaultValue="lucy" style={{ width: 120 }} disabled>
<Select.Option value="lucy">Lucy</Select.Option>
</Select>
mountNode,
);
要使用 dynamic import的话,需要改成这样:
// import Select from 'antd/lib/select'
import loadable from '@loadable/component'
const Select = loadable(async () => import('antd/lib/select'))
Select.Option = loadable(async () => import('antd/lib/select')
.then(select => select.Option))
ReactDOM.render(
<Select defaultValue="lucy" style={{ width: 120 }} disabled>
<Select.Option value="lucy">Lucy</Select.Option>
</Select>
mountNode,
);
即将 Option
组件也改成loadable包裹的形式。
假设有个React组件Foo
上带有异步方法bar
,原来的用法如下:
import Foo from 'foo'
ReactDOM.render(
<Foo onClick={() => Foo.bar() } />
mountNode,
);
上面的例子中,假设Foo.bar()是异步的,那么改成Promise也就不会影响功能,所以还可以继续使用loadable,改造后的代码如下:
// import Foo from 'foo'
import loadable from '@loadable/component'
const Foo = loadable(async () => import('foo'))
Foo.bar = async (...args) => (await import('foo')).bar(args)
ReactDOM.render(
<Foo onClick={() => Foo.bar() } />
mountNode,
);
这里在二次封装bar
方法的时候,直接使用的import
,而不是@loadable/component
,因其不是React组件,只是个异步函数。
放弃吧!
或者教教我吧。
懒加载有可能引发一些bug,在改造工程时,需要小心如下的问题。
- 如果使用了根据refs寻找dom的操作,则有可能因为延迟加载导致dom查找不到。
- 如果依赖了组件的生命周期方法(如componentDidMount)做一些事件绑定,则有可能因为懒加载导致事件处理函数未能及时绑定,丢失一些事件。
以上就是我最近对dynamic import在React中的应用心得,希望对大家有所帮助。
另外也欢迎大家收藏我的博客。