模块化的演变
在没有模块化的概念之前,代码量也不大的时候,可以直接将JavaScript代码写在html的<script>标签中,如下
1 | <script type="text/javascript"></script> |
当代码量变多了,也可以在html引入外部js脚本,如下
1 | <script src="js/index.js"></script> |
当业务变得复杂,人们开始根据页面来引入脚本,即一个页面就引入包含此逻辑的脚本,公用的脚本可以每个页面都引入。但是,当这个公用的脚本过于复杂,有多个处理逻辑,如下
1 | function fn1() {} |
有可能页面1只需要fn1,fn2等等很多函数,页面2却只需要fn1,却都要引入整个脚本,这就不太合理了。
那么,按照功能来分如何?我们在a.js, b.js, c.js分别处理a, b, c如下,然后引入该页面的逻辑index.js,简单起见这里index.js就简单的把a, b, c输出。
1 | <html> |
1 | // a.js |
1 | // b.js |
1 | // c.js |
1 | // index.js |
这种写法存在的问题:
- 顺序问题:在
html的引入脚本的时候,顺序必须要按照模块的逻辑,否则就会报错,这是因为解析html会造成阻塞,解析完一个脚本再进行下一个,假设我们把index.js先引入了,这个时候a,b,c都还没有定义。 - 作用域问题:可以看到这些文件是公用一个作用域的,在单独脚本暴露出来的变量,实际上都存在于全局作用域。容易发生变量覆盖等问题。
这里就引出了模块化需要解决的问题:加载顺序,污染的全局变量。
为了避免全局变量的污染,可以通过闭包来尝试解决,将需要的变量通过返回值暴露出去即可。
下面通过立即执行函数把之前例子改写如下
1 | // a.js |
该模块是暴露在全局的,可以a变量是模块局部的,在 b 中要使用可通过moduleA.a,下面通过注入的方式避免了在全局下寻找moduleA。
这样做除了保证模块的独立性,还使得模块之间的依赖关系变得明显。
类似于jQuery:;(function($){//do something}(jQuery))
1 | // b.js |
在c.js中同理,通过注入moduleB来使用moduleB的变量。
1 | // c.js |
最后,index.js依赖于以上模块。
1 | // index.js |
但是该方法依然要求保证引入脚本的顺序。
开发者通过实践,形成了一种插件化模式,很多也是通过立即执行函数来实现的。插件能够给用户提供一些可配置项,让用户通过配置项实现不同的功能。
模块化解决方案
CommonJS
CommonJS 是来源于 NodeJS 的一种规范,需要node环境。
并且由于它是同步加载的,通过require引用,必须加载完成后才会执行下面的代码(阻塞),这样就很不适合浏览器环境。
这对服务器端不是一个问题,因为所有的模块都存放在本地硬盘,可以同步加载完成,等待时间就是硬盘的读取时间。但是,对于浏览器,这却是一个大问题,因为模块都放在服务器端,等待时间取决于网速的快慢,可能要等很长时间,浏览器处于”假死”状态。
这也是之后引入异步模块定义(Asynchronous Module Definition, AMD)的原因。
使用 CommonJS 改写上面的例子,此时index.html只需要引入index.js即可。
1 | // [common JS] a.js |
1 | // [common JS] b.js |
1 | // [common JS] c.js |
1 | // [common JS] index.js |
需要注意的是,require会创建一个模块的实例,并且有缓存机制,
CommonJS一个模块对应一个脚本文件,require命令每次加载一个模块就会执行整个脚本,然后生成一个对象。这个对象一旦生成,以后再次执行相同的require命令都会直接到缓存中取值。
引用模块脚本进来其实是一个立即执行函数,例如,对于下面的 JS 模块文件
1 | var modulex = function () { |
引入时会被node.js包装成如下形式,这也是为什么我们能够使用exports, require, module的原因。
1 | (function (exports, require, module, __filename, __dirname) { |
AMD
异步模块定义(Asynchronous Module Definition, AMD)是是在浏览器环境下出现的模块化解决方案,异步加载模块,并且是前置依赖,即依赖加载完毕后才会执行最后一个参数的回调函数。
1 | //Calling define with module ID, dependency array, and factory function |
AMD基于require.js,主要解决的问题是:
- 实现
js文件的异步加载,避免网页失去响应; - 管理模块之间的依赖性,便于代码的编写和维护。
使用AMD改写上面的例子
1 | // [AMD] a.js |
1 | // [AMD] b.js |
1 | // [AMD] c.js |
需要在入口文件配置依赖的脚本的路径,在此例中是index.js。
1 | // [AMD] index.js |
AMD解决了模块依赖问题,规范化了输入输出。
CMD (SeaJS)
Common Module Definition,即通用模块定义。CMD 是 SeaJS 在推广过程中对模块定义的规范化产出。
CMD规范和AMD类似,都主要运行于浏览器端,写法上看起来也很类似。主要是区别在于模块初始化时机
- AMD 是依赖前置:只要模块作为依赖时,就会加载并初始化,加载完后执行回调
- CMD 是依赖就近:模块作为依赖且被引用时才会初始化,否则只会加载。
AMD的API默认是一个当多个用,CMD严格的区分推崇职责单一。例如,AMD里require分全局的和局部的。CMD 里面没有全局的require,提供seajs.use()来实现模块系统的加载启动。CMD里每个API都简单纯粹。
UMD 规范
集结了 CommonJs, CMD, AMD 的规范于一身,通过运行时或者编译时让同一个代码模块在使用 CommonJs, CMD, AMD的项目中运行。
1 | // UMD 简单实现 |
ES6 模块化
import命令用于输入其他模块提供的功能,export命令用于规定模块的对外接口。
1 | //lib.js |
ES6 Module 和 CommonJS 对比
ES6 Module是静态化的,在编译时确定模块的依赖关系。CommonJS和AMD只能在运行时确定。CommonJS模块输出的是一个值的浅拷贝,ES6输出的是值的引用
1 | // [CommonJS] |
1 | // [ES6 Module] |