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