前言
我们知道,JavaScript 最开始诞生,只是作为嵌入网页的脚本语言来使用,那时候,它最重要的一个功能,就是帮助浏览器在不跟后端通信的情况下,验证用户提交表单的合法性。
可是随着时间的发展,业务场景逐渐复杂,尤其 ajax 诞生之后,前端的能力愈发强大。管理十人团队的经验显然不能用来管理万人团队,维护百行代码的方式,自然也不适用于开发周期长、开发难度高,甚至涉及多人协作的中大型项目。
这种情况下,JS 模块化的呼声愈发响亮。
但那个时候,JS 语言本身是没有模块化这个概念的,于是各种模块化方案被依次推出。
本文将通过介绍 JS 模块化的发展历史,来逐个了解那些曾经流行过,或者正在流行的 JS 模块化规范和实现。
废话不多说,我们直接开始吧。先来看看第一个部分。
JS 模块化的发展历史
JS 模块化的发展历史,遵循所有新生事物的历史发展轨迹:在迎来正式规范之前,经历一段野蛮生长的时期。具体来说,它可以被分成如下三个阶段:
- CommonJS 出现前的史前混乱时期
- ES6 Module 出现前的大混战时期
- 现在:ES6 Module 的亮相
下面,我们逐项来了解。
CommonJS 出现前的史前混乱时期
从标题可以看出,这个时期的 JS 模块化是没有任何规范可言的,用最形象的四个字来形容,那就是:随 心 所 欲。
最开始大家通过拆分文件,来完成模块的划分。
好比,一个网页有那么几个功能区,功能区 A 由小明负责,功能区 B 划分给小刚,他们各自新建文件去完成功能,最后统一汇总到 HTML。这时候的文件目录,差不多长这样。
|- index.html
|- src
|- A
|- index.js
|- index.css
|- B
|- index.js
|- index.css
这样做看起来还算简洁明了,但也只是看起来,它很快迎来问题。
因为所有 JS 代码的执行环境都是全局作用域,通过 var 声明的变量也都是全局变量,如果小明和小刚不小心,声明了同一个名称的变量,页面就会出错。
// src/A/index.js
var name = 'a';
// src/B/index.js
var name = 'b';
假设 A 文件先加载,B 文件在后,那么当小明尝试使用 name 时,他就会发现,这个变量已经被小刚污染。但这个问题也是不能解决,给变量添加命名空间就可以。
我们给小明分配一个命名空间叫做 A,给小刚分配一个命名空间叫做 B,于是代码变成了如下模样:
// src/A/index.js
app.A = {
name: 'a'
};
// src/B/index.js
app.B = {
name: 'b'
};
这样的代码虽然写起来很恶心,相当恶心,但也不是不能接受,至少它解决了全局作用域下,变量命名混乱的问题。
可是!
随着小明的代码越写越多,小刚的代码越写越长,他们会慢慢发现:欸?我为什么要把所有变量都挂载到命名空间下面啊?明明有些变量我只在自己的文件里用到,根本不需要暴露给外面啊!
这个问题也容易解决,闭包!
好了,我们再来改一下代码。
// src/A/index.js
app.A = (function(){
var name = 'a';
var age = 18; // 不暴露的私有变量
return {
getName: function(){
return name;
}
}
})();
// src/B/index.js
app.B = (function(){
var name = 'b';
var age = 19; // 不暴露的私有变量
return {
getName: function(){
return name;
}
}
})();
通过一个立即执行的函数(IIFE),小明和小刚将他们需要导出的方法和变量,作为函数的返回值,暴露给命名空间,其余的,都隐藏在闭包内部。
这是一个相对完善的做法,虽然离完美还差了很远很远,很远很远。
上面例子中,如果 A 先于 B 加载,那么理所当然的,B 可以使用 A 所暴露的变量和方法,A 却不能使用 B 的。
这是一个依赖管理的问题,而当页面的复杂度进一步提升,参与开发的人员进一步变多,模块之间的依赖也将变得更加难以维护。举个例子:小明依赖小刚、小刚依赖小美、小美依赖小帅、小帅又依赖了小刚,这时候,你到底该先加载谁?
但这个方法还是在一定时期内,被很多团队使用了相当长一段时间,直到 CommonJS 规范的出现。
ES6 Module 出现前的大混战时期
1. CommonJS
2009年,JavaScript 社区提出了一个名叫 CommonJS 的标准,通过它,社区希望实现让 JS 运行在任何平台的伟大愿景。
JS 模块化的规范标准也被蕴含在其中,至此, JS 模块化大混战的帷幕也正式被拉开。
首先,2009 年 5 月,Node.js 出世了。
CommonJS 规范推动过程中,Node.js 应运而生,同时,Node.js 的出世也标志着 JS 模块化编程的正式诞生。
CommonJS 规定,每个模块内部有两个变量可以使用,require 和 module,开发者通过 module.exports 来完成接口和变量的导出,通过 require 来实现模块的导入。像这样:
// src/A/index.js
var name = 'a';
var age = 18;
// 导出一个获得私有变量 name 的方法。
module.exports = {
getName: function () {
return name;
}
};
// src/B/index.js
var A = require('../A/index.js');
var name = 'b';
var age = 19;
console.log(A.getName());// a
// 导出一个获得私有变量 name 的方法。
module.exports = {
getName: function () {
return name;
}
};
这个规范的实现是如此优雅、如此便捷,以至于,它刚刚推出,便受到社区的一致欢迎,但它也不是没有问题,它还有很多问题,最明显一个就是:
它跟前端没关系。
是的,我们都知道,Node.js 是运行在服务端的 JS,那么, 除非你有转去服务端的打算,否则,这玩意儿的出现,跟你没半毛钱的关系也没有。
他们的欢呼跟你没关,他们的雀跃只让你觉得:好酸啊。
不过俗话也说了,有问题就会有答案,有了 CommonJS + Node.js 这个先例,我们聪明又勤劳的前端开发者们,很快根据自身经验,摸索出了一套适用于前端开发的新规范。
它跟 CommonJS 最大的一个区别在于,它是异步加载模块的,所以,我们称它为:AMD( Asynchronous Module Definition,异步模块定义)。
2. AMD
它通过一个 define 函数,来实现对模块的定义和引用,基于该规范,RequireJs 被捯饬出来。你可以这样使用它:
<script src="require.js"></script>
<script src="src/A/index.js"></script>
此前的 JS 文件,也会被修改成如下模样:
// src/A/index.js
define(function () {
var name = 'a';
var age = 18; // 私有变量,不暴露
return {
getName: function () {
return name;
}
};
});
// src/B/index.js
define(['../src/A/index.js'], function (A) {
var name = 'b';
var age = 19; // 私有变量,不暴露
console.log(A.getName()); // a
return {
getName: function () {
return name;
}
};
});
它解决了互相依赖的问题,按需加载也被支持,它在很大程度上,解决了此前模块管理混乱的问题。
但它并不是结束。
很快有人提出,RequireJs 虽然能用,却并非最优,按照这个规范编写出来的代码,既不够简洁,也不够优雅。他们希望能够像 Node.js 编写模块那样,在前端也实现自然直观的代码组织,基于这个想法,社区又多出一个 CMD 规范和基于该规范 sea.js。
3. CMD
Sea.js 官网这么介绍 Sea.js的:
Sea.js 追求简单、自然的代码书写和组织方式,具有以下核心特性:
- 简单友好的模块定义规范:Sea.js 遵循 CMD 规范,可以像 Node.js 一般书写模块代码。
- 自然直观的代码组织方式:依赖的自动加载、配置的简洁清晰,可以让我们更多地享受编码的乐趣。
按照这个规范编写出来的代码长这样:
// src/A/index.js
define(function (require, exports, module) {
var name = 'a'
var age = 18; // 私有变量,不暴露
exports.getName = function () {
return name;
};
});
// src/B/index.js
define(function (require, exports, module) {
var A = require('../A/index.js');
var name = 'b'
var age = 19; // 私有变量,不暴露
console.log(A.getName()); // a
exports.getName = function () {
return name;
};
});
在这之后,还有基于 CommonJS 规范的 Webpack,它让前端开发者可以像书写 Node.js 的代码那样,编写前端页面。
可以说,在 ES6 Module 出现之前,很长一段时间内,JS 模块化都处在这三种规范的混战之中。
然后时间来到 2015,es6 正式推出。
ES6 Module
前面提到的 CommonJS 、AMD 和 CMD 看起来天差地别,但它们其实也有不少共同之处:
- 它们都在代码运行后,才能确定导出的内容。
- 它们都是社区的开发者们,制定的规范方案。并不是语言层面的标准。
ES6 Module 在语言标准的层面上,实现了模块化功能,而且实现得相当简单,完全可以取代 CommonJS、CMD 和 AMD,成为浏览器和服务器通用的模块解决方案。
它主要由两个命令构成:export 和 import,export 负责导出,import 负责引入,在支持 ES6 Module 的环境下,你可以像这样编写代码:
// src/A/index.js
let name = 'a'
let age = 18; // 私有变量,不暴露
let getName = function () {
return name;
};
export { getName };
// src/B/index.js
let A = import '../A/index.js';
let name = 'b'
let age = 19; // 私有变量,不暴露
console.log(A.getName()); // a
let getName = function () {
return name;
};
export { getName };
目前,Node.js 已经全面支持 ES6 Module,现代浏览器也基本上实现了对这一语法糖的支持。
以上就是本文的全部内容啦,感谢看到这里的大小可爱,大家有缘江湖再见!
网友评论