JavaScript模块化演进历史
前言
回顾 JavaScript 的发展历程,从最初的简单浏览器脚本语言,到如今构建互联网应用程序的现代编程语言,模块化技术在这一演变中发挥了关键作用。本篇文章将从 JavaScript 的模块化探索及模块化规范的持续演进两部分,回顾 JavaScript 模块化的演进历程。
模块化理念
在回顾 JavaScript 模块化技术的演进历程之前, 我们先了解一些基本的概念。何谓模块?且看维基百科给出来的定义:
模块:是指由数个基础功能组件组成的特定功能组件,可用来组成具完整功能之系统、设备或程序。模块通常都会具有相同的制程或逻辑,更改其组成组件可调适其功能或用途。
我们可以简单地将模块理解为一个独立的文件或脚本,每个文件/脚本都被视为一个模块,包含特定的功能或逻辑。模块将代码分离到独立的单元中,方便管理、复用和维护。
那么,什么又是模块化呢?
模块化是现代软件工程的核心原则之一,它通过将大型的复杂系统拆解为更小、更容易管理和理解的部分——功能模块,来提高系统的可维护性和可拓展性。每个功能模块都是一个独立的单元,具有清晰定义的接口和职责,能够与其他模块交互以完成复杂的任务。
模块化是将复杂系统拆分为独立模块的设计方法,这些模块能够独立开发、测试和维护,并可以在不同项目或环境中复用。模块化在现代软件开发中起到了至关重要的作用,特别是在大型应用程序的开发中,模块化能够显著提高开发效率和代码质量。
JavaScript 模块化探索
我们知道,由于一些 历史的原因,很长一段时间 JavaScript 语言是没有模块化的概念的。
最早,我们就是直接在 script 标签里书写 JS 代码的:
<script>
function add (a, b) {
return a + b;
};
add(1,2);
</script>
随着代码量的增加,我们将业务逻辑拆分成多个 js 文件:
/* a.js */
function add(a, b) {
return a + b;
}
/* b.js */
function average(a, b) {
return add(a, b) / 2;
}
/* main.js */
var result = average(5, 10);
console.log(result);
然后在 HTML 中的引入如下:
<script src="./a.js"></script>
<script src="./b.js"></script>
<script src="./main.js"></script>
我们可以明显发现,由于这些 js 文件中的函数和变量直接暴露在全局作用域中,不仅容易导致命名冲突,还使得文件之间的依赖关系难以管理。
命名空间
随着代码量的增加,这些问题愈加严重。为了解决命名冲突并促进团队协作,命名空间的概念应运而生。
var moduleA = {
name = 'moduleA'
}
moduleA.add = function(a, b) {
console.log(a + b)
}
moduleA.add(1, 2)
引入命名空间后,命名冲突问题得到了部分缓解,模块间的依赖关系也更为清晰。然而,这种封装方式本质上是对象,对外仍然暴露了不应被访问的内容,安全性不足。
例如:b.js
模块的开发者,可以很方便的通过 moduleA.name
来取到 模块A
中的名字,但是也可以通过 moduleA.name = 'rename'
来任意改掉模块A
中的名字,而这件事情,模块A
却毫不知情!这显然是不被允许的。
显然,这种方式并未解决根本问题,但模块化的概念已经开始萌芽,只是暂时通过命名空间来实现。
立即调用函数表达式(IIFE)
立即调用函数的匿名闭包是模块化实现的基石
为了更好地解决全局作用域污染和命名冲突问题,开发者引入了自执行函数和闭包的概念,这就是IIFE(Immediately Invoked Function Expression) 。
立即调用函数表达式(IIFE)是一个在定义时就会立即执行的 JavaScript 函数。它是一种设计模式,也被称为自执行匿名函数,主要包含两部分:
-
第一部分是一个具有词法作用域的匿名函数,并且用圆括号运算符
()
运算符闭合起来。这样不但阻止了外界访问自执行匿名函数中的变量,而且不会污染全局作用域。 -
第二部分创建了一个立即执行函数表达式
()
,通过它,JavaScript 引擎将立即执行该函数。
IIFE 提供了一种简单而有效的方式来创建独立的作用域。通过将函数包装在括号内并立即调用,它创建了一个新的执行上下文,这样函数内部的变量和函数不会污染全局作用域。
IIFE 示例代码如下:
var utils = (function() {
var moduleA = {}
moduleA.add = function(a, b) {
console.log(a * b)
}
return moduleA
}())
utils.add(1,2);
然而,这种实现并不完美,仍然需要手动维护依赖顺序。例如,jQuery 将所有函数都集中在全局对象 $
中,项目必须确保 jQuery 加载完成后才能使用 $
。
此时,我们认识到,模块化不仅需要解决全局变量污染和数据保护的问题,还必须有效地管理模块之间的依赖关系。
JavaScript 模块化规范的持续演进
随着 JavaScript 应用程序复杂性的提升,对模块化的需求也日益增长,推动了社区不断提出和发展新的模块化规范。
CommonJs
2009年1月,Mozilla 工程师 Kevin Dangoor 创建了名为 ServerJS 的项目。到了2009年8月,这个项目更名为 CommonJS,以彰显其 API 的广泛适用性。通过 CommonJS,Node.js 实现了高效的模块加载、管理和组织,使服务器端 JavaScript 编程变得更加模块化和高效。
CommonJS
约定:
- 每个文件就是一个模块, 有自己的作用域
- 每个文件中定义的变量、函数、类都是私有的,对其它文件不可见
- 每个模块内部可以通过 exports 或者 module.exports 对外暴露接口
- 每个模块通过 require 加载另外的模块
CommonJs
使用代码示例如下:
/* a.js */
function add(a, b) {
return a + b;
}
module.exports = {
add,
};
/* b.js */
const moduleA = require('./a.js');
const result = moduleA.add(5, 10);
console.log(result);
CommonJS 提供了统一的模块定义和管理方式,使模块间的依赖关系更加明确和易于管理。它标志着 JavaScript 在服务器端模块化的初步发展,并为现代 JavaScript 生态系统奠定了基础。
CommonJS 规范在服务端实现了模块化,提供了同步加载模块的方式,这在本地磁盘读取模块时非常高效。然而,在浏览器中,同步加载可能导致浏览器假死,因为从服务器加载模块需要时间。因此,在客户端,异步加载模块成为更合理的选择,以避免阻塞和性能问题。
AMD
AMD和CMD只是一种设计规范,而不是一种实现。
AMD
即 Asynchronous Module Definition
,它采用异步的方式加载 JavaScript 模块,模块的加载并不会影响它后面语句的运行。
AMD 规范规定用 define
定义模块用 require
加载模块,语法如下:
// 模块定义
define(id?, dependencies?, factory);
// 模块加载
require([module], callback);
AMD
提供了一种异步加载模块的机制,旨在解决前端模块化中的依赖管理和加载顺序问题,从而与 CommonJS
在不同环境下的模块化需求形成了有益的补充。RequireJS
遵循 AMD
规范,为客户端 JavaScript 模块化开发提供了简化的解决方案。
CMD
CMD
(Common Module Definition
)是玉伯在开发 SeaJS 的时候提出来的,SeaJS 要解决的问题和 RequireJS 一样。不同于 AMD 的依赖前置,CMD 是就近依赖。且看下面这个案例:
// AMD
define(['a', 'b'], function(a, b) {
// 模块 a 和 b 在这里就都执行好并可用了
})
// CMD
define(function(require, exports) {
// ...
var a = require('a') // 模块 a 运行到此处才执行
// ...
if (false) {
var b = require('b') // 当某些条件为 false 时,模块 b 永远也不会执行
}
})
AMD
强调异步加载和预定义依赖,适用于需要提前加载模块的场景。CMD
则支持按需加载,提供了更大的灵活性,允许在模块内部动态引入依赖。尽管两者有差异,但它们的出现都推动了客户端 JavaScript 模块化的发展。
UMD
UMD
即通用模块定义(Universal Module Definition
)。为了兼容 CommonJS 和AMD,UMD 规范被引入。它允许同一模块在不同环境中运行,既可以在 Node.js 中使用,也可以在浏览器中使用。
核心代码实现如下:
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define(['dependency'], factory);
} else if (typeof module === 'object' && module.exports) {
// Node, CommonJS-like
module.exports = factory(require('dependency'));
} else {
// Browser globals (root is window)
root.myModule = factory(root.dependency);
}
}(this, function (dependency) {
return {
greet: function() {
console.log('Hello, UMD');
}
};
}));
UMD
本质上就是帮你判断应该用 AMD
还是 CommonJS
,是哪个就用哪个方式来定义模块,都不是的话就挂到全局对象上。
ES Module
在 ES Module
之前,JavaScript
并没有官方的模块化机制,开发者依赖于像CommonJS
、AMD
和 UMD
等第三方规范来实现模块化。这些规范虽然有效,但都存在一些局限性,且在浏览器和服务器端的实现上有所不同,导致代码复用性和跨环境的兼容性变得复杂。
为了统一 JavaScript
的模块化标准,同时满足现代 Web 开发的需求,TC39(ECMAScript 标准委员会)在 ES6 中正式引入了 ES Module
。
ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。
基本使用示例如下:
/* a.js */
export const add = (a, b) => {
return a + b
}
/* main.js */
import { add } from "./a.js"
add(1, 2)
ES Module
的引入标志着 JavaScript 正式进入了模块化开发的时代。它统一了模块的定义和使用方式,使得代码的复用性和维护性大大增强。此外,ES Module 还推动了前端开发工具链的发展,比如 Webpack、Rollup 等工具都基于 ES Module 进行打包和优化,极大地提升了开发效率和代码性能。
总结
从 IIFE 的模块化雏形到 ES Module 的成熟标准,JavaScript 模块化技术的发展不仅反映了 Web 开发的不断演进,也预示着未来技术将更加模块化和组件化。同时,这一进程也体现了 JavaScript 从简单脚本语言转变为支持复杂应用的现代编程语言的历程。