文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

前端模块化的前世今生

2024-12-01 15:33

关注

大家好,我是 CUGGZ。

随着前端项目越来越庞大,代码复杂性不断增加,对于模块化的需求越来越大。模块化是工程化基础,只有将代码模块化,拆分为合理单元,才具备调度整合的能力。下面就来看看模块化的概念,以及不同模块化方案的使用方式和优缺点。

一、模块概述

1、概念

由于代码之间会发生大量交互,如果结构不合理,这些代码就会变得难以维护、难以测试、难以调试。而使用模块化就解决了这些问题,模块化的特点如下:

模块化是一种将系统分离成独立功能部分的方法,可以将系统分割成独立的功能部分,严格定义模块接口,模块间具有透明性。通过将代码进行模块化分隔,每个文件彼此独立,开发者更容易开发和维护代码,模块之间又能够互相调用和通信,这就是现代化开发的基本模式。

2、模式

JavaScript 模块包含三个部分:

3、类型

模块化的贯彻执行离不开相应的约定,即规范。这是能够进行模块化工作的重中之重。实现模块化的规范有很多,比如:AMD、RequireJS、CMD、SeaJS、UMD、CommonJS、ES6 Module。除此之外,IIFE(立即执行函数)也是实现模块化的一种方案。

本文将介绍其中的六个:

二、IIFE

在 ECMAScript 6 之前,模块并没有被内置到 JavaScript 中,因为 JavaScript 最初是为小型浏览器脚本设计的。这种模块化的缺乏,导致在代码的不同部分使用了共享全局变量。

比如,对于以下代码:

var name = 'JavaScript';
var age = 20;

当上面的代码运行时,name​ 和 age​ 变量会被添加到全局对象中。因此,应用中的所有 JavaScript 脚本都可以访问全局变量 name​ 和 age,这就很容易导致代码错误,因为在其他不相关的单元中也可以访问和修改这些全局变量。除此之外,向全局对象添加变量会使全局命名空间变得混乱并增加了命名冲突的机会。

所以,我们就需要一种封装变量和函数的方法,并且只对外公开定义的接口。因此,为了实现模块化并避免使用全局变量,可以使用如下方式来创建模块:

(function () {
// 声明私有变量和函数
return {
// 声明公共变量和函数
}
})();

上面的代码就是一个返回对象的闭包,这就是我们常说的IIFE(Immediately Invoked Function Expression),即立即调用函数表达式。在该函数中,就创建了一个局部范围。这样就避免了使用全局变量(IIFE 是匿名函数),并且代码单元被封装和隔离。

可以这样来使用 IIFE 作为一个模块:

var module = (function(){
var age = 20;
var name = 'JavaScript'
var fn1 = function(){
console.log(name, age)
};
var fn2 = function(a, b){
console.log(a + b)
};
return {
age,
fn1,
fn2,
};
})();
module.age; // 20
module.fn1(); // JavaScript 20
module.fn2(128, 64); // 192

在这段代码中,module​ 就是我们定义的一个模块,它里面定义了两个私有变量 age​ 和 name​,同时定义了两个方法 fn1​ 和 fn2​,其中 fn1​ 中使用 module​ 中定义的私有变量,fn2​ 接收外部传入参数。最后,module 向外部暴露了age、fn1、fn2。这样就形成了一个模块。

当试图在 module​ 外部直接调用fn1时,就会报错:

fn1(); // Uncaught ReferenceError: fn1 is not defined

当试图在 module​ 外部打印其内部的私有变量name​时,得到的结果是 undefined:

module.name; // undefined

上面的 IIFE 的例子是遵循模块模式的,具备其中的三部分,其中 age、name、fn1、fn2 就是模块内部的代码实现,返回的 age、fn1、fn2 就是导出的内容,即接口。调用 module 方法和变量就是导入使用。

三、CommonJS

1、概念

(1)定义

CommonJS 是社区提出的一种 JavaScript 模块化规范,它是为浏览器之外的 JavaScript 运行环境提供的模块规范,Node.js 就采用了这个规范。

注意:

  • 浏览器不支持使用 CommonJS 规范。
  • Node.js 不仅支持使用 CommonJS 来实现模块,还支持最新的  ES 模块。

CommonJS 规范加载模块是同步的,只有加载完成才能继续执行后面的操作。不过由于 Node.js 主要运行在服务端,而所需加载的模块文件一般保存在本地硬盘,所以加载比较快,而无需考虑使用异步的方式。

(2)语法

CommonJS 规范规定每个文件就是一个模块,有独立的作用域,对于其他模块不可见,这样就不会污染全局作用域。在 CommonJS 中,可以分别使用 export​ 和 require​ 来导出和导入模块。在每个模块内部,都有一个 module 对象,表示当前模块。通过它来导出 API,它有以下属性:

(3)特点

CommonJS 规范具有以下特点:

(4)优缺点

CommonJS 的优点:

CommonJS 的缺点

2、使用

在 CommonJS 中,可以通过 require 函数来导入模块,它会读取、执行 JavaScript 文件,并返回该模块的 exports 对象,该对象只有在模块脚本运行完才会生成。

(1)模块导出

可以通过以下两种方式来导出模块内容:

module.exports.TestModule = function() {
console.log('exports');
}
exports.TestModule = function() {
console.log('exports');
}

则合两种方式的导出结果是一样的,module.exports和exports的区别可以理解为:exports是module.exports的引用,如果在exports调用之前调用了exports=...,那么就无法再通过exports来导出模块内容,除非通过exports=module.exports重新设置exports的引用指向。

当然,可以先定义函数,再导出:

function testModule() {
console.log('exports');
}
module.exports = testModule;

这是仅导出一个函数的情况,使用时就是这样的:

testModule = require('./MyModule');
testModule();

如果是导出多个函数,就可以这样:

function testModule1() {
console.log('exports1');
}
function testModule2() {
console.log('exports2');
}

导入多个函数并使用:

({testModule1, testModule2} = require('./MyModule'));
testModule1();
testModule2();

(2)模块导入

可以通过以下方式来导入模块:

const module = require('./MyModule');

注意,如果 require​ 的路径没有后缀,会自动按照.js、.json和.node的顺序进行补齐查找。

(3)加载过程

在  CommonJS 中,require 的加载过程如下:

  1. 优先从缓存中加载.
  2. 如果缓存中没有,检查是否是核心模块,如果是直接加载。
  3. 如果不是核心模块,检查是否是文件模块,解析路径,根据解析出的路径定位文件,然后执行并加载。
  4. 如果以上都不是,沿当前路径向上逐级递归,直到根目录的node_modules目录。

3、示例

下面来看一个购物车的例子,主要功能是将商品添加到购物车,并计算购物车商品总价格:

// cart.js
var items = [];
function addItem (name, price)
item.push({
name: name,
price: price
});
}
exports.total = function () {
return items.reduce(function (a, b) {
return a + b.price;
}, 0);
};
exports.addItem = addItem;

这里通过两种方式在 exports 对象上定义了两个方法:addItem 和 total,分别用来添加购物车和计算总价。

下面在控制台测试一下上面定义的模块:

let cart = require('./cart');

这里使用相对路径来导入 cart 模块,打印 cart 模块,结果如下:

cart // { total: [Function], addItem: [Function: addItem] }

向购物车添加一些商品,并计算当前购物车商品的总价格:

cart.addItem('book', 60);
cart.total() // 60

cart.addItem('pen', 6);
cart.total() // 66

这就是创建模块的基本方法,我们可以创建一些方法,并且只公开希望其他文件使用的部分代码。该部分成为 API,即应用程序接口。

这里有一个问题,只有一个购物车,即只有一个模块实例。下面来在控制台执行以下代码:

second_cart = require('./cart');

