logo

鱼肚的博客

Don't Repeat Yourself

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}

基本这种原理,也出现了几种解决方案:

  1. react-loadable
  2. React.lazy
  3. @component/loadable

三者的简单对比如下:

  1. react-loadable不再活跃维护,不再推荐使用
  2. React.lazy是React官方推出的解决方案,不支持SSR
  3. @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。

但是有的组件是比较复杂的,其复杂性主要体现在两个方面:

  1. 某些组件带有子组件。如antd中的 Select.Option
  2. 某些组件带有方法,如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方法同理。

那么如何处理这种复杂组件呢?分为三种情况:

  1. 如果丢失的属性还是React组件,则可以使用 loadable 二次处理下子属性。
  2. 如果丢失的静态方法是异步的,也可以使用 loadable 二次处理下。
  3. 如果丢失的静态方法是同步的,只能放弃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,在改造工程时,需要小心如下的问题。

  1. 如果使用了根据refs寻找dom的操作,则有可能因为延迟加载导致dom查找不到。
  2. 如果依赖了组件的生命周期方法(如componentDidMount)做一些事件绑定,则有可能因为懒加载导致事件处理函数未能及时绑定,丢失一些事件。

结束语

以上就是我最近对dynamic import在React中的应用心得,希望对大家有所帮助。

另外也欢迎大家收藏我的博客。