CommonJS是服务器模块的规范,Node.js采用了这个规范。html
根据 CommonJS
规范,一个单独的文件就是一个模块,每个模块都是一个单独的做用域,在一个文件定义的变量(还包括函数和类),都是私有的,对其余文件是不可见的。前端
CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操做。node
CommonJS
中,加载模块使用 require
方法。该方法读取一个文件并执行,最后返回文件内部的 exports
对象。jquery
Node.js
主要用于服务器编程,加载的模块文件通常都已经存在本地硬盘,加载起来较快,不用考虑异步加载的方式,因此CommonJS
的同步加载模块规范是比较适用的。webpack但若是是浏览器环境,要从服务器加载模块,这是就必须采用异步模式。因此就有了
AMD
,CMD
等解决方案。git
var x = 5; var addX = function(value) { return value + x; }; module.exports.x = x; module.exports.addX = addX; // 也能够改写为以下 module.exports = { x: x, addX: addX, }; 复制代码
let math = require('./math.js'); console.log('math.x',math.x); console.log('math.addX', math.addX(4)); 复制代码
AMD
= Asynchronous Module Definition
,即 异步模块定义。AMD
规范加载模块是异步的,并容许函数回调,没必要等到全部模块都加载完成,后续操做能够正常执行。AMD
中,使用 require
获取依赖模块,使用 exports
导出 API
。//规范 API define(id?, dependencies?, factory); define.amd = {}; // 定义无依赖的模块 define({ add: function(x,y){ return x + y; } }); // 定义有依赖的模块 define(["alpha"], function(alpha){ return { verb: function(){ return alpha.verb() + 1; } } }); 复制代码
require([module], callback) 中
callback
为模块加载完成后的回调函数程序员
//加载 math模块,完成以后执行回调函数 require(['math'], function(math) {  math.add(2, 3); }); 复制代码
RequireJS
是一个前端模块化管理的工具库,遵循 AMD
规范,RequireJS
是对 AMD
规范的阐述。es6
RequireJS
基本思想为,经过一个函数来将全部所需的或者所依赖的模块装载进来,而后返回一个新的函数(模块)。后续全部的关于新模块的业务代码都在这个函数内部操做。github
RequireJS
要求每一个模块均放在独立的文件之中,并使用 define
定义模块,使用 require
方法调用模块。web
按照是否有依赖其余模块状况,能够分为 独立模块 和 非独立模块。
define({ method1: function(){}, method2: function(){} }); //等价于 define(function() { return { method1: function(){}, method2: function(){} } }); 复制代码
define([ 'module1', 'module2' ], function(m1, m2) { ... }); //等价于 define(function(require) { var m1 = require('module1'); var m2 = require('module2'); ... }); 复制代码
require
方法调用模块require(['foo', 'bar'], function(foo, bar) { foo.func(); bar.func(); }); 复制代码
CMD
= Common Module Definition
,即 通用模块定义。CMD
是 SeaJS
在推广过程当中对模块定义的规范化产出。
CMD规范和AMD相似,都主要运行于浏览器端,写法上看起来也很相似。主要是区别在于 模块初始化时机
CMD
推崇依赖就近,AMD
推崇依赖前置。AMD
的 API
默认是一个当多个用,CMD
严格的区分推崇职责单一。例如,AMD
里 require
分全局的和局部的。CMD里面没有全局的 require
,提供 seajs.use()
来实现模块系统的加载启动。CMD
里每一个 API
都简单纯粹。//AMD define(['./a','./b'], function (a, b) { //依赖一开始就写好 a.test(); b.test(); }); //CMD define(function (requie, exports, module) { //依赖能够就近书写 var a = require('./a'); a.test(); ... //软依赖 if (status) { var b = requie('./b'); b.test(); } }); 复制代码
使用Sea.js,在书写文件时,须要遵照CMD(Common Module Definition)模块定义规范。一个文件就是一个模块。
exports
暴露接口。这意味着不须要命名空间了,更不须要全局变量。这是一种完全的命名冲突解决方案。require
引入依赖。这可让依赖内置,开发者只需关心当前模块的依赖,其余事情 Sea.js
都会自动处理好。对模块开发者来讲,这是一种很好的 关注度分离,能让程序员更多地享受编码的乐趣。define
定义模块,更多详情参考SeasJS | 极客学院。例如,对于下述util.js
代码
var org = {}; org.CoolSite = {}; org.CoolSite.Utils = {}; org.CoolSite.Utils.each = function (arr) { // 实现代码 }; org.CoolSite.Utils.log = function (str) { // 实现代码 }; 复制代码
能够采用SeaJS重写为
define(function(require, exports) { exports.each = function (arr) { // 实现代码 }; exports.log = function (str) { // 实现代码 }; }); 复制代码
经过 exports
就能够向外提供接口。经过 require('./util.js')
就能够拿到 util.js
中经过 exports
暴露的接口。这里的 require
能够认为是 Sea.js
给 JavaScript 语言增长的一个语法关键字,经过 require
能够获取其余模块提供的接口。
define(function(require, exports) { var util = require('./util.js'); exports.init = function() { // 实现代码 }; }); 复制代码
两者区别主要表如今模块初始化时机
AMD(RequireJS)中只要模块做为依赖时,就会加载并初始化。即尽早地执行(依赖)模块。至关于全部的require都被提早了,并且模块执行的顺序也不必定100%就是require书写顺序。
CMD(SeaJS)中,模块做为依赖且被引用时才会初始化,不然只会加载。即只会在模块真正须要使用的时候才初始化。模块加载的顺序是严格按照require书写的顺序。
从规范上来讲,AMD 更加简单且严谨,适用性更广,而在RequireJS强力的推进下,在国外几乎成了事实上的异步模块标准,各大类库也相继支持AMD规范。
但从SeaJS与CMD来讲,也作了不少不错东西:一、相对天然的依赖声明风格 二、小而美的内部实现 三、贴心的外围功能设计 四、更好的中文社区支持。
UMD
= Universal Module Definition
,即通用模块定义。UMD
是AMD
和 CommonJS
的糅合。
AMD
模块以浏览器第一的原则发展,异步加载模块。CommonJS
模块以服务器第一原则发展,选择同步加载。它的模块无需包装(unwrapped modules)。 这迫令人们又想出另外一个更通用的模式UMD
(Universal Module Definition),实现跨平台的解决方案。
UMD
先判断是否支持 Node.js
的模块(exports
)是否存在,存在则使用 Node.js
模块模式。再判断是否支持 AMD
(define
是否存在),存在则使用 AMD
方式加载模块。(function (window, factory) { if (typeof exports === 'object') { module.exports = factory(); } else if (typeof define === 'function' && define.amd) { define(factory); } else { window.eventUtil = factory(); } })(this, function () { //module ... }); 复制代码
CommonJS
输出的是值的拷贝。CommonJS
模块是运行时加载,ES6 模块是编译时输出接口。CommonJS 模块输出的是值的拷贝(类比于基本类型和引用类型的赋值操做)。对于基本类型,一旦输出,模块内部的变化影响不到这个值。对于引用类型,效果同引用类型的赋值操做。
// lib.js var counter = 3; var obj = { name: 'David' }; function changeValue() { counter++; obj.name = 'Peter'; }; module.exports = { counter: counter, obj: obj, changeValue: changeValue, }; 复制代码
// main.js var mod = require('./lib'); console.log(mod.counter); // 3 console.log(mod.obj.name); // 'David' mod.changeValue(); console.log(mod.counter); // 3 console.log(mod.obj.name); // 'Peter' // Or console.log(require('./lib').counter); // 3 console.log(require('./lib').obj.name); // 'Peter' 复制代码
counter
是基本类型值,模块内部值的变化不影响输出的值变化。obj
是引用类型值,模块内部值的变化影响输出的值变化。也能够借助取值函数(getter
),将 counter
转为引用类型值,效果以下。
在类的内部,可使用
get
和set
关键字,对某个属性设置存执函数和取值函数,拦截该属性的存取行为。 —— class | 阮一峰
// lib.js var counter = 3; function incCounter() { counter++; } module.exports = { get counter() { return counter }, incCounter: incCounter, }; 复制代码
// main.js var mod = require('./lib'); console.log(mod.counter); // 3 mod.incCounter(); console.log(mod.counter); // 4 复制代码
ES6 模块是动态关联模块中的值,输出的是值得引用。原始值变了,import
加载的值也会跟着变。
ES6
模块的运行机制与CommonJS
不同。JS 引擎对脚本静态分析时,遇到模块加载命令import
,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。ES6 模块中,原始值变了,import
加载的值也会跟着变。所以,ES6 模块是动态引用,而且不会缓存值。 —— ES6 Module 的加载实现 | 阮一峰
// lib.js export let counter = 3; export function incCounter() { counter++; } // main.js import { counter, incCounter } from './lib'; console.log(counter); // 3 incCounter(); console.log(counter); // 4 复制代码
CommonJS
模块是运行时加载,ES6 模块是编译时输出接口。
这是由于,CommonJS
加载的是一个对象(即 module.exports
属性),该对象只有在脚本运行完才会生成。而 ES6
模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
ES6 模块是编译时输出接口,所以有以下2个特色
import
命令会被 JS 引擎静态分析,优先于模块内的其余内容执行export
命令会有变量声明提高的效果在文件中的任何位置引入 import
模块都会被提早到文件顶部
// a.js console.log('a.js') import { foo } from './b'; // b.js export let foo = 1; console.log('b.js 先执行'); // 执行结果: // b.js 先执行 // a.js 复制代码
虽然 a
模块中 import
引入晚于 console.log('a')
,可是它被 JS 引擎经过静态分析,提到模块执行的最前面,优于模块中的其余部分的执行。
因为 import
和 export
是静态执行,因此 import
和 export
具备变量提高效果。即 import
和 export
命令在模块中的位置并不影响程序的输出。
// a.js import { foo } from './b'; console.log('a.js'); export const bar = 1; export const bar2 = () => { console.log('bar2'); } export function bar3() { console.log('bar3'); } // b.js export let foo = 1; import * as a from './a'; console.log(a); // 执行结果: // { bar: undefined, bar2: undefined, bar3: [Function: bar3] } // a.js 复制代码
a
模块引用了 b
模块,b
模块也引用了 a
模块,export
声明的变量也是优于模块其它内容的执行的。但具体对变量赋值须要等到执行到相应代码的时候。
重复引入某个相同的模块时,模块只会执行一次。
CommonJS 模块的重要特性是加载时执行,即脚本代码在 require
的时候,就会所有执行。一旦出现某个模块被“循环加载”,就只输出已经执行的部分,还未执行的部分不会输出。
//a.js exports.done = false; var b = require('./b.js'); console.log('在 a.js 之中,b.done = %j', b.done); exports.done = true; console.log('a.js 执行完毕'); 复制代码
上面代码之中,a.js
脚本先输出一个 done
变量,而后加载另外一个脚本文件 b.js
。注意,此时 a.js
代码就停在这里,等待 b.js
执行完毕,再往下执行。
再看 b.js
的代码。
//b.js exports.done = false; var a = require('./a.js'); console.log('在 b.js 之中,a.done = %j', a.done); exports.done = true; console.log('b.js 执行完毕'); 复制代码
上面代码之中,b.js
执行到第二行,就会去加载 a.js
,这时,就发生了“循环加载”。系统会 a.js
模块对应对象的 exports
属性取值,但是由于 a.js
尚未执行完,从 exports
属性只能取回已经执行的部分,而不是最后的值。
a.js
已经执行的部分,只有一行。
exports.done = false; 复制代码
所以,对于 b.js
来讲,它从 a.js
只输入一个变量 done
,值为 false
。
而后,b.js
接着往下执行,等到所有执行完毕,再把执行权交还给 a.js
。因而,a.js
接着往下执行,直到执行完毕。咱们写一个脚本 main.js
,验证这个过程。
var a = require('./a.js'); var b = require('./b.js'); console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done); 复制代码
执行 main.js
,运行结果以下。
$ node main.js 在 b.js 之中,a.done = false b.js 执行完毕 在 a.js 之中,b.done = true a.js 执行完毕 在 main.js 之中, a.done=true, b.done=true 复制代码
上面的代码证实了2点
b.js
之中,a.js
没有执行完毕,只执行了第一行main.js
执行到第二行时,不会再次执行 b.js
,而是输出缓存的 b.js
的执行结果,即它的第四行。exports.done = true; 复制代码
总之,CommonJS 输入的是被输出值的拷贝,不是引用。
另外,因为 CommonJS 模块遇到循环加载时,返回的是当前已经执行的部分的值,而不是代码所有执行后的值,二者可能会有差别。因此,输入变量的时候,必须很是当心。
var a = require('a'); // 安全的写法 导入总体,保证module已经执行完成 var foo = require('a').foo; // 危险的写法 exports.good = function (arg) { return a.foo('good', arg); // 使用的是 a.foo 的最新值 }; exports.bad = function (arg) { return foo('bad', arg); // 使用的是一个部分加载时的值 }; 复制代码
上面代码中,若是发生循环加载,require('a').foo
的值极可能后面会被改写,改用 require('a')
会更保险一点。
// a.js console.log('a starting'); exports.done = false; const b = require('./b'); console.log('in a, b.done =', b.done); exports.done = true; console.log('a done'); // b.js console.log('b starting'); exports.done = false; const a = require('./a'); console.log('in b, a.done =', a.done); exports.done = true; console.log('b done'); // node a.js // 执行结果: // a starting // b starting // in b, a.done = false // b done // in a, b.done = true // a done 复制代码
从上面的执行过程当中,能够看到,在 CommonJS 规范中,当遇到 require()
语句时,会执行 require
模块中的代码,并缓存执行的结果,当下次再次加载时不会重复执行,而是直接取缓存的结果。正由于此,出现循环依赖时才不会出现无限循环调用的状况。
跟 CommonJS 模块同样,ES6 不会再去执行重复加载的模块,又因为 ES6 动态输出绑定的特性,能保证 ES6 在任什么时候候都能获取其它模块当前的最新值。
ES6 模块在编译时就会静态分析,优先于模块内的其余内容执行,因此致使了咱们没法写出像下面这样的代码
if(some condition) { import a from './a'; }else { import b from './b'; } // or import a from (str + 'b'); 复制代码
由于编译时静态分析,致使了咱们没法在条件语句或者拼接字符串模块,由于这些都是须要在运行时才能肯定的结果在 ES6 模块是不被容许的,因此 动态引入import()
应运而生。
import()
容许你在运行时动态地引入 ES6 模块,想到这,你可能也想起了 require.ensure
这个语法,可是它们的用途却大相径庭的。
require.ensure
的出现是 webpack
的产物,它是由于浏览器须要一种异步的机制能够用来异步加载模块,从而减小初始的加载文件的体积,因此若是在服务端的话, require.ensure
就无用武之地了,由于服务端不存在异步加载模块的状况,模块同步进行加载就能够知足使用场景了。 CommonJS 模块能够在运行时确认模块加载。
而 import()
则不一样,它主要是为了解决 ES6 模块没法在运行时肯定模块的引用关系,因此须要引入 import()
。
先来看下它的用法
import()
提供一个基于 Promise
的 API
import()
能够在脚本的任何地方使用 import()
接受字符串文字,能够根据须要构造说明符// a.js const str = './b'; const flag = true; if(flag) { import('./b').then(({foo}) => { console.log(foo); }) } import(str).then(({foo}) => { console.log(foo); }) // b.js export const foo = 'foo'; // babel-node a.js // 执行结果 // foo // foo 复制代码
固然,若是在浏览器端的 import()
的用途就会变得更普遍,好比 按需异步加载模块,那么就和 require.ensure
功能相似了。
由于是基于 Promise
的,因此若是你想要同时加载多个模块的话,能够是 Promise.all
进行并行异步加载。
Promise.all([ import('./a.js'), import('./b.js'), import('./c.js'), ]).then(([a, {default: b}, {c}]) => { console.log('a.js is loaded dynamically'); console.log('b.js is loaded dynamically'); console.log('c.js is loaded dynamically'); }); 复制代码
还有 Promise.race
方法,它检查哪一个 Promise
被首先 resolved
或 reject
。咱们可使用 import()
来检查哪一个 CDN
速度更快:
const CDNs = [ { name: 'jQuery.com', url: 'https://code.jquery.com/jquery-3.1.1.min.js' }, { name: 'googleapis.com', url: 'https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js' } ]; console.log(`------`); console.log(`jQuery is: ${window.jQuery}`); Promise.race([ import(CDNs[0].url).then(()=>console.log(CDNs[0].name, 'loaded')), import(CDNs[1].url).then(()=>console.log(CDNs[1].name, 'loaded')) ]).then(()=> { console.log(`jQuery version: ${window.jQuery.fn.jquery}`); }); 复制代码
固然,若是你以为这样写还不够优雅,也能够结合 async/await
语法糖来使用。
async function main() { const myModule = await import('./myModule.js'); const {export1, export2} = await import('./myModule.js'); const [module1, module2, module3] = await Promise.all([ import('./module1.js'), import('./module2.js'), import('./module3.js'), ]); } 复制代码
动态 import()
为咱们提供了以异步方式使用 ES 模块的额外功能。
根据咱们的需求动态或有条件地加载它们,这使咱们可以更快,更好地建立更多优点应用程序。
Webpack容许使用不一样的模块类型,可是底层
必须使用同一种实现。全部的模块能够直接在盒外运行。
import MyModule from './MyModule.js'; 复制代码
var MyModule = require('./MyModule.js'); 复制代码
define(['./MyModule.js'], function (MyModule) { }); 复制代码