模块化用于解决引入多个js文件时的命名冲突和文件依赖问题。
关键字:
- CommonJS:一个同步模块规范。这种方式通过一个叫做require的方法,同步加载依赖,然后返导出API供其它模块使用,一个模块可以通过exports或者module.exports导出API。
CommonJS规范中,一个单独的文件就是一个模块。每一个模块都是一个单独的作用域,在一个文件中定义的变量,都是私有的,对其他文件是不可见的。
服务端Node.JS就是用的这种方式。- AMD:异步模块定义,定义了一套 JavaScript 模块依赖异步加载标准,来解决同步加载的问题。通过define关键字定义模块及回调函数。
- CMD:同步模块定义。
- UMD:通用模块定义,规范类似于兼容 CommonJS 和 AMD 的语法糖,是模块定义的跨平台解决方案。
ES6 Modules:ES6标准提出的原生模块定义。
模块化构建工具:
- browerify:用来打包CommonJS模块以便其在浏览器里运行的模块构建工具
- RequireJS:加载AMD的模块化构建工具
- SeaJS:加载CMD的模块化构建工具
- webpack:模块化构建工具,兼容CommonJS, AMD, ES6各类规范
什么是模块化?
一个页面可能会引入多个js文件,可能会有 jQuery、Bootstrap、一些插件以及一些业务代码。
<script src="jquery.js"></script>
<script src="bootstrap.js"></script>
<script src="plugin-A.js"></script>
<script src="plugin-B.js"></script>
<script src="app.js"></script>
当我们在多个 JavaScript 文件之间进行通讯时,我们可能会把一个变量挂到 window 上,变成一个全局的变量。当项目变得越来越复杂,这些全局变量也会变得越来越多,很容易出现命名冲突的情况。
而且,在很多时候,我们多个 JS 文件之间是有依赖关系的,比如说我们图中的 plugin-B。js 如果依赖了 plugin-A.js,当别人想使用 plugin-B.js 但是没有引入 plugin-A.js 的时候,那么 plugin-B.js 就不能正常运行了。所以我们遇到了一个比较繁琐的 文件依赖 问题。
所以如何解决命名冲突和文件依赖的问题呢?
首先,对于命名冲突问题,其根本原因在于所有的.js文件时共享作用域的,而且它们可能定义了一些全局变量。
解决方法就是:限制作用域,并且移除全局变量。
其次,针对依赖混乱问题,我们可以通过规定一些特殊的语法,来在代码中声明依赖关系,再开发一个工具来自动化处理文件之间的依赖,就可以解决依赖混乱的问题了。
而解决以上两个问题的做法就是模块化。
模块模式
我们把每一个 .js 文件都视为一个 模块,模块内部有自己的作用域,不会影响到全局。并且,我们约定一些关键词来进行依赖声明和 API 暴露。在js中有几种用于实现模块的方法和规范:
- 对象字面量表示法
- Module模式
以下是一些模块化规范,括号内为对应的脚本加载器
- CMD(SeaJS)
- AMD(RequireJS)
- CommonJS(NodeJS)
- ES6 Module (ECMAScript 2015)
比较有名模块化规范的是 CMD、AMD、CommonJS 和 ES6 Module,它们都是为了实现在浏览器端模块化开发的目的。前面两个规范分别来自 SeaJS 及 RequireJS,这两个规范现在基本已经很少人用了;CommonJS 由于是被 NodeJS 所采用的,所以很多人用;而 ES6 Module 自然是来自去年正式发布的 ECMAScript 2015 所采用的了,以后会逐渐成为最主要的模块化规范。
模块的基本写法
对象字面量
在对象字面量表示法中,一个对象被描述为一组包含在大括号中、以逗号分隔的名值对。
var person = {
name:"gigi",
sayName:function(){
alert(this.name);
}
}
对象字面量不需要使用new操作符进行实例化。
使用对象字面量有助于封装和组织代码,即将所有属性和方法封装在一个变量中。
var myModule = {
//属性
myProperty:"someValue",
//一些用于配置的属性
myConfig:{
language:"en",
useCaching:true
},
//基本方法
myMethod:function(){
//……
},
//用于输出配置的方法
myMethod2:function(){
concole.log(this.myConfig.useCaching);
},
//用于设置配置的方法
myMethod3:function(newConfig){
this.myConfig = newConfig;
}
}
myModule.myMethod();
myModule.myMethod2(); //true
myModule.myMethod3({
language:"fr",
useCaching:false
});
Module(模块)模式
Module模式用于进一步模拟类的概念,通过这种方式,能够使一个单独的对象拥有公有/私有变量和方法,从而构成自己的一个私有作用域,减少命名冲突的可能性。
这一部分的内容也可以参考js学习笔记:函数——私有变量
var myModule = (function(){
var counter = 0;
return {
incrementCounter:function(){
counter++;
},
resetCounter:function(){
counter = 0;
}
};
})();
myModule.incrementCounter();
myModule.resetCounter();
- 使用一个返回值为对象的立即调用函数。结果就是myModule对象就是函数返回的那个对象。
- 使用闭包来封装私有变量和方法,防止其泄露至全局作用域、发生命名冲突。
通过该模式,只需返回一个公有API,而其他的一切则都维持在私有闭包里。
这为我们提供了一个屏蔽处理底层事件逻辑的解决方案,同时只暴露一个接口供应用程序和其他部分使用。
需要理解的是,在js没有真正意义上的私有,我们只是用函数作用域来模拟这个概念。
在Module模式内,由于闭包的存在,声明的变量和方法只在该模式内部可用。但在返回对象上定义的变量和方法,则是对外部可见的。
在这个例子中,代码的其它部分无法直接调用incrementCounter()和resetCounter()。
并且counter变量实际上是完全与全局作用域隔离的,因此它表现的就像是一个私有变量。他的存在被局限在模块的闭包内,因此唯一能够访问其作用域的代码就是这两个函数。
下面是一个包含命名空间、公有和私有变量的Module模式的例子:
vat myNamespace = (function(){
//私有计数器变量
var myPrivateVar = 0;
//私有函数
var myPrivateMethod = function(foo){
console.log(foo);
};
return {
//公有变量
myPublicVar:"foo",
//调用私有变量和方法的公有函数
muPublicFunction:function(bar){
myPrivateVar++;
myPrivateMethod(bar);
}
}
})()
可以将全局变量作为参数传递给匿名函数并在模块中使用,如jQuery。在引入的同时也能给予其本地命名。
var myModule = (function($){
function privateMethod1(){
$(".container").html("test");
}
return {
publicMethod:function(){
privateMethod1();
}
}
})(jQuery)
myModule.publicMethod();
优点
Module模式支持私有数据,并且只有公有部分能够接触这些私有数据。缺点
由于访问公有和私有成员的方式不同,当我们想改变成员的可见性时需要修改每一处使用该成员的代码。
同时也无法为私有成员创建自动化单元测试。
模块规范
上面介绍了js模块的基本写法,下面介绍如何规范地使用模块。
模块规范用于统一模块的写法,方便互相使用。
目前,通行的Javascript模块规范共有两种:CommonJS和AMD。
AMD规范
概览
AMD规范则是Async Module Define,它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。
- 如果是在服务器端,模块文件一般都已经存在于本地,所以加载起来比较快,不用考虑异步加载的方式,所以CommonJS规范比较适用。
- 如果是在浏览器环境,要从服务器端加载模块,这是就必须采用异步模式,因此浏览器端一般采用AMD规范。
define
AMD规范使用define方法定义模块:
define("myModule",['package/lib'], function(lib){
function foo(){
lib.log('hello world!');
}
return {
foo: foo
};
});
- 第一个参数是模块id。
- 第二个参数是依赖的模块,这些模块都会在后台无阻塞地加载
- 第三个参数则作为加载完毕的回调函数。回调函数将会使用载入的模块作为参数。
require
require用于加载模块。可以用于动态加载依赖。
但是与commonjs的用法不同,要求两个参数:要加载的模块和回调函数。
//foo和bar是两个外部模块,两个模块加载以后的输出作为回调函数的参数传入(foo和bar)。
require(["foo","bar"],function(foo,bar){
foo.doSomething();
})
下面是一个动态加载依赖的示例:
define(function(require){
var isReady = false;
var fooBar;
//动态加载依赖
require(["foo","bar"],function(foo,bar){
isReady = true;
foobar = foo()+bar();
})
//依然返回一个模块
return {
isReady:isReady,
fooBar:fooBar
}
})
目前,主要有两个Javascript库实现了AMD规范:require.js和curl.js。
使用RequireJS加载AMD模块:
require(["app/myModule"],function(myModule){
//开始主模块
var module = new myModule();
module.doStuff();
})
除了RequireJS还可以使用Curl来加载AMD模块
curl(["app/myModule.js"],function(myModule){
//开始主模块
var module = new myModule();
module.doStuff();
})
AMD优点
AMD展现的不仅仅是一个典型的Module模式,它具有许多优点:
- 封装模块定义,避免全局命名空间污染
- 不依赖于服务器端工具,适用于浏览器端(与CommonJS相比)
commonjs模块规范
概览
根据这个规范,每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类、都是私有的,对其他文件不可见。
与AMD不同,这种模块周围没有函数封装器(所以我们看不到define那种用法)。
CommonJs规范规定,每个模块内部,module变量代表当前模块。
module.exports
module对象的exports属性是对外的接口。加载某个模块,其实是加载该模块的module.exports属性值。
//example.js
var x = 5;
var addX = function (value) {
return value + x;
};
module.exports.x = x;
module.expotrs.addX = addX;
上面代码通过module.exports输出变量x和函数addX。
require用于加载其他模块。
var example = require('./example.js');
console.log(example.x);
console.log(example.addX(1));
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 = function(x) {console.log(x)};
上面这种写法就导致exports不再指向module.exports了。
下面的这种写法也是无效的:
exports.hello = function() {
return 'hello';
};
module.exports = 'Hello world';
上面代码中,由于module.exports被重新赋值了,因此hello函数是无法对外输出的。
这意味着,如果一个模块的对外接口,就是一个单一的值,不能使用exports输出,只能使用module.exports输出。
module.exports = function (x){ console.log(x);};
如果你觉得,exports与module.exports之间的区别很难分清,一个简单的处理方法,就是放弃使用exports,只使用module.exports。
require命令
基本用法
Node使用CommonJS模块规范,内置的require命令用于加载模块文件。
require命令的基本功能是,读入并执行一个JavaScript文件,然后返回该模块的exports对象。如果没有发现指定模块,会报错。
// example.js
var invisible = function () {
console.log("invisible");
}
exports.message = "hi";
exports.say = function () {
console.log(message);
}
运行下面的命令,可以输出exports对象。
var example = require('./example.js');
example
// {
// message: "hi",
// say: [Function]
// }
如果模块输出的是一个函数,那就不能定义在exports对象上面,而要定义在module.exports变量上面。
module.exports = function () {
console.log("hello world")
}
require('./example2.js')()
上面代码中,require命令调用自身,等于是执行module.exports,因此会输出 hello world。
加载规则
require命令用于加载文件,后缀名默认为.js
var foo = require('foo');
// 等同于
var foo = require('foo.js');
模块的加载机制
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与CommonJS
二者都是有效的模块格式,有着不同的最终目标。
AMD采用浏览器优先的开发方法,选择异步行为,但是没有任何I/O的概念。
AMD支持对象、函数、构造函数、字符串、JSON以及很多其他类型的模块,在浏览器中原生运行,使用非常灵活。
另一方面,CommonJS采用服务器优先的方法,采用同步行为,同时支持非包装模块,得以摆脱AMD的define包装器。
CMD
另一种优秀的模块管理工具是 Sea.js,它的写法是:
define(function(require, exports, module) {
var foo = require('foo'); // 同步
foo.add(1, 2);
...
require.async('math', function(math) { // 异步
math.add(1, 2);
});
});
Sea.js 也被称为就近加载,从它的写法上可以很明显的看到和 Require.js 的不同。我们可以在需要用到依赖的时候才申明。
Sea.js 遇到依赖后只会去下载 JS 文件,并不会执行,而是等到所有被依赖的 JS 脚本都下载完以后,才从头开始执行主逻辑。因此被依赖模块的执行顺序和书写顺序完全一致。
由 Sea.js 引申出来的规范被称为 CMD(Common Module Definition)。
ES6 Modules
在JS的最新规范ECMAScript 6 (ES6)中,引入了模块功能。
ES6 的模块功能汲取了CommonJS 和 AMD 的优点,拥有简洁的语法并支持异步加载,并且还有其他诸多更好的支持。
之前在CommonJS中讲过,其输出的是输入的复制,因此模块内部的任何变化都不会再影响输出。
但在ES6 Module中,通过 import 语句,可以引入实时只读的模块:
//counter.js
export let counter = 1;
export function increment(){
counter++;
}
export function decrement(){
counter--;
}
//main.js
import * as counter from 'counter';
console.log(counter.counter); //1
counter.increment();
console.log(counter.counter); //2
UMD
UMD 希望提供一个前后端跨平台的解决方案(支持AMD与CommonJS模块方式)。
UMD的实现原理就是:
- 先判断是否支持AMD(define是否存在),如果存在就使用AMD方式加载模块。
- 再判断是否支持CommonJS(exports是否存在),如果存在则使用CommonJS
- 如果前两个都不存在,则将模块公开到全局。
(function(root, factory){
if(typeof define == "function" && define.amd){
define([],factory);
}else if(typeof exports == "object"){
module.exports = factory();
}else{
root.returnExports = factory();
}
}(this,function(){
……
return {
……
};
}));
参考链接: