MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

JavaScript模块化发展历程与ES Modules

2021-01-105.1k 阅读

JavaScript模块化发展历程

早期的全局函数与对象模式

在JavaScript发展的早期阶段,并没有原生的模块化系统。开发者们主要通过全局函数和对象来组织代码。例如,假设有一个简单的项目,包含一个用于计算的功能和一个用于显示结果的功能。

// 全局函数定义
function calculate(a, b) {
    return a + b;
}

function displayResult(result) {
    console.log('The result is: ', result);
}

// 使用函数
let result = calculate(2, 3);
displayResult(result);

这种方式存在明显的问题。当项目规模逐渐增大,不同模块之间的变量和函数容易产生命名冲突。比如,如果另一个开发者在同一个项目中也定义了一个名为calculate的函数,就会导致错误。而且,代码的可维护性和可复用性也较差,因为所有的代码都暴露在全局作用域中。

立即执行函数表达式(IIFE)模式

为了解决全局作用域污染和命名冲突的问题,立即执行函数表达式(IIFE)模式应运而生。IIFE 可以创建一个独立的作用域,避免变量和函数泄漏到全局作用域。

// 使用IIFE创建模块
let mathModule = (function () {
    function add(a, b) {
        return a + b;
    }

    function subtract(a, b) {
        return a - b;
    }

    return {
        add: add,
        subtract: subtract
    };
})();

// 使用模块
let sum = mathModule.add(5, 3);
let diff = mathModule.subtract(5, 3);
console.log('Sum: ', sum);
console.log('Difference: ', diff);

在上述代码中,mathModule通过IIFE创建了一个独立的作用域,addsubtract函数被封装在这个作用域内,只有通过返回的对象才能访问到这些函数,有效地避免了全局作用域的污染。

然而,IIFE模式也有一些局限性。例如,多个IIFE之间难以管理依赖关系。如果一个模块依赖于另一个模块,开发者需要手动确保依赖模块在使用前已经被加载和执行。

CommonJS规范

随着Node.js的兴起,JavaScript开始在服务器端广泛应用。为了满足服务器端开发对模块化的需求,CommonJS规范诞生了。CommonJS采用了exportsmodule.exports来导出模块的接口,使用require函数来引入其他模块。

// math.js - 定义一个CommonJS模块
function add(a, b) {
    return a + b;
}

function subtract(a, b) {
    return a - b;
}

exports.add = add;
exports.subtract = subtract;
// 或者使用module.exports
// module.exports = {
//     add: add,
//     subtract: subtract
// };
// main.js - 使用CommonJS模块
let math = require('./math.js');

let sum = math.add(10, 5);
let diff = math.subtract(10, 5);
console.log('Sum: ', sum);
console.log('Difference: ', diff);

CommonJS规范的优点是简单易懂,适合服务器端的同步加载场景。因为在服务器端,文件系统的读取是相对快速且同步的。但是,在浏览器环境中,由于网络加载的异步性,CommonJS规范并不适用。如果在浏览器中使用CommonJS,会导致页面阻塞,直到所有模块都被加载完成,这严重影响用户体验。

AMD(Asynchronous Module Definition)规范

为了在浏览器环境中实现模块化开发,AMD规范诞生了。AMD规范主要用于异步加载模块,其核心是define函数和require函数。define函数用于定义模块,require函数用于异步加载模块。

<!DOCTYPE html>
<html>

<head>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.min.js"></script>
</head>

<body>
    <script>
        // 定义一个AMD模块
        define('math', function () {
            function add(a, b) {
                return a + b;
            }

            function subtract(a, b) {
                return a - b;
            }

            return {
                add: add,
                subtract: subtract
            };
        });

        // 使用AMD模块
        require(['math'], function (math) {
            let sum = math.add(20, 10);
            let diff = math.subtract(20, 10);
            console.log('Sum: ', sum);
            console.log('Difference: ', diff);
        });
    </script>
</body>

</html>

