JavaScript 模块化总结

关键词: AMD、CMD、UMD、CommonJS、ES Modulejavascript

规范JavaScript的模块定义和加载机制,下降学习和使用各类框架的门槛,可以以一种统一的方式去定义和使用模块,提升开发效率,下降了应用维护成本。html

目录:前端

模块化的历史

想当初,Brendan Eich 只用了十天就创造了 JavaScript 这门语言,谁曾想这门一直被看做玩具性质的语言在近几年得到了爆发性地发展,从浏览器端扩展到服务器,再到 native 端,变得愈来愈火热。而这门语言创造当初的诸多限制也在前端工程化的今天被放大,社区也在积极推进其变革。实现模块化的开发正是其中最大的需求,本文梳理 JavaScript 模块化开发的历史和将来,以做学习之用。java

JavaScript 模块化的发展历程,是以 2009 年 CommonJS 的出现为分水岭,这一规范极大地推进前端发展。在1999年至2009年期间,模块化探索都是基于语言层面的优化,2009 年后前端开始大量使用预编译。node

刀耕火种的原始时代(1999 - 2009)

在 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>
复制代码

可是,即便有规范的目录结构,也不能避免由此而产生的大量全局变量,这就致使了一不当心就会有变量冲突的问题,就比如上面这个例子中的 writeHellowebpack

因而在 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 外是不能直接访问和操做的,能够经过暴露一些方法来访问和操做,好比说上面例子里面的 getHellowriteHello2 个方法,这就是所谓的 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个好处:

  1. 提升性能:经过 IIFE 的参数传递经常使用全局对象 window、document,在做用域内引用这些全局对象。JavaScript 解释器首先在做用域内查找属性,而后一直沿着链向上查找,直到全局范围,所以将全局对象放在 IIFE 做用域内能够提高js解释器的查找速度和性能;
  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 规范的落地,以及各类前端工具、解决方案的出现,才真正使得前端开发大放光芒。

大步踏进工业化 (2009 - 至今)

CommonJS 的出现真正使得前端进入工业化时代。前面说了,2009 年之前的各类模块化方案虽然始终停留在语言层面上,虽然也有 YUI 这样的工具,但还不足以成为引领潮流的工具。究其缘由,仍是由于前端工程复杂度还没积累到必定程度,随着 Node.js 的出现,JS 涉足的领域转向后端,加上 Web app 变得愈来愈复杂,工程发展到必定阶段,要出现的必然会出现。

CommonJS 是一套同步的方案,它考虑的是在服务端运行的Node.js,主要是经过 require 来加载依赖项,经过 exports 或者 module.exports 来暴露接口或者数据的方式。

因为在服务端能够直接读取磁盘上的文件,因此能作到同步加载资源,但在浏览器上是经过 HTTP 方式获取资源,复杂的网络状况下没法作到同步,这就致使必须使用异步加载机制。这里发展出两个有影响力的方案:

  • 基于 AMD 的 RequireJS
  • 基于 CMD 的 SeaJS

它们分别在浏览器实现了definerequiremodule的核心功能,虽然二者的目标是一致的,可是实现的方式或者说是思路,仍是有些区别的,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 的语法,兼容更多的平台。

各类模块化方案出现的时间线

  • 1999: 直接定义依赖
  • 2002: 命名空间模式
  • 2003: 闭包模块化模式
  • 2006: 模版依赖定义
  • 2006:注释依赖定义
  • 2007:外部依赖定义
  • 2009:Sandbox 模式
  • 2009:依赖注入
  • 2009: 🌟CommonJS 规范
  • 2009: 🌟AMD 规范,
  • 2009: 🌟CMD 规范,差很少跟 AMD 规范一样时间出现,都是为了解决浏览器端模块化问题,它是由 sea.js 在推广过程当中对模块定义的规范化产出。
  • 2011: UMD 规范
  • 2012: Labeled Modules
  • 2013: YModules
  • 2015: 🌟ES Module

CommonJS

介绍

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
复制代码

特色

  • 全部代码都运行在模块做用域,不会污染全局做用域。
  • 模块能够屡次加载,可是只会在第一次加载时运行一次,而后运行结果就被缓存了,之后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
  • 模块加载的顺序,按照其在代码中出现的顺序。

module 对象

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 变量指向一个值,由于这样等于切断了 exportsmodule.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

介绍

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 的介绍

RequireJS 能够看做是对 AMD 规范的具体实现,它的用法和上节所展现的有所区别。

下载地址:requirejs.org/docs/downlo…

下面简单介绍一下其用法:

  1. 在 index.html 中引用 RequireJS:
<!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 后缀能够省略)。

  1. main.js
// 配置文件
require.config({
    baseUrl: 'js',
    paths: {
        jquery: 'lib/jquery-1.11.1',
    }
});

// 加载模块
require(['jquery', 'script/hello'],function ($, hello) {
    $("#btn").click(function(){
      hello.showMessage("test");
    });
});
复制代码
  1. hello.js
