Skip to content

一文搞懂js模块化:ES6与异步加载、动态导入

1.CommonJS和ES6 Module的区别详解

CommonJS 和 ES6 Module(简称为 Module)是 JavaScript 中用于模块化编程的两种不同的模块系统,它们有一些区别和特点:

CommonJS:

  1. 同步加载: CommonJS 模块是同步加载的,意味着模块代码会在模块被引入时立即执行,阻塞代码的执行,直到模块加载完成。
  2. 适用于服务器端: CommonJS 最初是为服务器端开发设计的,它非常适合在 Node.js 中使用,因为在服务器端,同步加载不会对性能产生太大影响。
  3. exports 和 require: 在 CommonJS 中,使用 exports 对象暴露模块的内容,通过 require 函数引入其他模块的内容。
  4. 适用于小规模应用: 由于同步加载的特性,CommonJS 在大规模的浏览器应用中可能会造成阻塞,不适合浏览器端的模块化开发。

ES6 Module:

  1. 异步加载: ES6 Module 支持异步加载,模块代码在需要时才会执行,不会阻塞代码的执行。
  2. 适用于浏览器和服务器: ES6 Module 被设计为在浏览器和服务器中都能使用,因此适合前端和后端开发。
  3. export 和 import: 在 ES6 Module 中,使用 export 关键字导出模块的内容,通过 import 关键字引入其他模块的内容。
  4. 静态解析: ES6 Module 的导入和导出是静态的,这意味着在代码分析阶段就能够确定模块的依赖关系,这有助于工具进行代码优化和打包。

总结来说,CommonJS 和 ES6 Module 都是模块化开发的方式,但适用于不同的场景。CommonJS 适合服务器端开发,而 ES6 Module 则适用于浏览器端和服务器端,尤其在大规模应用中更具优势,因为它支持异步加载和静态解析。在现代的 JavaScript 开发中,通常优先考虑使用 ES6 Module。

怎么在浏览器端使用commonjs呢?

在浏览器端使用 CommonJS 的方式有一些限制,因为浏览器默认不支持同步加载 CommonJS 模块。然而,你可以通过使用一些工具和技术来在浏览器中使用 CommonJS。

  1. Browserify: Browserify 是一个流行的工具,可以将 CommonJS 模块转换为浏览器可识别的代码。你可以使用类似 CommonJS 的语法在浏览器端进行模块化开发,并使用 Browserify 将模块打包成一个浏览器可用的文件。
  2. Webpack: **Webpack 是另一个流行的构建工具,支持将 CommonJS 模块转换为浏览器能够识别的模块代码。**类似 Browserify,Webpack 也可以打包多个模块成一个文件。
  3. RequireJS: RequireJS 是一个 AMD (Asynchronous Module Definition) 规范的实现,虽然不是完全的 CommonJS,但它也支持在浏览器中进行模块化开发。
  4. ES6 Module 转换: 如果你想在浏览器中使用类似 CommonJS 的模块语法,你可以使用 Babel 等工具将代码转换为 ES6 Module 语法,然后使用现代浏览器的原生模块加载功能。

尽管可以通过这些工具在浏览器中使用 CommonJS,但现代浏览器通常支持原生的 ES6 Module,而且 ES6 Module 在性能和开发体验方面都有优势。因此,在浏览器端,推荐使用 ES6 Module 进行模块化开发。如果你需要兼容旧浏览器,可以考虑使用工具来进行转换和打包。

2.ES6 Module的异步加载和动态导入

注意:ES6的异步加载和动态导入不是一个概念,异步加载是默认的,并且指的是js——>html,而动态导入是我们手动调用函数的,并且是js——>js的!

所以不要把模块的异步加载和动态导入混为一谈,虽然动态导入默认也是异步的,但是这些概念之间还是区别相对比较大的!

1.异步加载

ES6 Module 支持异步加载,这意味着模块代码在需要时才会执行,不会阻塞代码的执行。这种异步加载的特性在现代的前端开发中非常有用,特别是在大规模应用中,可以优化页面加载和性能。

浏览器对于带有type="module"<script>,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了<script>标签的defer属性。

html
<script type="module" src="./foo.js"></script>
<!-- 等同于 -->
<script type="module" src="./foo.js" defer></script>

如果网页有多个<script type="module">,它们会按照在页面出现的顺序依次执行。

<script>标签的async属性也可以打开,这时只要加载完成,渲染引擎就会中断渲染立即执行。执行完成后,再恢复渲染。