在上述代码中,通过define函数定义了math模块,然后使用require函数异步加载并使用该模块。AMD规范使得浏览器端的模块化开发更加高效,避免了页面阻塞。然而,AMD规范的语法相对复杂,并且在实际使用中,模块的定义和加载逻辑可能会变得比较繁琐。

CMD(Common Module Definition)规范

CMD规范是Sea.js在推广过程中对模块定义的规范化产出。它与AMD规范有一些相似之处,但在模块定义和依赖处理上有自己的特点。CMD规范使用define函数来定义模块,并且对依赖的处理更加灵活。

<!DOCTYPE html>
<html>

<head>
    <script src="https://cdn.jsdelivr.net/npm/seajs@3.0.3/dist/sea.js"></script>
</head>

<body>
    <script>
        // 定义一个CMD模块
        define(function (require, exports, module) {
            function add(a, b) {
                return a + b;
            }

            function subtract(a, b) {
                return a - b;
            }

            exports.add = add;
            exports.subtract = subtract;
        });

        // 使用CMD模块
        seajs.use(['./math.js'], function (math) {
            let sum = math.add(30, 20);
            let diff = math.subtract(30, 20);
            console.log('Sum: ', sum);
            console.log('Difference: ', diff);
        });
    </script>
</body>

</html>

在CMD规范中,模块的依赖可以在使用时才声明,而不是像AMD那样在定义模块时就声明所有依赖。这种方式使得模块的定义更加灵活,在一定程度上减少了不必要的依赖加载。但是,CMD规范并没有像AMD那样得到广泛的应用,其生态系统相对较小。

ES Modules

ES Modules 简介

随着JavaScript的发展,ES6(ECMAScript 2015)引入了原生的模块化系统,即ES Modules。ES Modules 为JavaScript提供了标准化的模块解决方案,既适用于浏览器环境,也适用于Node.js环境(Node.js从v8.5.0版本开始部分支持ES Modules,从v13.2.0版本开始全面支持)。

ES Modules 使用importexport关键字来实现模块的导入和导出。这使得代码的模块化结构更加清晰和直观。

ES Modules 的导出方式

  1. 命名导出
    • 定义:命名导出允许在模块中导出多个命名的变量、函数或类。在导入时,需要使用相同的名称。
    • 示例
// math.js
export function add(a, b) {
    return a + b;
}

export function subtract(a, b) {
    return a - b;
}
// main.js
import { add, subtract } from './math.js';

let sum = add(10, 5);
let diff = subtract(10, 5);
console.log('Sum: ', sum);
console.log('Difference: ', diff);
  1. 默认导出
    • 定义:每个模块只能有一个默认导出。默认导出不需要使用特定的名称,在导入时可以自定义名称。
    • 示例
// greeting.js
const greeting = 'Hello, world!';
export default greeting;
// main.js
import customGreeting from './greeting.js';
console.log(customGreeting);
  1. 混合导出
    • 定义:模块可以同时使用命名导出和默认导出。
    • 示例
// utils.js
export function multiply(a, b) {
    return a * b;
}

const message = 'This is a utility module';
export default message;
// main.js
import defaultMessage, { multiply } from './utils.js';
console.log(defaultMessage);
let product = multiply(3, 4);
console.log('Product: ', product);

ES Modules 的导入方式

  1. 导入单个命名导出
    • 语法import { exportName } from './module.js';
    • 示例:从math.js模块中导入add函数。
// math.js
export function add(a, b) {
    return a + b;
}
// main.js
import { add } from './math.js';
let result = add(2, 3);
console.log('Result: ', result);
  1. 导入多个命名导出
    • 语法import { exportName1, exportName2 } from './module.js';
    • 示例:从math.js模块中导入addsubtract函数。
// math.js
export function add(a, b) {
    return a + b;
}

export function subtract(a, b) {
    return a - b;
}
// main.js
import { add, subtract } from './math.js';
let sum = add(5, 3);
let diff = subtract(5, 3);
console.log('Sum: ', sum);
console.log('Difference: ', diff);
  1. 导入默认导出
    • 语法import customName from './module.js';
    • 示例:从greeting.js模块中导入默认导出。
