我怎么记得我好像写过相关类型的文章,但是我找遍了我的博客没有~那就再写一遍吧,其实模块化的核心内容也算不上是复杂,只不过需要整理一下,规划一下罢了。嘻嘻。
开始写标题的时候我就在纠结一件事情,就是,先吃喜欢吃的,还是后吃喜欢吃的,翻译过来就是我应该先写CommonJS和ES6 Module,还是先写CMD和AMD。嗯,我决定了,谁先做好了我就先吃谁。
其实模块化的缘由很简单,就一句话,不对,就一个词,两个字,分类。如果一定让我在加一点,那应该是“隔离”。没了~~但是这么少不太好,我举个可能不那么恰当的例子吧。
刚开始这个世界上只有三个人,起名字的时候会“刻意”的避开彼此已经叫过的名字,他们在一起生活,日子欣欣向荣,一片美好,对未来充满了期待。
时间飞逝,三个人变成了三十人,他们勉强还是住在一起,起名字的时候虽然费事一点,但是也还能不重复,日子还是欣欣向荣,一片美好,对未来充满了期待。
时间又飞逝,三十人变成了三百人,那这不太好管理了,于是三位首领就说,你们有领导能力的几个人站出来,组成各自的部落,去吧,我相信你们可以的。于是每个部落住在一起,部落与部落之间的人可以重名,叫名字的时候再加上一个部落的名称呗,嗯~又一片欣欣向荣。
时间继续飞逝,三百人变成了三千人,这三千人住在几个大部落里也很不方便,你拿我的苹果,我偷了你得猪,这肯定不行,有碍于社会的稳定发展,于是三个创始者叫上部落的组长说,我们给每个人分一块地,盖一个房子,把三五个人分割成一个家庭,家庭之间由部落作为纽带,关联彼此,在形式上又相互独立,不可以随便拿别家的苹果。很完美~
时间飞飞飞飞逝,三千人变成了三百万人……我们需要法律了。
OK,上面的小例子,人,就是函数,部落就是命名空间,房子就是IIFE,法律就是后续发展的模块化规范。那么我们依照上面的描述,如何转换成代码?
一、社会的起源与法律的雏形
最开始的时候,浏览器只需要简单的图文展示就是可以了,没什么复杂的交互和逻辑的处理,所以,当我们只有三个人的时候,我们可以很自由,很随意:
function a(){} function b(){}
随着Web的发展,交互的增多,项目的扩大,很容易有人也声明了同样名称的函数,于是纷争开始了,那咋解决纷争呢?嗯,命名空间也就是拆分部落,就像这样:
var zaking1 = { a:function(){}, b:function(){} } var zaking2 = { a:function(){}, b:function(){} }
但是这样并不能真正的解决问题,因为虽然从形式上区分了部落,但是部落之间没有任何的隔离,部落内部也是混乱的,所以各个首领就制定了一个方案,IIFE,利用闭包的特性,来实现数据的隔离,暴露出对外的入口:
var module = (function () { var name = "zaking"; function getName() { console.log(name); } return { getName }; })(); module.getName();
我们盖好了房子,还给房子建好了可以出入的门,但是我怎么邀请别人进来呢?
var module = (function (neighbor) { var name = "zaking"; function getName() { console.log(name + "和邻居:" + neighbor); } return { getName }; })("xiaowangba"); module.getName();
传个参数呗,这就是依赖注入。在这个阶段,最有代表性的就是jQuery了,它的封闭性的核心实现,跟上面的代码几乎无异,我们可以看下jQuery的模块的实现:
(function (global, factory) { factory(global); })(typeof window !== "undefined" ? window : this, function (window, noGlobal) { if (typeof noGlobal === "undefined") { window.jQuery = window.$ = jQuery; } return jQuery; });
当然我这里略了很多,你看它,无非就是一个闭包,传入了window和jQuery本身,然后再绑定到window上,这样,我们就只能访问到暴露出来的$以及$上的方法和属性,我们根本无法修改内部的数据。
OK,到了这个阶段,其实算是一个转折点,我们有了初步的法律,还需要后续针对法律的完善,
二、法律的初现与CommonJs
随着社会的发展,出现一种规则已成必然,于是commonJs统领举起模块化的大旗,让JavaScript迈向了另一个阶段。commonJs最初由 JavaScript
社区中的 Mozilla
的工程师Kevin Dangoor
在Google Groups中创建了一个ServerJs小组。该组织的目标是为web服务器、桌面和命令行应用程序以及浏览器构建JavaScript生态系统。嗯,它的野心很大~,后来,他就就把ServerJs改成了commonJs,毕竟ServerJs的范围有点小,commonJs更符合他们的初衷。
而后,在同一年的年底,NodeJs出现了,Javascript不仅仅可以用于浏览器,在服务器端也开始攻城略地。NodeJs的初衷是基于commonJs社区的模块化规范,但是NodeJs并没有完全遵循于社区的一些腐朽过时的约束,它实现了自己的想法。
commonJs规范的写法,如果大家写过NodeJs一定都有所了解,大概是这样的:
// a.js module.exports = 'zaking' // b.js const a = require("./a"); console.log(a); // zaking
看起来挺简单的,但是这里隐藏了一些不那么容易被理解的特性。
在NodeJs中,一个文件就是一个模块,有自己的作用域,在一个文件里面定义的函数、对象都是私有的,对其他文件不可见。并且,当第一次加载某个模块的时候,NodeJ会缓存该模块,待再次加载的时候,会直接从模块中取出module.exports属性返回。比如:
// a.js var name = "zaking"; exports.name = name; // b.js var a = require("./a.js"); console.log(a.name); // zaking a.name = "xiaoba"; var b = require("./a.js"); console.log(b.name); // xiaoba
诶?为啥你写的是“exports.”,不是module.exports?NodeJs在实现CommonJs规范的时候为了方便,给每个模块都提供了一个exports私有变量,指向module.exports。有一点要尤其注意,exports
是模块内的私有局部变量,它只是指向了 module.exports
,所以直接对 exports
赋值是无效的,这样只是让 exports
不再指向 module.exports
了而已。
我们回到上面的代码,按理来说,我第二次引入的b的name应该是“zaking”啊。但是实际上,在第一次引入之后的引入,并不会再次执行模块的内容,只是返回了缓存的结果。
另外一个核心的点是,我们导入的是导出值的拷贝,也就是说一旦引入之后,模块内部关于该值的变化并不会被影响。
// a.js var name = "zaking"; function changeName() { name = "xiaowangba"; } exports.name = name; exports.changeName = changeName; // b.js var a = require("./a.js"); console.log(a.name); // zaking a.changeName(); console.log(a.name); // zaking
嗯,一切看起来都很不错。
三、争奇斗艳,百家争鸣
在上一小节,我们简单介绍了模块化的始祖也就是CommonJs以及实现了该规范的NodeJs的一些核心内容。但是NodeJs的实现的一个关键的点是,它在读取或者说加载模块的时候是同步的,这在服务器没什么问题,但是对于浏览器来说,这个问题就很严重,因为大量的同步模块加载意味着大量的白屏等待时间。
基于这样的问题,从CommonJs中独立出了AMD规范。
1、AMD规范与RequireJs
AMD,即Asynchronous Module Definition,翻译过来就是异步模块化规范,它的主要目的就是解决CommonJs不能在浏览器中使用的问题。但是RequireJs在实现上,希望可以通吃,也就是可以在任何宿主环境下使用。
我们先来看个例子:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <script src="https://requirejs.org/docs/release/2.3.6/comments/require.js"></script> <body></body> <script> require(["./a"]); </script> </html>
然后,我们的a.js是这样的:
define(function () { function fun1() { alert("it works"); } fun1(); });
define用来声明一个模块,require导入。我们还可以这样:
require(["./a"], function () { alert("load finished"); });
导入前置依赖的模块,在第二个参数也就是回调中执行。RequireJs会在所有的模块解析完成后执行回调函数。就算你倒入了一个没有使用的模块,RequireJs也一样会加载:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <script src="https://requirejs.org/docs/release/2.3.6/comments/require.js"></script> <body></body> <script> require(["./a", "./b"], function (a, b) { a.fun1(); }); </script> </html>
然后分别是a.js和b.js:
// a.js define(function () { function fun1() { alert("it works fun1"); } return { fun1: fun1, }; }); // b.js define(function () { function fun2() { alert("it works fun2"); } return { fun2: fun2, }; });
结果大家可以试一试~
以上就是RequireJs的简单用法,我们据此知道了两个核心内容,RequireJs基于AMD规范,RequireJs会加载你引入的所有模块,哪怕你并不会真的用到它。
2、CMD规范与SeaJs
由于RequireJs的一些问题,又出现了基于CMD规范的SeaJs,SeaJs和RequireJs有一个最大的不同就是RequireJs希望可以通吃,但是SeaJs则更专注于浏览器,哦对了,CMD的全称叫做:Common Module Definition,即通用模块规范。
SeaJs的简单用法如下:
// 所有模块都通过 define 来定义 define(function(require, exports, module) { // 通过 require 引入依赖 var a = require('xxx') var b = require('yyy') // 通过 exports 对外提供接口 exports.doSomething = ... // 或者通过 module.exports 提供整个接口 module.exports = ... }) // a.js define(function(require, exports, module){ var name = 'morrain' var age = 18 exports.name = name exports.getAge = () => age }) // b.js define(function(require, exports, module){ var name = 'lilei' var age = 15 var a = require('a.js') console.log(a.name) // 'morrain' console.log(a.getAge()) //18 exports.name = name exports.getAge = () => age })
上面的代码是从网上抄的,大概说明白了基本的使用方法。我们可以看到,SeaJs的导入和导出的方式,跟NodeJs好像~~而SeaJs从书写形式上,更像是CommonJs和AMD的结合。当然,我只是说书写形式上。
而AMD和CMD,RequireJs和SeaJs,都是由社区发起的,并没有语言层面的规范,包括CommonJs以及NodeJs,所以,这个时代还是一个百花争艳,没有统一的时代,不过在现在,这些都不重要了。如果非要我说些什么,那就是,忘记这两个东西,去学下面的重点。
四、大一统
百花争艳的时代确实有些烦人,这个那个那个这个,又都不被官方认可,还好,官方终于还是出手了,ES6的出现,在语言层面上就提出了对于模块化的规范,也就是ES6 Module。它太重要了,具体语法我就不多说了,文末的链接附上了阮一峰大神的《ES6入门指南》关于ES6 Module的地址。
所以到了ES6的时候,你要学习的就是ES6 Module,NodeJs也在逐步实现对ES6 Module的支持。最终,秦始皇会一统天下,这是必然的结果。
这篇文章到这里就结束了,说实话,模块化的问题和历史由来已久,从萌芽到统一,至少十几年的过程,而市面上也已有大量的文章介绍彼此的区别和各自的特点,我写来写去,也不过是复制一遍,毫无意义。
但是我又想学一下模块化,以及模块化的历史,额……,请原谅我的无知,所以才有了这篇文章,但是写着写着,发现我能表达出来的东西并不多,因为都是故事,都是历史,并且对于未来的开发好像也没什么实际的意义和价值。
所以,在如此纠结的心态下有了这篇文章,原谅我无知又想逼逼的心情吧。
最后的最后,如果你想学习模块化,在现阶段,只需要去深入学习ES6 Module,和学习一下NodeJs的CommonJs,以及了解一下各模块化的区别即可,因为现在是即将统一,还未完全统一的时候。
参考资料:
https://wiki.commonjs.org/wiki/CommonJS
https://github.com/seajs/seajs/issues/242
https://es6.ruanyifeng.com/#docs/module