// 定义模块
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 使用方法见官网文档或网上资料,这里只作基本介绍。

CMD

介绍

在前端的模块化发展上,还有另外一种与 AMD 相提并论的规范,这就是 CMD:

CMD 即 Common Module Definition 通用模块定义。 CMD 是 SeaJS 在推广过程当中对模块定义的规范化产出。 CMD 规范的前身是 Modules/Wrappings 规范。

规范介绍

在 CMD 规范中,一个模块就是一个文件。代码的书写格式以下:

define(factory);
复制代码

1. define Function

define 是一个全局函数,用来定义模块。

define(factory)

define 接受 factory 参数,factory 能够是一个函数,也能够是一个对象或字符串。

factory 为对象、字符串时,表示模块的接口就是该对象、字符串。好比能够以下定义一个 JSON 数据模块:

define({ "foo": "bar" });
复制代码

也能够经过字符串定义模板模块:

define('I am a template. My name is {{name}}.');
复制代码

factory 为函数时,表示是模块的构造方法。执行该构造方法,能够获得模块向外提供的接口。factory 方法在执行时,默认会传入三个参数:requireexportsmodule

define(function(require, exports, module) {
  // 模块代码
});
复制代码

define(id?, deps?, factory)

define 也能够接受两个以上参数。字符串 id 表示模块标识,数组 deps 是模块依赖。好比:

define('hello', ['jquery'], function(require, exports, module) {
  // 模块代码
});
复制代码

iddeps 参数能够省略。省略时,能够经过构建工具自动生成。

注意:带 id 和 deps 参数的 define 用法不属于 CMD 规范,而属于 Modules/Transport 规范。

define.cmd

一个空对象,可用来断定当前页面是否有 CMD 模块加载器:

if (typeof define === "function" && define.cmd) {
  // 有 Sea.js 等 CMD 模块加载器存在
}
复制代码

2. require Function

requirefactory 函数的第一个参数。

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

});
复制代码

这能够用来获取模块路径,通常用在插件环境或需动态拼接模块路径的场景下。

3. exports 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 赋值是无效的,不能用来更改模块接口。

4. module 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);

});
复制代码

SeaJS 的介绍

文档地址:Sea.js - A Module Loader for the Web

简单入手:

  1. index.html
<!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>
复制代码
  1. main.js
// 全部模块都经过 define 来定义
define(function(require, exports, module) {

  // 经过 require 引入依赖
  var $ = require('jquery');
  var Spinning = require('./spinning');

  // 经过 exports 对外提供接口
  exports.doSomething = ...

  // 或者经过 module.exports 提供整个接口
  module.exports = ...

});
复制代码

UMD

特色:兼容 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

介绍

在 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 还有如下好处:

  • 再也不须要 UMD 模块格式了,未来服务器和浏览器都会支持 ES Module 格式。目前,经过各类工具库,其实已经作到了这一点。
  • 未来浏览器的新 API 就能用模块格式提供,再也不必须作成全局变量或者 navigator 对象的属性。
  • 再也不须要对象做为命名空间(好比 Math 对象),将来这些功能能够经过模块提供。

特色

  • 静态编译
  • 输出的值引用,而非值拷贝
  • import 只能写在顶层,由于是静态语法

样例

  1. 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;
复制代码
  1. 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 是做为终极通用解决方案。

AMD 和 CMD 的区别

  • 执行时机: AMD 是提早执行,CMD 是延迟执行。
  • 对依赖的处理:AMD 推崇依赖前置,CMD 推崇依赖就近。
  • API 设计理念:AMD 的 API 默认是一个当多个用,很是灵活,CMD 的 API 严格区分,推崇职责单一。
  • 遵循的规范:RequireJS 遵循的是 Modules/AMD 规范,SeaJS 遵循的是 Mdoules/Wrappings 规范的 define 形式。
  • 设计理念:SeaJS 设计理念是 focus on web, 努力成为浏览器端的模块加载器,RequireJS 想成为浏览器端的模块加载器,同时也想成为 Rhino / Node 等环境的模块加载器。

CommonJS 和 ES Module 的区别

  • 加载时机:CommonJS 是运行时加载(动态加载),ES Module 是编译时加载(静态加载)
  • 加载模块:CommonJS 模块就是对象,加载的是该对象,ES Module 模块不是对象,加载的不是对象,是接口
  • 加载结果:CommonJS 加载的是整个模块,即将全部的接口所有加载进来,ES Module 能够单独加载其中的某个接口(方法)
  • 输出:CommonJS 输出值的拷贝,ES Module 输出值的引用
  • this: CommonJS 指向当前模块,ES Module 指向 undefined

参考

CommonJS 知识

AMD 模块相关

CMD 模块相关

ES Module 模块相关

各个规范之间的比较

模块化历史

相关文章
相关标签/搜索