// greeting.js
const greeting = 'Hello, there!';
export default greeting;
// main.js
import myGreeting from './greeting.js';
console.log(myGreeting);
  1. 导入所有导出
    • 语法import * as moduleAlias from './module.js';
    • 示例:从math.js模块中导入所有导出,并通过别名访问。
// math.js
export function add(a, b) {
    return a + b;
}

export function subtract(a, b) {
    return a - b;
}
// main.js
import * as math from './math.js';
let sum = math.add(7, 2);
let diff = math.subtract(7, 2);
console.log('Sum: ', sum);
console.log('Difference: ', diff);

ES Modules 在浏览器中的使用

在浏览器中使用ES Modules非常简单。只需要在<script>标签中设置type="module"

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF - 8">
    <title>ES Modules in Browser</title>
</head>

<body>
    <script type="module">
        import { add, subtract } from './math.js';

        let sum = add(15, 10);
        let diff = subtract(15, 10);
        console.log('Sum: ', sum);
        console.log('Difference: ', diff);
    </script>
</body>

</html>
// math.js
export function add(a, b) {
    return a + b;
}

export function subtract(a, b) {
    return a - b;
}

需要注意的是,浏览器加载ES Modules是异步的,不会阻塞页面的渲染。这与传统的<script>标签加载方式不同。如果需要同步加载,可以使用import()动态导入,它返回一个Promise。

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF - 8">
    <title>ES Modules in Browser</title>
</head>

<body>
    <script type="module">
        async function loadMathModule() {
            let { add, subtract } = await import('./math.js');
            let sum = add(20, 15);
            let diff = subtract(20, 15);
            console.log('Sum: ', sum);
            console.log('Difference: ', diff);
        }

        loadMathModule();
    </script>
</body>

</html>

ES Modules 在Node.js中的使用

在Node.js中使用ES Modules,需要将文件的扩展名改为.mjs,或者在package.json文件中设置"type": "module",这样就可以使用.js文件作为ES Modules。

// math.mjs
export function add(a, b) {
    return a + b;
}

export function subtract(a, b) {
    return a - b;
}
// main.mjs
import { add, subtract } from './math.mjs';

let sum = add(25, 20);
let diff = subtract(25, 20);
console.log('Sum: ', sum);
console.log('Difference: ', diff);

在Node.js中,ES Modules与CommonJS模块的交互需要注意一些细节。例如,CommonJS模块不能直接导入ES Modules,但ES Modules可以通过import * as cjsModule from './cjsModule.cjs';的方式导入CommonJS模块。

ES Modules 的优势

  1. 标准化:ES Modules为JavaScript提供了官方的、标准化的模块化解决方案,结束了多种非官方规范并存的局面,使得代码的模块化开发更加统一和规范。
  2. 简洁的语法importexport关键字使得模块的导入和导出操作更加简洁明了,代码的可读性大大提高。
  3. 静态分析:ES Modules支持静态分析,这意味着在编译阶段就能确定模块的依赖关系,有助于优化打包和压缩工具的性能,例如Tree - shaking技术可以通过静态分析去除未使用的代码,减小打包后的文件体积。
  4. 浏览器和服务器端通用:ES Modules既可以在浏览器环境中使用,也可以在Node.js环境中使用,为全栈JavaScript开发提供了一致的模块化体验。

ES Modules 的局限性

  1. 兼容性:虽然现代浏览器和较新版本的Node.js都支持ES Modules,但在一些旧版本的浏览器和Node.js环境中,可能需要使用转译工具(如Babel)来确保兼容性。
  2. CommonJS互操作性:与CommonJS模块的交互存在一定的复杂性,尤其是在混合使用不同类型模块的项目中,需要开发者谨慎处理依赖关系和导入导出方式。

综上所述,ES Modules的出现为JavaScript模块化开发带来了革命性的变化,成为了现代JavaScript项目中不可或缺的一部分。尽管存在一些局限性,但随着技术的不断发展和普及,ES Modules将在未来的JavaScript开发中发挥更加重要的作用。无论是小型项目还是大型企业级应用,ES Modules都能提供高效、清晰的模块化解决方案。