Dynamic Import在React中的应用

发布于: 2020-04-14作者: 鱼肚最后更新: 2020-09-09

Dynamic import 提出已有一段时间,围绕着它也产生了许多的解决方案。今天我分享一下最近在React工程中使用它的一些心得。

基本原理

Dynamic import的基本使用形式,在Github中有案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!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普通模块

对于普通的模块,要使用dynamic import只需要修改其引入方式,并合理地处理promise即可。

举例来说,假设原来有这样一段代码:

1
2
3
4
5
6
7
8
9
10
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方法,当然相应的调用处也需要更新。

改造之后的代码如下:

1
2
3
4
5
6
7
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组件的特殊性

在上面的例子中,我们可以看到dynamic import的一个副作用,就是对应的方法必须是异步的。

这对于React组件来说是个挑战,因为React组件的render方法中不应该有异步处理,自然也不能去处理这些因dynamic import页产生的async方法/promise。

但是这个问题并不是没有解决办法。

简单的React组件的处理

要处理React组件的异步获取,原理上也并不复杂。

大概的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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> 
  }
}

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

  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
2
3
4
5
6
7
8
9
10
// 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,
);

复杂React组件的处理

上面举例的button只是个简单的组件,直接使用 loadable 将其包一层,即可实现dynamic import。

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

  1. 某些组件带有子组件。如antd中的 Select.Option
  2. 某些组件带有方法,如antd中的Form.create

这里先解释下原因:

在上面的示意代码中,我们也可以看到 loadable 本身是一个Component,它内部会处理Promise得到真正的组件。在Promise解析完成之前,它没办法知道此组件上有哪些静态属性和方法。

因此如果使用这种方法包裹Select组件

1
2
import loadable from '@loadable/component'
const Select = loadable(async () => import('antd/lib/select'))

那么这个Select组件上就必定会丢失掉 Option这个静态属性,Form.create方法同理。

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

  1. 如果丢失的属性还是React组件,则可以使用 loadable 二次处理下子属性。
  2. 如果丢失的静态方法是异步的,也可以使用 loadable 二次处理下。
  3. 如果丢失的静态方法是同步的,只能放弃dynamic import了。

二次处理子组件

以Select举例,在改成dynamic import之前,它一般是这样使用的:

1
2
3
4
5
6
7
8
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的话,需要改成这样:

1
2
3
4
5
6
7
8
9
10
11
12
// 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,原来的用法如下:

1
2
3
4
5
6
import Foo from 'foo'

ReactDOM.render(
  <Foo onClick={() => Foo.bar() } />
  mountNode,
);

上面的例子中,假设Foo.bar()是异步的,那么改成Promise也就不会影响功能,所以还可以继续使用loadable,改造后的代码如下:

1
2
3
4
5
6
7
8
9
// 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,在改造工程时,需要小心如下的问题。

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

结束语

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

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

关注我:
分享文章:

0条评论