html
<script type="module" src="./foo.js" async></script>

一旦使用了async属性,<script type="module">就不会按照在页面出现的顺序执行,而是只要该模块加载完成,就执行该模块。

ES6 模块也允许内嵌在网页中,语法行为与加载外部脚本完全一致。

html
<script type="module">
  import utils from "./utils.js";

  // other code
</script>

2.动态导入(异步导入)

异步模块是指在应用程序运行过程中,不需要在初始加载时立即获取的模块或代码片段。这些模块可以在需要时动态加载,以减少初始加载时间,提高应用程序的性能和效率。在前端开发中,异步模块通常用于按需加载应用程序的特定部分,而不是一次性加载所有代码。这种加载方式可以显著减少初始加载时间,尤其对于大型或复杂的应用程序来说尤为重要。

异步模块其实说白了就是通过动态导入引入的模块!

动态导入(Dynamic Import)是 ES6 Module 提供的一种特性,允许在代码运行时异步加载模块它可以用于按需加载模块(可以根据条件导入、根据事件的触发导入),优化应用的性能和资源加载。动态导入是通过 import() 函数来实现的,该函数返回一个 Promise,当模块加载完成后,Promise 将会被解析。

以下是动态导入的基本用法示例:

js
import('./myModule.js')
  .then(module => {
    // 在模块加载完成后执行的代码
    module.myFunction();
  })
  .catch(error => {
    // 处理加载错误
    console.error(error);
  });

动态导入的特点和优势包括:

  1. 按需加载: 可以根据需要在运行时加载模块,减少初始加载时间。
  2. 异步执行: 动态导入是异步的,不会阻塞主线程,可以提高应用的交互性能。
  3. 代码拆分: 可以将应用拆分成更小的模块,优化资源加载,减少首次加载的大小(因为新的模块里面,可能有更多的npm包的依赖,这些代码加载是很耗费资源的)——> 这是主要的原因,因为分模块了,有很多当前用不到的npm就没必要在首次进入的时候加载,能让用户尽快看到当前的页面!而且动态导入本身就是异步的,对于加载的体验感更好!
  4. 条件加载: 可以根据条件来决定是否加载特定的模块。
  5. 并行加载: 多个模块可以并行加载,提高加载效率。

需要注意的是,动态导入的语法和特性基本在所有浏览器中都得到了支持,但在一些旧版本的浏览器中可能需要使用转换工具(如 Babel)来进行降级处理。

在实际开发中,你可以根据需要在特定的地方使用动态导入,以实现按需加载和优化应用性能。

3.在Vue2中使用异步动态导入

在 Vue 中使用动态导入 ES6 模块(也称为动态 import 或 ES6 动态导入)可以帮助你实现按需加载模块,减小初始加载体积,提升应用性能。以下是一个示例,展示了在 Vue 组件中如何使用动态导入:

假设你有一个异步加载的模块 MyAsyncModule.js,它导出一个组件:

js
// MyAsyncModule.js
export default {
  name: 'MyAsyncModule',
  data() {
    return {
      message: 'Hello from async module!',
    };
  },
};

然后,在你的 Vue 组件中,你可以使用动态导入来异步加载这个模块:

html
<template>
  <div>
    <button @click="loadAsyncModule">Load Async Module</button>
    <div v-if="showAsyncModule">
      <AsyncModule />
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      showAsyncModule: false,
    };
  },
  methods: {
    //只有在需要用到AsyncModule组件的时候才加载:
    async loadAsyncModule() {
      // 使用动态导入来异步加载模块
      const asyncModule = await import('./MyAsyncModule');

      // 注册异步加载的模块为局部组件
      this.$options.components.AsyncModule = asyncModule.default;

      // 更新状态以显示异步模块
      this.showAsyncModule = true;
    },
  },
};
</script>

在上面的示例中,import('./MyAsyncModule') 是动态导入的方式,返回一个 Promise,在加载完成后,我们通过 asyncModule.default 来获取导入的模块。然后,我们通过将异步模块注册为局部组件,可以在模板中直接使用 <AsyncModule /> 来渲染异步组件。

使用动态导入可以帮助你在需要的时候再加载模块,提升了应用的加载速度和性能。

this.$options.components.AsyncModule = asyncModule.default;这句话是什么原理?

this.$options.components 是一个对象,用于存储当前组件的局部注册组件

