模块化是咱们平常开发都要用到的基本技能,使用简单且方便,可是不多人能说出来可是的缘由及发展过程。如今经过对比不一样时期的js的发展,将JavaScript模块化串联起来整理学习记忆。javascript
技术的诞生是为了解决某个问题,模块化也是。在js模块化诞生以前,开发者面临不少问题:随着前端的发展,web技术日趋成熟,js功能愈来愈多,代码量也愈来愈大。以前一个项目一般各个页面公用一个js,可是js逐渐拆分,项目中引入的js愈来愈多:html
<script src="zepto.js"></script> <script src="jhash.js"></script> <script src="fastClick.js"></script> <script src="iScroll.js"></script> <script src="underscore.js"></script> <script src="handlebar.js"></script> <script src="datacenter.js"></script> <script src="util/wxbridge.js"></script> <script src="util/login.js"></script> <script src="util/base.js"></script>
当年我刚刚实习的时候,项目中的js就是相似这样,这样的js引入形成了问题:前端
这些问题严重干扰开发,也是平常开发中常常遇到的问题。java
我以为用乐高积木来比喻模块化再好不过了。每一个积木都是固定的颜色形状,想要组合积木必须使用积木凸起和凹陷的部分进行链接,最后多个积木累积成你想要的形状。node
模块化实际上是一种规范,一种约束,这种约束会大大提高开发效率。将每一个js文件看做是一个模块,每一个模块经过固定的方式引入,而且经过固定的方式向外暴露指定的内容。git
按照js模块化的设想,一个个模块按照其依赖关系组合,最终插入到主程序中。es6
模块化这种规范提出以后,获得社区和广大开发者的响应,不一样时间点有多种实现方式。咱们举个例子:a.jsgithub
// a.js var aStr = 'aa'; var aNum = cNum + 1;
b.jsweb
// b.js var bStr = aStr + ' bb';
c.jsnpm
// c.js var cNum = 0;
index.js
// index.js console.log(aNum, bStr);
四份文件,不一样的依赖关系(a依赖c,b依赖a,index依赖a b)在没有模块化的时候咱们须要页面中这样:
<script src="./c.js"></script> <script src="./a.js"></script> <script src="./b.js"></script> <script src="./index.js"></script>
严格保证加载顺序,不然报错。
这是最容易想到的也是最简便的解决方式,早在模块化概念提出以前不少人就已经使用闭包的方式来解决变量重名和污染问题。
这样每一个js文件都是使用IIFE包裹的,各个js文件分别在不一样的词法做用域中,相互隔离,最后经过闭包的方式暴露变量。每一个闭包都是单独一个文件,每一个文件仍然经过script标签的方式下载,标签的顺序就是模块的依赖关系。
上面的例子咱们用该方法修改下写法:
a.js
// a.js var a = (function(cNum){ var aStr = 'aa'; var aNum = cNum + 1; return { aStr: aStr, aNum: aNum }; })(cNum);
b.js
// b.js var bStr = (function(a){ var bStr = a.aStr + ' bb'; return bStr; })(a);
c.js
// c.js var cNum = (function(){ var cNum = 0; return cNum; })();
index.js
;(function(a, bStr){ console.log(a.aNum, bStr); })(a, bStr)
这种方法下仍然须要在入口处严格保证加载顺序:
<script src="./c.js"></script> <script src="./a.js"></script> <script src="./b.js"></script> <script src="./index.js"></script>
这种方式最简单有效,也是后续其余解决方案的基础。这样作的意义:
不过各个模块的依赖关系仍然要经过加装script的顺序来保证。
一开始一些人在闭包的解决方案上作出了规范约束:每一个js文件始终返回一个object,将内容做为object的属性。
好比上面的例子中b.js
// b.js var b = (function(a){ var bStr = a.aStr + ' bb'; return { bStr: bStr }; })(a);
及时返回的是个值,也要用object包裹。后来不少人开始使用面向对象的方式开发插件:
;(function($){ var LightBox = function(){ // ... }; LightBox.prototype = { // .... }; window['LightBox'] = LightBox; })($);
使用的时候:
var lightbox = new LightBox();
当年不少人都喜欢这样开发插件,而且认为能写出这种插件的水平至少不低。这种方法只是闭包方式的小改进,约束js文件返回必须是对象,对象其实就是一些个方法和属性的集合。这样的优势:
本质上这种方法只是对闭包方法的规范约束,并无作什么根本改动。
早期雅虎出品的一个工具,模块化管理只是一部分,其还具备JS压缩、混淆、请求合并(合并资源须要server端配合)等性能优化的工具,说其是现有JS模块化的鼻祖一点都不过度。
// YUI - 编写模块 YUI.add('dom', function(Y) { Y.DOM = { ... } }) // YUI - 使用模块 YUI().use('dom', function(Y) { Y.DOM.doSomeThing(); // use some methods DOM attach to Y }) // hello.js YUI.add('hello', function(Y){ Y.sayHello = function(msg){ Y.DOM.set(el, 'innerHTML', 'Hello!'); } },'3.0.0',{ requires:['dom'] }) // main.js YUI().use('hello', function(Y){ Y.sayHello("hey yui loader"); })
YUI的出现使人眼前一新,他提供了一种模块管理方式:经过YUI全局对象去管理不一样模块,全部模块都只是对象上的不一样属性,至关因而不一样程序运行在操做系统上。YUI的核心实现就是闭包,不过好景不长,具备里程碑式意义的模块化工具诞生了。
2009年Nodejs发布,其中Commonjs是做为Node中模块化规范以及原生模块面世的。Node中提出的Commonjs规范具备如下特色:
基本上Commonjs发布以后,就成了Node里面标准的模块化管理工具。同时Node还推出了npm包管理工具,npm平台上的包均知足Commonjs规范,随着Node与npm的发展,Commonjs影响力也愈来愈大,而且促进了后面模块化工具的发展,具备里程碑意义的模块化工具。以前的例子咱们这样改写:
a.js
// a.js var c = require('./c'); module.exports = { aStr: 'aa', aNum: c.cNum + 1 };
b.js
// b.js var a = require('./a'); exports.bStr = a.aStr + ' bb';
c.js
// c.js exports.cNum = 0;
入口文件就是 index.js
var a = require('./a'); var b = require('./b'); console.log(a.aNum, b.bStr);
能够直观的看到,使用Commonjs管理模块,十分方便。Commonjs优势在于:
这里补充一点沙箱编译:require进来的js模块会被Module模块注入一些变量,使用当即执行函数编译,看起来就好像:
(function (exports, require, module, __filename, __dirname) { //原始文件内容 })();
看起来require和module好像是全局对象,其实只是闭包中的入参,并非真正的全局对象。以前专门整理探究过 Node中的Module源码分析,也能够看看阮一峰老师的require()源码解读,或者廖雪峰老师的CommonJS规范。
Commonjs的诞生给js模块化发展有了重要的启发,Commonjs很是受欢迎,可是局限性很明显:Commonjs基于Node原生api在服务端能够实现模块同步加载,可是仅仅局限于服务端,客户端若是同步加载依赖的话时间消耗很是大,因此须要一个在客户端上基于Commonjs可是对于加载模块作改进的方案,因而AMD规范诞生了。
AMD是"Asynchronous Module Definition"的缩写,意思就是"异步模块定义"。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。全部依赖这个模块的语句,都定义在一个回调函数中,等到全部依赖加载完成以后(前置依赖),这个回调函数才会运行。
AMD与Commonjs同样都是js模块化规范,是一套抽象的约束,与2009年诞生。文档这里。该约束规定采用require语句加载模块,可是不一样于CommonJS,它要求两个参数:
require([module], callback);
第一个参数[module],是一个数组,里面的成员就是要加载的模块;第二个参数callback,则是加载成功以后的回调函数。若是将前面的代码改写成AMD形式,就是下面这样:
require(['math'], function (math) { math.add(2, 3); });
定义了一个文件,该文件依赖math模块,当math模块加载完毕以后执行回调函数,这里并无暴露任何变量。不一样于Commonjs,在定义模块的时候须要使用define函数定义:
define(id?, dependencies?, factory);
define方法与require相似,id是定义模块的名字,仍然会在全部依赖加载完毕以后执行factory。
RequireJs是js模块化的工具框架,是AMD规范的具体实现。可是有意思的是,RequireJs诞生以后,推广过程当中产生的AMD规范。文档这里。
RequireJs有两个最鲜明的特色:
<script>
引入依赖,在<script>
标签的onload事件监听文件加载完毕;一个模块的回调函数必须得等到全部依赖都加载完毕以后,才可执行,相似Promise.all。仍是上面那个例子:
配置文件main.js
requirejs.config({ shim: { // ... }, paths: { a: '/a.js', b: '/b.js', c: '/c.js', index: '/index.js' } }); require(['index'], function(index){ index(); });
a.js
define('a', ['c'], function(c){ return { aStr: 'aa', aNum: c.cNum + 1 } });
b.js
define('b', ['a'], function(a){ return { bStr = a.aStr + ' bb'; } });
c.js
define('c', function(){ return { cNum: 0 } });
index.js
define('index', ['a', 'b'], function(a, b){ return function(){ console.log(a.aNum, b.bStr); } });
页面中嵌入
<script src="/require.js" data-main="/main" async="async" defer></script>
RequireJs当年在国内很是受欢迎,主要是如下优势:
不过我的以为RequireJs配置仍是挺麻烦的,可是当年已经很是方便了。
一样是受到Commonjs的启发,国内(阿里)诞生了一个CMD(Common Module Definition)规范。该规范借鉴了Commonjs的规范与AMD规范,在二者基础上作了改进。
define(id?, dependencies?, factory);
与AMD相比很是相似,CMD规范(2011)具备如下特色:
SeaJs是CMD规范的实现,跟RequireJs相似,CMD也是SeaJs推广过程当中诞生的规范。CMD借鉴了不少AMD和Commonjs优势,一样SeaJs也对AMD和Commonjs作出了不少兼容。
SeaJs核心特色:
<script>
标签加载依赖。修改下上面那个例子:
a.js
console.log('a1'); define(function(require,exports,module){ console.log('inner a1'); require('./c.js') }); console.log('a2')
b.js
console.log('b1'); define(function(require,exports,module){ console.log('inner b1'); }); console.log('b2')
c.js
console.log('c1'); define(function(require,exports,module){ console.log('inner c1'); }); console.log('c2')
页面引入
<body> <script src="/sea.js"></script> <script> seajs.use(['./a.js','./b.js'],function(a,b){ console.log('index1'); }) </script> </body>
对于seaJs中的就近依赖,有必要单独说一下。来看一下上面例子中的log顺序:
依赖关系梳理完毕,开始动态script标签下载依赖,控制台输出:
a1 a2 b1 b2 c1 c2
inner a1
inner c1
inner b1
index
完整的顺序就是:
a1 a2 b1 b2 c1 c2 inner a1 inner c1 inner b1 index
这是一个能够很好理解SeaJs的例子。
以前的各类方法和框架,都出自于各个大公司或者社区,都是民间出台的结局方法。到了2015年,ES6规范中,终于将模块化归入JavaScript标准,今后js模块化被官方扶正,也是将来js的标准。
以前那个例子再用ES6的方式实现一次:
a.js
import {cNum} from './c'; export default { aStr: 'aa', aNum: cNum + 1 };
b.js
import {aStr} from './a'; export const bStr = aStr + ' bb';
c.js
export const bNum = 0;
index.js
import {aNum} from './a'; import {bStr} from './b'; console.log(aNum, bStr);
能够看到,ES6中的模块化在Commonjs的基础上有所不一样,增长了关键字import,export,default,as,from,而不是全局对象。另外深刻理解的话,有两点主要的区别:
一个经典的例子:
// counter.js exports.count = 0 setTimeout(function () { console.log('increase count to', ++exports.count, 'in counter.js after 500ms') }, 500) // commonjs.js const {count} = require('./counter') setTimeout(function () { console.log('read count after 1000ms in commonjs is', count) }, 1000) //es6.js import {count} from './counter' setTimeout(function () { console.log('read count after 1000ms in es6 is', count) }, 1000)
分别运行 commonjs.js 和 es6.js:
➜ test node commonjs.js increase count to 1 in counter.js after 500ms read count after 1000ms in commonjs is 0 ➜ test babel-node es6.js increase count to 1 in counter.js after 500ms read count after 1000ms in es6 is 1
这个例子解释了CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。ES6 模块的运行机制与 CommonJS 不同。JS 引擎对脚本静态分析的时候,遇到模块加载命令import
,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的import
有点像 Unix 系统的“符号链接”,原始值变了,import
加载的值也会跟着变。所以,ES6 模块是动态引用,而且不会缓存值,模块里面的变量绑定其所在的模块。
更多ES6模块化特色,参照阮一峰老师的ECMAScript 6 入门。
写了这么多,其实都是走马观花地从使用方式和运行原理分析了不一样方法的实现。如今从新看一下当时模块化的痛点:
不一样的模块化手段都在致力于解决这些问题。前两个问题其实很好解决,使用闭包配合当即执行函数,高级一点使用沙箱编译,缓存输出等等。
我以为真正的难点在于依赖关系梳理以及加载。Commonjs在服务端使用fs能够接近同步的读取文件,可是在浏览器中,无论是RequireJs仍是SeaJs,都是使用动态建立script标签方式加载,依赖所有加载完毕以后执行,省去了开发手动书写加载顺序这一烦恼。
到了ES6,官方出台设定标准,不在须要出框架或者hack的方式解决该问题,该项已经做为标准要求各浏览器实现,虽然如今浏览器所有实现该标准尚无时日,可是确定是将来趋势。