JavaScript 模块动态导入及适用场景

1,490 阅读3分钟

我们在使用 Vue 或 React 等框架构建应用程序时,经常需要编写大量组件,我们并没有将这些组件都写到一个文件中,而是将组件分开,放在各自文件中,这样便是为每个组件创建一个模块。

以 React 为例,通常在某个模块中使用其他模块时,是通过这样的方式来导入并使用的。

import React from 'react';
import Count1 from './Count1.jsx';
import Count2 from './Count2.jsx';

const App = () => {
  const shouldShowCountIndex = 1;

  return (
    <div>
      {shouldShowCountIndex === 1 ? <Count1 /> : <Count2 />}
    </div>
  )
};

什么是动态导入

在文件顶部导入所有模块时,所有模块都会在文件的其余部分之前加载。在某些情况下,我们只需要根据某个条件导入一个模块。通过动态导入,我们可以按需导入模块。

import React, { useState, useEffect } from 'react';

const App = () => {
  const [CountComponent, setCountComponent] = useState(null);

  useEffect(() => {
    const loadDynamicComponent = async () => {
      const module = await import('./Count1.jsx');

      setCountComponent(module.default);
    }

    loadDynamicComponent();
  }, []);


  return (
    <div>
      {CountComponent ? <CountComponent /> : null}
    </div>
  )
};

通过动态导入,我们可以减少页面加载时间。只需要在用户需要的时候,加载真正需要的代码。

另外, import() 还可以使用模版字符串,以此来实现根据动态变量值来动态加载模块。

const module = await import(`./Count${count}.jsx`);

下面我们来看两个场景,是否适合。

场景分析

  1. 场景一:根据不同的变量值使用不同的图片

    import React from 'react';
    import { fetchImageIndex } from '../api';
    import Image1 from '../assets/image1.png';
    import Image2 from '../assets/image2.png';
    import Image3 from '../assets/image3.png';
    import Image4 from '../assets/image4.png';
    
    const App = () => {
      const [imageIndex, setImageIndex] = useState(1);
    
      useEffect(() => {
        const getImageIndex = async () => {
          const imageIndex = await fetchImageIndex();
    
          setImage(imageIndex);
        }
    
        getImageIndex();
      }, [count]);
    
      const image = imageIndex === 1 ? Image1 : imageIndex === 2 ? Image2 : imageIndex === 3 ? Image3 : Image4;
    
      return (
        <div>
          // 使用图片资源 image
        </div>
      )
    };
    

    换成动态导入,我们可以这样写

    import React, { useState, useEffect } from 'react';
    import { fetchImageIndex } from '../api';
    
    const App = () => {
      const [image, setImage] = useState(null);
    
      useEffect(() => {
        const loadImage = async () => {
          const index = await fetchImageIndex();
          const resource = await import(`../assets/image${index}.png`);
    
          setImage(resource);
        }
    
        loadImage();
      }, []);
    
      return (
        <div>
          // 使用图片资源 image
        </div>
      )
    };
    }
    

    这样,我们不依赖硬编码的模块路径,如果有 10+ 张的图片,也不会增加页面的加载时间,同时如果新增图片,也不用修改代码。

    同样地,也适合动态导入有限个组件的场景。

  2. 场景二:动态使用组件库所有 Icon 组件

    有一个场景,是根据服务端返回的 icon key ,动态加载对应的 Icon 组件。但是组件库中的 Icon 组件包含 1000+ 个,如果将所有的 Icon 组件都加载进来,然后通过 key 来匹配,那么页面的加载时间就会很长。那么是否可以通过动态导入来解决呢?

    很遗憾,如果我们像上面那样,通过动态导入来解决,所有的 Icon 组件都会打入到 JS 文件中,尽管可以通过配置打包策略来分割 JS 文件,但会增加额外的工程复杂度和文件数量开销。

    所以这里我们可以采用相对”传统“的字体图标的方式来实现动态图标的需求。通过将 Icon 打包成字体文件,可以实现更好的压缩来减小字体体积。另外,使用方式也相对方便。

    <i class="icon icon-add" />
    

    一个 1600 个图标的字体文件测试:

    • icons.woff2:120.59 KB
    • icons-font.css:89 KB (minified)

结论

当你遇到性能问题时再去考虑使用动态导入,尤其是某些模块的加载不是页面呈现的关键点时。

当使用动态导入时,需要考虑其额外带来的工程成本和体积成本。有时候,可以通过其他方案来替代。例如动态 Icon 场景。

你有其他的见解吗?

博客原文