那这时会创建一个新的购物车吗?事实并非如此,打印当前购物车的商品总金额,它仍然是66:

second_cart.total();  // 66

当我们㤇创建多个实例时,就需要再模块内创建一个构造函数,下面来重写 cart.js 文件:

// cart.js
function Cart () {
this.items = [];
}
Cart.prototype.addItem = function (name, price) {
this.items.push({
name: name,
price: price
});
}
Cart.prototype.total = function () {
return this.items.reduce(function(a, b) {
return a + b.price;
}, 0);
};
module.export = Cart;

现在,当需要使用此模块时,返回的是 Cart 构造函数,而不是具有 cart 函数作为一个属性的对象。下面来导入这个模块,并创建两个购物车实例:

Cart = require('./second_cart');
cart1 = new Cart();
cart2 = new Cart();
cart1.addItem('book', 50);
cart1.total(); // 50
cart2.total(); // 50

四、AMD

1、概念

CommonJS 的缺点之一是它是同步的,AMD 旨在通过规范中定义的 API 异步加载模块及其依赖项来解决这个问题。AMD 全称为 Asynchronous Module Definition,即异步模块加载机制。它规定了如何定义模块,如何对外输出,如何引入依赖。

AMD规范重要特性就是异步加载。所谓异步加载,就是指同时并发加载所依赖的模块,当所有依赖模块都加载完成之后,再执行当前模块的回调函数。这种加载方式和浏览器环境的性能需求刚好吻合。

(1)语法

AMD 规范定义了一个全局函数 define,通过它就可以定义和引用模块,它有 3 个参数:

define(id?, dependencies?, factory);

其包含三个参数:

除此之外,要想使用此模块,就需要使用规范中定义的 require 函数:

require(dependencies?, callback);

其包含两个参数:

有关 AMD API 的更详细说明,可以查看 GitHub 上的 AMD API 规范:https://github.com/amdjs/amdjs-api/blob/master/AMD.md。

(2)兼容性

该规范的浏览器兼容性如下:

(3)优缺点

AMD 的优点:

AMD 的缺点

2、使用

当然,上面只是 AMD 规范的理论,要想理解这个理论在代码中是如何工作的,就需要来看看 AMD 的实际实现。RequireJS 就是 AMD 规范的一种实现,它被描述为“JavaScript 文件和模块加载器”。下面就来看看 RequireJS 是如何使用的。

(1)引入RequireJS

可以通过 npm 来安装 RequireJS:

npm i requirejs

也可以在 html 文件引入 require.js 文件:

<script data-main="js/config" src="js/require.js">script>

这里 script标签有两个属性:

在 script 标签下添加以下代码来初始化 RequireJS:

<script>
require(['config'], function() {
//...
})
script>

当页面加载完配置文件之后, require()​ 中的代码就会运行。这个 script​ 标签是一个异步调用,这意味着当 RequireJS 通过 src="js/require.js​ 加载时,它将异步加载 data-main 属性中指定的配置文件。因此,该标签下的任何 JavaScript 代码都可以在 RequireJS 获取时执行配置文件。

那 AMD 中的 require()​ 和 CommonJS 中的 require() 有什么区别呢?

(2)定义 AMD 模块

下面是 AMD 中的一个基本模块定义:

define(['dependency1', 'dependency2'], function() {
// 模块内容
});

这个模块定义清楚地显示了其包含两个依赖项和一个函数。

下面来定义一个名为addition.js的文件,其包含一个执行加法操作的函数,但是没有依赖项:

// addition.js
define(function() {
return function(a, b) {
alert(a + b);
}
});

再来定义一个名为 calculator.js 的文件:

define(['addition'], function(addition) {
addition(7, 9);
});

当 RequireJS 看到上面的代码块时,它会去寻找依赖项,并通过将它们作为参数传递给函数来自动将其注入到模块中。

RequireJS 会自动为 addition.js​ 和 calculator.js​ 文件创建一个