关键词: AMD、CMD、UMD、CommonJS、ES Modulejavascript
规范JavaScript的模块定义和加载机制,下降学习和使用各类框架的门槛,可以以一种统一的方式去定义和使用模块,提升开发效率,下降了应用维护成本。html
目录:前端
想当初,Brendan Eich 只用了十天就创造了 JavaScript 这门语言,谁曾想这门一直被看做玩具性质的语言在近几年得到了爆发性地发展,从浏览器端扩展到服务器,再到 native 端,变得愈来愈火热。而这门语言创造当初的诸多限制也在前端工程化的今天被放大,社区也在积极推进其变革。实现模块化的开发正是其中最大的需求,本文梳理 JavaScript 模块化开发的历史和将来,以做学习之用。java
JavaScript 模块化的发展历程,是以 2009 年 CommonJS 的出现为分水岭,这一规范极大地推进前端发展。在1999年至2009年期间,模块化探索都是基于语言层面的优化,2009 年后前端开始大量使用预编译。node
在 1999 年的时候,那会尚未全职的前端工程师,写 JS 是直接将变量定义在全局,作的好一些的或许会作一些文件目录规划,将资源归类整理,这种方式被称为直接定义依赖,举个例子:jquery
// greeting.js var helloInLang = { en: 'Hello world!', es: '¡Hola mundo!', ru: 'Привет мир!' }; function writeHello(lang) { document.write(helloInLang[lang]); } // third_party_script.js function writeHello() { document.write('The script is broken'); } 复制代码
// index.html <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Basic example</title> <script src="./greeting.js"></script> <script src="./third_party_script.js"></script> </head> 复制代码
可是,即便有规范的目录结构,也不能避免由此而产生的大量全局变量,这就致使了一不当心就会有变量冲突的问题,就比如上面这个例子中的 writeHello
。webpack
因而在 2002 年左右,有人提出了命名空间模式的思路,用于解决遍地的全局变量,将须要定义的部分归属到一个对象的属性上,简单修改上面的例子,就能实现这种模式:git
// greeting.js var app = {}; app.helloInLang = { en: 'Hello world!', es: '¡Hola mundo!', ru: 'Привет мир!' }; app.writeHello = function (lang) { document.write(helloInLang[lang]); } // third_party_script.js function writeHello() { document.write('The script is broken'); } 复制代码
不过这种方式,毫无隐私可言,本质上就是全局对象,谁均可以来访问而且操做,一点都不安全。es6
2003 年左右就有人提出利用 IIFE 结合 Closures 特性,以此解决私有变量的问题,这种模式被称为闭包模块化模式:github
// greeting.js var greeting = (function() { var module = {}; var helloInLang = { en: 'Hello world!', es: '¡Hola mundo!', ru: 'Привет мир!', }; module.getHello = function(lang) { return helloInLang[lang]; }; module.writeHello = function(lang) { document.write(module.getHello(lang)); }; return module; })(); 复制代码
IIFE 能够造成一个独立的做用域,其中声明的变量,仅在该做用域下,从而达到实现私有变量的目的,就如上面例子中的 helloInLang
,在该 IIFE 外是不能直接访问和操做的,能够经过暴露一些方法来访问和操做,好比说上面例子里面的 getHello
和 writeHello2
个方法,这就是所谓的 Closures。
同时,不一样模块之间的引用也能够经过参数的形式来传递:
// x.js // @require greeting.js var x = (function(greeting) { var module = {}; module.writeHello = function(lang) { document.write(greeting.getHello(lang)); }; return module; })(greeting); 复制代码
此外使用 IIFE,还有2个好处:
除了这些方式,还有其余的如模版依赖定义、注释依赖定义、外部依赖定义,不是很常见,但其本质都是想在语言层面解决模块化的问题。
不过,这些方案,虽然解决了依赖关系的问题,可是没有解决如何管理这些模块,或者说在使用时清晰描述出依赖关系,这点仍是没有被解决,能够说是少了一个管理者。
没有管理者的时候,在实际项目中,得手动管理第三方的库和项目封装的模块,就像下面这样把全部须要的 JS 文件一个个按照依赖的顺序加载进来:
<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="deferred.js"></script> <script src="util/wxbridge.js"></script> <script src="util/login.js"></script> <script src="util/base.js"></script> <script src="util/city.js"></script> 复制代码
对于这个问题,社区出现了新的工具,如 LABjs、YUI。YUI 做为昔日前端领域的佼佼者,很好的糅合了命名空间模式及沙箱模式,如如下的例子:
// 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 团队还提供的一系列用于 JS 压缩、混淆、请求合并(合并资源须要 server 端配合)等性能优化的工具,说其是现有 JS 模块化的鼻祖一点都不过度。
不过随着 Node.js 的到来,新出的 CommonJS 规范的落地,以及各类前端工具、解决方案的出现,才真正使得前端开发大放光芒。
CommonJS 的出现真正使得前端进入工业化时代。前面说了,2009 年之前的各类模块化方案虽然始终停留在语言层面上,虽然也有 YUI 这样的工具,但还不足以成为引领潮流的工具。究其缘由,仍是由于前端工程复杂度还没积累到必定程度,随着 Node.js 的出现,JS 涉足的领域转向后端,加上 Web app 变得愈来愈复杂,工程发展到必定阶段,要出现的必然会出现。
CommonJS 是一套同步的方案,它考虑的是在服务端运行的Node.js,主要是经过 require
来加载依赖项,经过 exports
或者 module.exports
来暴露接口或者数据的方式。
因为在服务端能够直接读取磁盘上的文件,因此能作到同步加载资源,但在浏览器上是经过 HTTP 方式获取资源,复杂的网络状况下没法作到同步,这就致使必须使用异步加载机制。这里发展出两个有影响力的方案:
它们分别在浏览器实现了define
、require
及module
的核心功能,虽然二者的目标是一致的,可是实现的方式或者说是思路,仍是有些区别的,AMD 偏向于依赖前置,CMD 偏向于用到时才运行的思路,从而致使了依赖项的加载和运行时间点会不一样。
// CMD define(function (require) { var a = require('./a'); // <- 运行到此处才开始加载并运行模块a var b = require('./b'); // <- 运行到此处才开始加载并运行模块b // more code .. }) 复制代码
// AMD define( ['./a', './b'], // <- 前置声明,也就是在主体运行前就已经加载并运行了模块a和模块b function (a, b) { // more code .. } ) 复制代码
这里也有很多争议的地方,在于 CommonJS 社区认为 AMD 模式破坏了规范,反观 CMD 模式,简单的去除 define
的外包装,这就是标准的 CommonJS 实现,因此说 CMD 是最贴近 CommonJS 的异步模块化方案。不过 AMD 的社区资源比 CMD 更丰富,这也是 AMD 更加流行的一个缘由。
此外同一时期还出现了一个 UMD 的方案,其实它就是 AMD 与 CommonJS 的集合体,经过 IIFE 的前置条件判断,使一个模块既能够在浏览器运行,也能够在 Node.js 中运行,举个例子:
// UMD (function(define) { define(function () { var helloInLang = { en: 'Hello world!', es: '¡Hola mundo!', ru: 'Привет мир!' }; return { sayHello: function (lang) { return helloInLang[lang]; } }; }); }( typeof module === 'object' && module.exports && typeof define !== 'function' ? function (factory) { module.exports = factory(); } : define )); 复制代码
不过这个用的比较少,仅做了解。
2015年6月,ECMAScript2015 发布了,JavaScript 终于在语言标准的层面上,实现了模块功能,使得在编译时就能肯定模块的依赖关系,以及其输入和输出的变量,不像 CommonJS、AMD 之类的须要在运行时才能肯定,成为浏览器和服务器通用的模块解决方案。
// lib/greeting.js const helloInLang = { en: 'Hello world!', es: '¡Hola mundo!', ru: 'Привет мир!' }; export const getHello = (lang) => ( helloInLang[lang]; ); export const sayHello = (lang) => { console.log(getHello(lang)); }; // hello.js import { sayHello } from './lib/greeting'; sayHello('ru'); 复制代码
与 CommonJS 用 require()
方法加载模块不一样,在 ES Module 中,import
命令能够具体指定加载模块中用 export
命令暴露的接口(不指定具体的接口,默认加载 export default
),没有指定的是不会加载的,所以会在编译时就完成模块的加载,这种加载方式称为编译时加载或者静态加载。
而 CommonJS 的 require()
方法是在运行时才加载的:
// lib/greeting.js const helloInLang = { en: 'Hello world!', es: '¡Hola mundo!', ru: 'Привет мир!' }; const getHello = function (lang) { return helloInLang[lang]; }; exports.getHello = getHello; exports.sayHello = function (lang) { console.log(getHello(lang)) }; // hello.js const sayHello = require('./lib/greeting').sayHello; sayHello('ru'); 复制代码
能够看出,CommonJS 中是将整个模块做为一个对象引入,而后再获取这个对象上的某个属性。
所以 ES Module 的编译时加载,在效率上面会提升很多,此外,还会带来一些其它的好处,好比引入宏(macro)和类型检验(type system)这些只能靠静态分析实现的功能。
不过因为 ES Module 在低版本的 Node.js 和浏览器上支持度有待增强,因此通常仍是经过 Babel 进行转换成 es5 的语法,兼容更多的平台。
Node 应用由模块组成,采用 CommonJS 模块规范。
每一个文件就是一个模块,有本身的做用域。在一个文件里面定义的变量、函数、类,都是私有的,对其余文件不可见。
CommonJS 规范规定,每一个模块内部,module
变量表明当前模块。这个变量是一个对象,它的 exports
属性(即 module.exports
)是对外的接口。加载某个模块,实际上是加载该模块的 module.exports
属性。
var x = 5; var addX = function (value) { return value + x; }; module.exports.x = x; module.exports.addX = addX; 复制代码
require
方法用于加载模块。
var example = require('./example.js'); console.log(example.x); // 5 console.log(example.addX(1)); // 6 复制代码
Node 内部提供一个 Module
构建函数。全部模块都是 Module
的实例。
function Module(id, parent) { this.id = id; this.exports = {}; this.parent = parent; // ... } 复制代码
每一个模块内部,都有一个 module
对象,表明当前模块。它有如下属性:
module.id
模块的识别符,一般是带有绝对路径的模块文件名。module.filename
模块的文件名,带有绝对路径。module.loaded
返回一个布尔值,表示模块是否已经完成加载。module.parent
返回一个对象,表示调用该模块的模块。module.children
返回一个数组,表示该模块要用到的其余模块。module.exports
表示模块对外输出的值module.exports
属性表示当前模块对外输出的接口,其余文件加载该模块,实际上就是读取 module.exports
变量。
为了方便,Node 为每一个模块提供一个 exports
变量,指向 module.exports
。这等同在每一个模块头部,有一行这样的命令:
var exports = module.exports; 复制代码
形成的结果是,在对外输出模块接口时,能够向 exports
对象添加方法。
exports.area = function (r) { return Math.PI * r * r; }; exports.circumference = function (r) { return 2 * Math.PI * r; }; 复制代码
注意,不能直接将 exports
变量指向一个值,由于这样等于切断了 exports
与 module.exports
的联系。
// 无效代码 exports.hello = function() { return 'hello'; }; module.exports = 'Hello world'; 复制代码
上面代码中,hello
函数是没法对外输出的,由于 module.exports
被从新赋值了。
这意味着,若是一个模块的对外接口,就是一个单一的值,不能使用 exports
输出,只能使用 module.exports
输出。
module.exports = function (x){ console.log(x);}; 复制代码
一般,咱们会把相关的文件会放在一个目录里面,便于组织。这时,最好为该目录设置一个入口文件,让 require
方法能够经过这个入口文件,加载整个目录。
在目录中放置一个 package.json
文件,而且将入口文件写入 main
字段。下面是一个例子。
// package.json { "name" : "some-library", "main" : "./lib/some-library.js" } 复制代码
require
发现参数字符串指向一个目录之后,会自动查看该目录的 package.json
文件,而后加载 main
字段指定的入口文件。若是 package.json
文件没有 main
字段,或者根本就没有 package.json
文件,则会加载该目录下的 index.js
文件或 index.node
文件。
第一次加载某个模块时,Node会缓存该模块。之后再加载该模块,就直接从缓存取出该模块的 module.exports
属性。
require('./example.js'); require('./example.js').message = "hello"; require('./example.js').message // "hello" 复制代码
上面代码中,连续三次使用 require
命令,加载同一个模块。第二次加载的时候,为输出的对象添加了一个 message
属性。可是第三次加载的时候,这个 message
属性依然存在,这就证实 require
命令并无从新加载模块文件,而是输出了缓存。
若是想要屡次执行某个模块,可让该模块输出一个函数,而后每次 require
这个模块的时候,从新执行一下输出的函数。
全部缓存的模块保存在 require.cache
之中,若是想删除模块的缓存,能够像下面这样写。
// 删除指定模块的缓存 delete require.cache[moduleName]; // 删除全部模块的缓存 Object.keys(require.cache).forEach(function(key) { delete require.cache[key]; }) 复制代码
注意,缓存是根据绝对路径识别模块的,若是一样的模块名,可是保存在不一样的路径,require
命令仍是会从新加载该模块。
CommonJS 模块的加载机制是,输入的是被输出的值的拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。请看下面这个例子。
下面是一个模块文件lib.js
。
// lib.js var counter = 3; function incCounter() { counter++; } module.exports = { counter: counter, incCounter: incCounter, }; 复制代码
上面代码输出内部变量 counter
和改写这个变量的内部方法 incCounter
。
而后,加载上面的模块。
// main.js var counter = require('./lib').counter; var incCounter = require('./lib').incCounter; console.log(counter); // 3 incCounter(); console.log(counter); // 3 复制代码
上面代码说明,counter
输出之后,lib.js
模块内部的变化就影响不到 counter
了。
AMD 全称为 Asynchromous Module Definition(异步模块定义)。 AMD 是 RequireJS 在推广过程当中对模块定义的规范化产出,它是一个在浏览器端模块化开发的规范。 AMD 模式能够用于浏览器环境而且容许异步加载模块,同时又能保证正确的顺序,也能够按需动态加载模块。
模块经过 define
函数定义在闭包中,格式以下:
define(id?: String, dependencies?: String[], factory: Function|Object); 复制代码
id
是模块的名字,它是可选的参数。
dependencies
指定了所要依赖的模块列表,它是一个数组,也是可选的参数,每一个依赖的模块的输出将做为参数一次传入 factory
中。若是没有指定 dependencies
,那么它的默认值是 ["require", "exports", "module"]
:
define(function(require, exports, module) {}) 复制代码
factory
是最后一个参数,它包裹了模块的具体实现,它是一个函数或者对象。若是是函数,那么它的返回值就是模块的输出接口或值。
用例:
定义一个名为 myModule 的模块,它依赖 jQuery 模块:
// 定义 define('myModule', ['jquery'], function($) { // $ 是 jquery 模块的输出 $('body').text('hello world'); }); // 使用 require(['myModule'], function(myModule) {}); 复制代码
定义一个没有 id 值的匿名模块,一般做为应用的启动函数:
define(['jquery'], function($) { $('body').text('hello world'); }); 复制代码
依赖多个模块的定义:
define(['jquery', './math.js'], function($, math) { // $ 和 math 一次传入 factory $('body').text('hello world'); }); 复制代码
模块输出:
define(['jquery'], function($) { var HelloWorldize = function(selector){ $(selector).text('hello world'); }; // HelloWorldize 是该模块输出的对外接口 return HelloWorldize; }); 复制代码
在模块定义内部引用依赖:
define(function(require) { var $ = require('jquery'); $('body').text('hello world'); }); 复制代码
RequireJS 能够看做是对 AMD 规范的具体实现,它的用法和上节所展现的有所区别。
下载地址:requirejs.org/docs/downlo…
下面简单介绍一下其用法:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>requirejs test</title> </head> <body> <div id="messageBox"></div> <button id="btn" type="button" name="button">点击</button> <script data-main="js/script/main.js" src="js/lib/require.js"></script> </body> </html> 复制代码
这里的 script
标签,除了指定 RequireJS 路径外,还有个 data-main
属性,这属性指定在加载完 RequireJS 后,就用 RequireJS 加载该属性值指定路径下的 JS 文件并运行,因此通常该 JS 文件称为主 JS 文件(其 .js 后缀能够省略)。
// 配置文件 require.config({ baseUrl: 'js', paths: { jquery: 'lib/jquery-1.11.1', } }); // 加载模块 require(['jquery', 'script/hello'],function ($, hello) { $("#btn").click(function(){ hello.showMessage("test"); }); }); 复制代码
// 定义模块 define(['jquery'],function($){ //变量定义区 var moduleName = "hello module"; var moduleVersion = "1.0"; //函数定义区 var showMessage = function(name){ if(undefined === name){ return; }else{ $('#messageBox').html('欢迎访问 ' + name); } }; //暴露(返回)本模块API return { "moduleName":moduleName, "version": moduleVersion, "showMessage": showMessage } }); 复制代码
咱们经过 define
方法定义一个 js 模块,并经过 return
对外暴露出接口(两个属性,一个方法)。同时该模块也是依赖于 jQuery。
RequireJS 支持使用 require.config
来配置项目,具体 API 使用方法见官网文档或网上资料,这里只作基本介绍。
在前端的模块化发展上,还有另外一种与 AMD 相提并论的规范,这就是 CMD:
CMD 即 Common Module Definition 通用模块定义。 CMD 是 SeaJS 在推广过程当中对模块定义的规范化产出。 CMD 规范的前身是 Modules/Wrappings 规范。
在 CMD 规范中,一个模块就是一个文件。代码的书写格式以下:
define(factory);
复制代码
Function
define 是一个全局函数,用来定义模块。
define(factory)
define
接受 factory
参数,factory
能够是一个函数,也能够是一个对象或字符串。
factory
为对象、字符串时,表示模块的接口就是该对象、字符串。好比能够以下定义一个 JSON 数据模块:
define({ "foo": "bar" }); 复制代码
也能够经过字符串定义模板模块:
define('I am a template. My name is {{name}}.'); 复制代码
factory
为函数时,表示是模块的构造方法。执行该构造方法,能够获得模块向外提供的接口。factory
方法在执行时,默认会传入三个参数:require
、exports
和 module
:
define(function(require, exports, module) { // 模块代码 }); 复制代码
define(id?, deps?, factory)
define
也能够接受两个以上参数。字符串 id
表示模块标识,数组 deps
是模块依赖。好比:
define('hello', ['jquery'], function(require, exports, module) { // 模块代码 }); 复制代码
id
和 deps
参数能够省略。省略时,能够经过构建工具自动生成。
注意:带 id
和 deps 参数的 define
用法不属于 CMD 规范,而属于 Modules/Transport 规范。
define.cmd
一个空对象,可用来断定当前页面是否有 CMD 模块加载器:
if (typeof define === "function" && define.cmd) { // 有 Sea.js 等 CMD 模块加载器存在 } 复制代码
Function
require
是 factory
函数的第一个参数。
require(id)
require
是一个方法,接受模块标识做为惟一参数,用来获取其余模块提供的接口。
define(function(require, exports) { // 获取模块 a 的接口 var a = require('./a'); // 调用模块 a 的方法 a.doSomething(); }); 复制代码
require.async(id, callback?)
require.async
方法用来在模块内部异步加载模块,并在加载完成后执行指定回调。callback
参数可选。
define(function(require, exports, module) { // 异步加载一个模块,在加载完成时,执行回调 require.async('./b', function(b) { b.doSomething(); }); // 异步加载多个模块,在加载完成时,执行回调 require.async(['./c', './d'], function(c, d) { c.doSomething(); d.doSomething(); }); }); 复制代码
注意:require
是同步往下执行,require.async
则是异步回调执行。require.async
通常用来加载可延迟异步加载的模块。
require.resolve(id)
使用模块系统内部的路径解析机制来解析并返回模块路径。该函数不会加载模块,只返回解析后的绝对路径。
define(function(require, exports) { console.log(require.resolve('./b')); // ==> http://example.com/path/to/b.js }); 复制代码
这能够用来获取模块路径,通常用在插件环境或需动态拼接模块路径的场景下。
Object
exports
是一个对象,用来向外提供模块接口。
define(function(require, exports) { // 对外提供 foo 属性 exports.foo = 'bar'; // 对外提供 doSomething 方法 exports.doSomething = function() {}; }); 复制代码
除了给 exports
对象增长成员,还可使用 return
直接向外提供接口。
define(function(require) { // 经过 return 直接提供接口 return { foo: 'bar', doSomething: function() {} }; }); 复制代码
若是 return
语句是模块中的惟一代码,还可简化为:
define({ foo: 'bar', doSomething: function() {} }); 复制代码
特别注意
:下面这种写法是错误的!
define(function(require, exports) { // 错误用法!!! exports = { foo: 'bar', doSomething: function() {} }; }); 复制代码
正确的写法是用 return
或者给 module.exports
赋值:
define(function(require, exports, module) { // 正确写法 module.exports = { foo: 'bar', doSomething: function() {} }; }); 复制代码
提示:exports
仅仅是 module.exports
的一个引用。在 factory
内部给 exports
从新赋值时,并不会改变 module.exports
的值。所以给 exports
赋值是无效的,不能用来更改模块接口。
Object
module
是一个对象,上面存储了与当前模块相关联的一些属性和方法。
module.id String
模块的惟一标识。
define('id', [], function(require, exports, module) { // 模块代码 }); 复制代码
上面代码中,define
的第一个参数就是模块标识。
module.uri String
根据模块系统的路径解析规则获得的模块绝对路径。
define(function(require, exports, module) { console.log(module.uri); // ==> http://example.com/path/to/this/file.js }); 复制代码
通常状况下(没有在 define
中手写 id
参数时),module.id
的值就是 module.uri
,二者彻底相同。
module.dependencies Array
dependencies
是一个数组,表示当前模块的依赖。
module.exports Object
当前模块对外提供的接口。
传给 factory
构造方法的 exports
参数是 module.exports
对象的一个引用。只经过 exports
参数来提供接口,有时没法知足开发者的全部需求。 好比当模块的接口是某个类的实例时,须要经过 module.exports
来实现:
define(function(require, exports, module) { // exports 是 module.exports 的一个引用 console.log(module.exports === exports); // true // 从新给 module.exports 赋值 module.exports = new SomeClass(); // exports 再也不等于 module.exports console.log(module.exports === exports); // false }); 复制代码
注意:对 module.exports
的赋值须要同步执行,不能放在回调函数里。下面这样是不行的:
// x.js define(function(require, exports, module) { // 错误用法 setTimeout(function() { module.exports = { a: "hello" }; }, 0); }); 复制代码
文档地址:Sea.js - A Module Loader for the Web
简单入手:
<!DOCTYPE html> <html> <head> <script type="text/javascript" src="sea.js"></script> <script type="text/javascript"> // seajs 的简单配置 seajs.config({ base: "../sea-modules/", alias: { "jquery": "jquery/jquery/1.10.1/jquery.js" } }) // 加载入口模块 seajs.use("../static/hello/src/main") </script> </head> <body> </body> </html> 复制代码
// 全部模块都经过 define 来定义 define(function(require, exports, module) { // 经过 require 引入依赖 var $ = require('jquery'); var Spinning = require('./spinning'); // 经过 exports 对外提供接口 exports.doSomething = ... // 或者经过 module.exports 提供整个接口 module.exports = ... }); 复制代码
特色:兼容 AMD 和 CommonJS 规范的同时,还兼容全局引用的方式
常规写法:
(function (root, factory) { if (typeof define === 'function' && define.amd) { //AMD define(['jquery'], factory); } else if (typeof exports === 'object') { //Node, CommonJS之类的 module.exports = factory(require('jquery')); } else { //浏览器全局变量(root 即 window) root.returnExports = factory(root.jQuery); } }(this, function ($) { //方法 function myFunc(){}; //暴露公共方法 return myFunc; })); 复制代码
在 ES Module 以前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。ES Module 在语言标准的层面上,实现了模块功能,并且实现得至关简单,彻底能够取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。
ES Module 的设计思想是尽可能的静态化,使得编译时就能肯定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时肯定这些东西。
CommonJS 和 AMD 模块,其本质是在运行时生成一个对象进行导出,称为“运行时加载”,无法进行“编译优化”,而 ES Module 不是对象,而是经过 export
命令显式指定输出的代码,再经过 import
命令输入。这称为“编译时加载”或者静态加载,即 ES Module 能够在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。固然,这也致使了无法引用 ES Module 模块自己,由于它不是对象。
因为 ES Module 是编译时加载,使得静态分析成为可能。有了它,就能进一步拓宽 JavaScript 的语法,好比引入宏(macro)和类型检验(type system)这些只能靠静态分析实现的功能。
除了静态加载带来的各类好处,ES Module 还有如下好处:
import
只能写在顶层,由于是静态语法export
只支持导出接口,能够看做对象形式,值没法被当成接口,因此是错误的。/*错误的写法*/ // 写法一 export 1; // 写法二 var m = 1; export m; /*正确的四种写法*/ // 写法一 export var m = 1; // 写法二 var m = 1; export {m}; // 写法三 var n = 1; export {n as m}; // 写法四 var n = 1; export default n; 复制代码
export default
命令用于指定模块的默认输出。export default
就是输出一个叫作 default
的变量或方法,而后系统容许你为它取任意名字// modules.js function add(x, y) { return x * y; } export {add as default}; // 等同于 // export default add; // app.js import { default as foo } from 'modules'; // 等同于 // import foo from 'modules'; 复制代码
JavaScript 模块规范主要有四种:CommonJS、AMD、CMD、ES Module。 CommonJS 用在服务器端,AMD 和CMD 用在浏览器环境,ES Module 是做为终极通用解决方案。