前端模块化机制概述

47 阅读5分钟

前端模块化机制

上古时代的宝贝

AMD (Async Module Definition)

AMD是浏览器端的模块化标准,它采用异步的方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在回调函数中,等到加载完成之后,这个回调函数才会运行。

CMD (Common Module Definition)

CMD是浏览器端的模块化标准,是在模块内部需要使用某个依赖时才去加载它。

UMD(Universal Module Definition)

UMD 是即通用模块定义,模块的代码结构通常会包含检测当前环境是浏览器环境还是Node环境的逻辑,然后根据不同的环境来定义模块的加载和导出方式。

AMD、CMD 和 UMD 已经成为较旧的模块化方案。在新项目中直接使用最新的 ESM 方案来实现模块化

现代模块化标准

CJS (CommonJS)

CommonJS 是Node端的模块化标准,一个文件就是一个模块,每个模块都有自己独立的作用域,模块内部定义的变量和函数不会自动暴露到全局环境中

  1. 模块加载机制
    • 同步加载:CommonJS 的模块加载是同步的。当执行到 require 函数时,会阻塞当前代码的执行,直到被加载的模块的代码全部执行完毕并返回结果。
    • 模块缓存:CommonJS 规范有一个模块缓存机制。当一个模块第一次被加载后,其结果会被缓存起来。如果后续再次使用 require 函数加载同一个模块,将直接返回缓存中的结果,而不会重新执行模块代码
  2. 模块的基本组成和使用方式
    • 模块定义

      • 在 CommonJS(CJS)中,每个文件都被看作是一个独立的模块。模块内部有自己的作用域,变量和函数不会自动暴露给外部。例如,一个简单的模块可以这样定义:
      // mathUtils.js
      function add(a, b) {
          return a + b;
      }
      function subtract(a, b) {
          return a - b;
      }
      module.exports = {
          add: add,
          subtract: subtract
      };
      
      • 这里定义了两个函数 add 和 subtract,并通过 module.exports 将它们封装成一个对象,这个对象就是该模块向外暴露的接口。
  • 模块引用

    • 在需要使用这些模块功能的其他文件中,可以通过 require 函数来引入模块。例如:
    // main.js
    const mathUtils = require('./mathUtils');
    console.log(mathUtils.add(5, 3)); 
    console.log(mathUtils.subtract(7, 2));
    
    • 首先使用 require('./mathUtils') 来加载 mathUtils.js 模块,require 函数返回的是 mathUtils.js 中通过 module.exports 暴露的对象。然后就可以通过这个对象访问 mathUtils 模块中的函数。

ESM (ES Module)

ES Module(ECMAScript Module)是 JavaScript 官方的标准化模块系统。它被引入到 ECMAScript 标准中,目的是为了在 JavaScript 语言层面提供一种统一、简洁且高效的模块管理方式,用于替代之前社区中各种非标准的模块规范(如 CommonJS、AMD 等),使得 JavaScript 在模块管理上更加规范和一致。

  1. 模块加载机制
    • 浏览器端加载:ES Module 在浏览器中的加载是异步的,这意味着它不会阻塞浏览器的主线程。当浏览器请求模块文件时,其他脚本和页面的渲染等操作可以继续进行。例如,一个包含多个 ES Module 的复杂页面,各个模块可以在后台异步加载。浏览器会对加载过的模块进行缓存。如果同一个模块被多次引用,后续的引用将直接使用缓存中的模块,而不会再次发起请求。
      • 在浏览器环境中,当遇到一个 ES Module 脚本标签(<script type="module">)时,浏览器会发起对该模块文件的请求。例如,对于 <script type="module" src="main.js"></script>,浏览器会先请求 main.js 文件
    • Node端加载: ES Module 在Node中的加载是同步的,Node.js 会按照模块依赖图的顺序加载模块,并且也有模块缓存机制。当一个模块被加载后,再次引用相同模块时会直接从缓存中获取。在加载过程中,有一些操作是异步的,比如读取文件内容(因为涉及到 I/O 操作)。但是Node.js 会处理这些异步操作,并且在模块的内容完全加载和解析完成之前,不会执行使用该模块的代码
      • 在 Node.js 中,要使用 ES Module,文件扩展名必须明确指定(通常是 .mjs),或者在 package.json 文件中设置 "type": "module"。这是为了与 CommonJS 模块(默认是 .js 文件)进行区分。例如,一个名为 app.mjs 的文件可以使用 ES Module 语法。
  2. 模块的基本组成和使用方式
    • 模块定义
      • 在一个 ES Module 文件中,可以使用 export 关键字来导出变量、函数、类等。例如

        // mathUtils.js
        export function add(a, b) {
            return a + b;
        }
        export function subtract(a, b) {
            return a - b;
        }
        
      • 或者也可以使用命名空间导出的方式,将多个需要导出的内容放在一个对象中进行导出,如下:

        // anotherMathUtils.js
        const multiply = (a, b) => a * b;
        const divide = (a, b) => a / b;
        export {
            multiply,
            divide
        };
        
    • 模块的导入(import)
      • 在需要使用这些模块的文件中,可以使用 import 语句来导入模块。例如,要使用上面定义的 mathUtils.js 模块,可以这样做:

        // main.js
        import {add, subtract} from './mathUtils.js';
        console.log(add(5, 3)); 
        console.log(subtract(7, 2));
        
      • 如果要使用命名空间导出的模块(如 anotherMathUtils.js),可以这样导入:

        // anotherMain.js
        import {multiply, divide} from './anotherMathUtils.js';
        console.log(multiply(4, 2)); 
        console.log(divide(10, 2));
        
      • 还可以使用别名来导入模块内容,这在避免命名冲突或者简化名称时很有用。例如:

        // aliasMain.js
        import {add as sum, subtract as diff} from './mathUtils.js';
        console.log(sum(3, 3)); 
        console.log(diff(8, 1));`
            ```