历史原因
很长一段时间,JavaScript 都没有语言级(language-level)模块语法。早期的项目也比较简单也比较小,所以那时是没有“模块化”的思想的。那带来的问题是:
- 命名冲突:在全局命名空间下定义的变量容易造成命名冲突,这会导致代码出现错误或者不可预测的行为。
- 依赖管理:代码的依赖关系很难清晰地描述,这会导致代码的可维护性和可扩展性变得非常差。
- 文件加载:在一个大型的JavaScript应用程序中,可能会有成百上千个文件需要加载,这会导致应用程序的加载时间变得非常长。
- 可读性:在没有模块化的情况下,代码的结构和组织很难理解,这会降低代码的可读性和可维护性。
因此,模块化编程规范的出现,可以解决上述问题,使得javaScript应用程序更加易于开发、维护和扩展。
各模块化编程规范的发展
JavaScript模块(2006年)
JavaScript模块的概念早在2006年就被提出来了,但当时没有任何官方的规范。这个时期,开发者们通常使用命名空间来避免全局变量的污染,但这种方式难以管理和维护。
CommonJS规范(2009年)
2009年,Node.js的出现催生了CommonJS规范的诞生。CommonJS规范定义了一个标准的模块格式,使得JavaScript代码可以更好地组织和共享。这个规范包含了模块的定义、引入和导出方式,以及模块的生命周期管理等内容。
在CommonJS规范中,每个模块都被视为一个独立的作用域,它可以通过“require”函数来引入其他模块,并通过“exports”对象来导出自己的内容。这个规范被广泛用于Node.js中,也被其他JavaScript运行时环境所采用。
在 Node.js 环境下,CommonJS 规范是内置的,不需要额外的库,
require函数是由 Node.js 实现的。Node.js 中的require函数可以加载本地文件和第三方模块,加载的模块可以通过module.exports` 导出数据。
// math.js
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
module.exports = {
add,
subtract
};
在另一个模块中,可以使用“require”函数引入该模块并使用其公开的函数和变量,如下所示:
javascript
// app.js
const math = require('./math.js');
console.log(math.add(1, 2)); // 输出 3
console.log(math.subtract(3, 2)); // 输出 1
这样,就可以将代码分解为多个模块,并将这些模块导入到一个主文件中。这种模块化的方式使得代码更易于维护和扩展,同时避免了全局变量的命名冲突。
AMD规范(2011年)
AMD(Asynchronous Module Definition)规范是一种用于浏览器端的模块规范。它与CommonJS规范不同的是,它支持异步加载模块,避免了阻塞浏览器的问题。
在AMD规范中,模块的定义和导入使用的是“define”函数,这个函数可以异步地加载模块,并在模块加载完成后执行回调函数。AMD规范被广泛用于浏览器端的JavaScript模块化开发。
// 定义dog.js
define(function () {
console.log("狗狗")
return {
say: () => "我是一条狗"
}
})
// 定义main.js
define(function(require) {
var dog = require('./dog')
console.log(dog.say())
})
// index.html引用
<script src="https://cdn.bootcss.com/require.js/2.3.6/require.min.js"></script>
<script>
// main.js在当前目录
requirejs(["main"])
</script>
在使用模块时,需要使用 require 函数来异步加载模块,并在回调函数中使用模块。例如:
在浏览器环境下,通常使用 AMD 规范或 ES6 规范进行模块化开发。浏览器中的
require函数需要通过第三方库实现,如 RequireJS 就是一个比较流行的实现 AMD 规范的库。
在浏览器中使用 AMD 规范时,需要引入 RequireJS 这个库,RequireJS 可以在浏览器中实现 AMD 规范。
UMD规范 (2012年)
UMD规范是一种用于编写通用模块的规范,它可以兼容CommonJS、AMD和全局变量等多种模块化方案。 下面是一个使用UMD规范的示例:
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define(['jquery'], factory);
} else if (typeof exports === 'object') {
// Node, CommonJS-like
module.exports = factory(require('jquery'));
} else {
// Browser globals (root is window)
root.returnExports = factory(root.jQuery);
}
}(this, function ($) {
// 方法定义
function myFunc() {};
// 暴露接口
return myFunc;
}));
这个示例使用了一个自执行函数,这个函数首先检查当前环境支持哪种模块化方案,然后根据情况将模块公开。在函数的最后,返回了要暴露的函数。
总之,UMD规范为JavaScript模块化提供了更多的灵活性和兼容性,使得开发者可以在不同的环境中使用相同的代码。
ES6模块规范(2015年)
ES6(ECMAScript 2015)是JavaScript的一个重要版本,它引入了许多新特性,其中包括了一种新的模块规范。ES6模块规范定义了一种新的模块格式,它可以在浏览器和Node.js中使用。
ES Module是一种在语言层面上支持的模块化规范,也是ES6(ECMAScript 2015)规范中新增的一项功能。ES Module的主要目的是让JavaScript在不同的环境中实现更加高效、可靠和安全的模块化。
ES Module的语法使用了import和export关键字来定义和导出模块,示例如下:
javascript
// 模块定义
// 定义一个默认导出
export default function() {
console.log('Hello from module!');
}
// 定义命名导出
export function foo() {
console.log('Hello from foo!');
}
export function bar() {
console.log('Hello from bar!');
}
// 模块导入
import myModule, { foo, bar } from './my-module.js';
myModule(); // 输出 "Hello from module!"
foo(); // 输出 "Hello from foo!"
bar(); // 输出 "Hello from bar!"
在上面的示例中,我们定义了一个默认导出和两个命名导出,并通过import语句导入了模块,并分别调用了其中的函数。
ES Module相较于CommonJS和AMD等模块化规范具有以下优势:
- 编译时静态分析:ES Module在编译时可以进行静态分析,这意味着可以在代码执行前检查模块依赖关系,从而提高代码的性能和可靠性。
- 单独的命名空间:ES Module中每个模块都有自己独立的命名空间,避免了命名冲突和全局变量污染的问题。
- 非循环依赖:ES Module支持非循环依赖,避免了CommonJS中存在的循环依赖问题。
需要注意的是,ES6是JavaScript语言的一个版本,而ES Module是其中的一项功能,因此它们之间有密切的关系。在ES6中引入了ES Module规范,使得JavaScript在语言层面上支持模块化,使得开发者可以更加方便地管理和组织代码。
模块化时代,最受欢迎的webpack
webpack和requirejs不一样的地方是,webpack在编译时确定了入口文件及文件的依赖关系,通过解析文件按文件依赖关系进行打包,webpack可以把es6的模块化代码,打包成AMD,UMD,可以直接查看https://webpack.js.org/configuration/output/#outputlibrarytarget
module.exports = {
mode: 'development',
entry: './example/entry.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
library: 'MyLibrary',
libraryTarget: 'amd' //commonjs,commonjs2, window,amd, global,
}
};