在上述代码中,我们通过 this.$options.components.AsyncModule 将异步加载的模块设置为当前组件的一个局部注册组件。

这个做法的原理是,在 Vue 组件的生命周期中,Vue 会遍历 this.$options.components 对象,将其中的组件注册为当前组件的局部组件。通过这种方式,我们可以在模板中直接使用局部组件的标签名来渲染组件。

在使用 ES6 模块语法(例如 importexport)时,如果一个模块导出的是一个默认导出(通过 export default),那么在导入这个模块时可以使用 .default 来获取默认导出的内容。

4.在Vue3中使用异步动态导入

在 Vue 3 的 Composition API 中,你可以通过使用 defineAsyncComponent 函数来实现类似的效果。以下是一个示例:

注意:这里我们是直接初始化页面的时候就加载组件了(而不是需要用到的时候再加载),因为这是异步导入的,这里的加载也不会影响其他组件的渲染,不会影响性能,因为异步加载的组件位于if语句里面,我们也不着急使用这个组件,所以让它异步慢慢加载就可以了!

html
<template>
  <div>
    <button @click="loadAsyncModule">Load Async Module</button>
    <div v-if="showAsyncModule">
      <AsyncModule />
    </div>
  </div>
</template>

<script>
import { defineAsyncComponent, ref } from 'vue';

export default {
  components: {
    AsyncModule: defineAsyncComponent(() => import('./AsyncModule')),
  },
  setup() {
    const showAsyncModule = ref(false);

    const loadAsyncModule = () => {
      showAsyncModule.value = true;
    };

    return {
      showAsyncModule,
      loadAsyncModule,
    };
  },
};
</script>

在上面的示例中,我们使用 defineAsyncComponent 函数来定义异步加载的组件。这个函数接受一个返回 import() 的回调函数作为参数,并将其返回值作为异步组件的定义。在 setup 函数中,我们定义了 showAsyncModuleloadAsyncModule,然后将 showAsyncModule 作为条件来决定是否渲染异步组件。

如果是使用setup语法糖:

html
<template>
  <div>
    <button @click="loadAsyncModule">Load Async Module</button>
    <div v-if="showAsyncModule">
      <AsyncModule />
    </div>
  </div>
</template>

<script setup>
import { ref, defineAsyncComponent } from 'vue';

const showAsyncModule = ref(false);

const loadAsyncModule = () => {
  showAsyncModule.value = true;
};

const AsyncModule = defineAsyncComponent(() => import('./AsyncModule')); //直接加载
</script>

5.webpack中异步加载和ES6的import动态导入的关系

1.两者的关系

import() 函数是 ECMAScript 6 (ES6) 的功能,也称为动态导入 (dynamic import)。它允许在运行时动态加载模块,以替代静态导入(import 关键字)。

然而,需要注意的是,虽然 import() 是 ES6 的功能,但它在实际应用中常常与模块打包工具一起使用,比如 webpack。webpack 是一个流行的模块打包工具,可以将多个模块打包成一个或多个文件,以优化网页加载性能并管理模块之间的依赖关系。

在webpack中,你可以使用 import() 函数来实现按需加载模块,这在异步加载模块时非常有用。webpack 会将 import() 转换为代码分割(code splitting)到单独的文件 以及 异步加载模块的逻辑,以确保在需要时加载相应的模块,而不会一次性加载所有模块

也就是说webpack实际上对于异步导入是有专门的处理的!

2.实际的应用

webpack里面有一个配置项专门用来声明异步模块的:

output.chunkLoading:声明加载异步模块的技术方案,支持 false/jsonp/require 等方式。

什么时候要用到import?

比如要做一个大型前端项目优化和重构,在初步梳理渲染链路的过程中,发现因为历史包袱产生了非常多了冗余依赖,这些依赖在首页很大程度上用不到,或者有大量的内容在首页的init状态下并不需要被加载进来。在这种情况下,进行合理的chunk拆分,配合良好的异步加载逻辑,就可以在减小资源体积这个层面上做出性能优化。

异步的模块则是在代码运行时动态加载进来的。最常见的就是使用import关键字导入的模块。

**当webpack打包到import关键字依赖的时候,就会将该依赖自动打包到一个单独的文件而不是原本所在的被引入文件产物中。**这样的结果就是引用了这个模块的原模块文件大小会减小。自然地,如果可以活用移步模块,就可以在文件内容的颗粒度上对最终的产物进行控制,减少依赖冗余。