JavaScript模块化规范(CommonJs AMD CMD UMD ES6)

1、 什么是模块化?

模块化是指将一个复杂的程序分解为多个模块,方便编码javascript

2、为何要使用模块化?

2.一、 函数写法

function m1(){
    // xxx
}
function m2(){
    // xxx
}

复制代码

上面的函数m一、m2就至关于一个模块,使用的时候,直接调用就能够了。css

可是这种作法缺点也很明显:因为函数是直接挂载在window(全局)对象下,"污染"了全局变量,没法保证不与其余模块发生变量名冲突,并且模块成员之间看不出直接关系。html

2.二、 对象写法

既然window对象的可命名属性名就那么多,那我再在window(全局)对象上面声明一个对象,而后把全部的模块成员都放到这个对象里面。前端

var module = {
    count: 0,
    function m1(){
        // xxx
    }
    function m2(){
        // xxx
    }
}

复制代码

上面的函数m1()和m2(),都封装在module1对象里。使用的时候,就是调用这个对象的属性。vue

module.m1();
复制代码

可是,这样的写法会暴露全部模块成员,内部状态能够被外部改写。好比,外部代码能够直接改变内部计数器的值。java

module1._count = 5;
复制代码

2.三、 当即执行函数

为了防止内部成员被暴露出去,咱们用当即执行函数能够实现私有化变量。node

const module2 = (function() {
	let _money = 100
	const m1 = () => {
		console.log(123)
	}
	const m2 = () => {
		console.log(456)
	}
	return {
		f1: m1,
		f2: m2
	}
})()
复制代码

使用上面的写法,外部代码没法读取内部的_count变量。react

console.info(module2._count); //undefined
复制代码

不过虽然这样function内部的变量就对全局隐藏了,达到是封装的目的。可是这样仍是有缺陷的,Module2这个变量仍是暴漏到全局了,随着模块的增多,全局变量仍是会愈来愈多。jquery

2.四、使用Script来引用JS模块

<script type="text/javascript" src="a.js"></script>
<script type="text/javascript" src="b.js"></script>
<script type="text/javascript" src="c.js"></script>
<script type="text/javascript" src="d.js"></script>
复制代码

缺点:git

(1)加载的时候会中止渲染网页,引入的js文件越多,网页失去响应的时间越长;

(2)会污染全局变量;

(3)js文件之间存在依赖关系,加载是有顺序的,依赖性最大的要放到最后去加载;当项目规模较大时,依赖关系变得错综复杂。

(4)要引入的js文件太多,不美观,代码难以管理。

2.五、总结

使用函数写法会致使全局变量污染,并有可能致使命名冲突

使用命名空间会致使内部属性被暴露,能够致使内部成员被改写

使用当即执行函数能够实现私有化变量,能够达到必定的防护做用。是早期较好的模块化方案

使用Script来引用JS模块会致使文件关系错综复杂,难以管理

3、模块化规范

3.一、CommonJS

2009年,美国程序员Ryan Dahl创造了node.js项目,将javascript语言用于服务器端编程。这标志"Javascript模块化编程"正式诞生。由于老实说,在浏览器环境下,没有模块也不是特别大的问题,毕竟网页程序的复杂性有限;可是在服务器端,必定要有模块,与操做系统和其余应用程序互动,不然根本无法编程。而node.js的模块系统,就是参照CommonJS规范实现的。

3.1.一、CommonJS特色

  • 全部代码都运行在模块做用域,不会污染全局做用域。
  • 模块能够屡次加载,可是只会在第一次加载时运行一次,而后运行结果就被缓存了,之后就直接读取缓存结果。要想让模块再次运行,必须清除缓存
  • 模块是同步加载的,所以模块加载的顺序,按照其在代码中出现的顺序
  • CommonJS采用同步加载不一样模块文件,适用于服务器端的。由于模块文件都存放在服务器的各个硬盘上,读取加载时间快,适合服务器端,不适应浏览器。 浏览器不兼容CommonJs,缘由是浏览器缺乏module、exports、require、global四个环境变量。如要使用须要工具转换。

3.1.二、基本语法

  • 暴露模块:module.exports = valueexports.xxx = value
  • 引入模块:require(xxx),若是是第三方模块,xxx为模块名;若是是自定义模块,xxx为模块文件路径。

此处咱们有个疑问:CommomJS暴露的模块究竟是什么?CommonJS规范规定,每一个模块内部,module变量表明当前模块,这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,实际上是加载该模块的module.exports属性。

// a.js
module.exports = {
    a: 1
}
// or 
exports.a = 1

// b.js
var module = require('./a.js')
module.a // -> log 1
复制代码

上面的写法很好用,可是 module.exports 和 exports 是咋回事?为啥这几句代码就实现模块化了,让咱们来看一下基础的实现

先说 require 吧

var module = require('./a.js')
module.a 
// 这里其实就是包装了一层当即执行函数,这样就不会污染全局变量了,
// 重要的是 module 这里,module 是 Node 独有的一个变量
module.exports = {
    a: 1
}
// 基本实现
var module = {
  id: 'xxxx', // 我总得知道怎么去找到他吧
  exports: {} // exports 就是个空对象
}
// 这个是为何 exports 和 module.exports 用法类似的缘由
var exports = module.exports 
var load = function (module) {
    // 导出的东西
    var a = 1
    module.exports = a
    return module.exports
};
// 而后当我 require 的时候去找到独特的
// id,而后将要使用的东西用当即执行函数包装下,over
复制代码

再来讲说module.exportsexports的区别。

  1. exports是指向的module.exports的引用
  2. module.exports初始值为一个空对象{},因此exports初始值也是{},可是不能对exports直接赋值,不会有任何效果,,看了上面代码的同窗确定明白为何了。
  3. require()返回的是module.exports而不是exports

3.1.三、模块的加载机制

CommonJS模块的加载机制是,输入的是被输出的值的拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。 这点与ES6模块化有重大差别(下文会介绍),请看下面这个例子:

// 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了。这是由于counter是一个原始类型的值,会被缓存。除非写成一个函数,才能获得内部变更后的值。

CommonJS规范是 Node 独有的,若是浏览器想使用该规范,就须要用到 Browserify 解析了。

3.1.四、Browserify

Browserify 可让你使用相似于 node 的 require() 的方式来组织浏览器端的 Javascript 代码,经过 预编译 让前端 Javascript 能够直接使用 Node NPM 安装的一些库。 -- 来自百度百科

①下载

  • 全局下载: npm install browserify -g
  • 局部下载: npm install browserify --save-dev

②打包编译 将须要打包编译的JS文件经过 运行代码browserify app.js -o bundle.js 将路径下的app.js文件编译output到bundle.js文件中

③页面使用引入 最后从新在页面文件中引入bundle.js文件<script type="text/javascript" src="./bundle.js"></script>

3.1.五、总结

  1. CommonJS采用同步加载不一样模块文件,适用于服务器端的,不适合在浏览器环境中,同步意味着阻塞加载,浏览器资源是异步加载的
  2. 代码没法直接运行在浏览器环境下,必须经过工具转换成标准的 ES5;

3.二、AMD (Asynchronous Module Definition)

见名知意,就是异步模块定义。上面已经介绍过,CommonJS是服务器端模块的规范,主要是为了JS在后端的表现制定的,不太适合前端。而AMD就是要为前端JS的表现制定规范。因为不是JavaScript原生支持,使用AMD规范进行页面开发须要用到对应的库函数,也就是require.js(还有个js库:curl.js)。实际上AMD 是 require.js在推广过程当中对模块定义的规范化的产出。 AMD采用异步方式加载模块,模块的加载不影响它后面语句的运行。全部依赖这个模块的语句,都定义在一个回调函数中,等到加载完成以后,这个回调函数才会运行。

3.2.一、模块的定义和使用

// 定义一个模块
define('module', ['dep'], function (dep) {
  return exports;
});

// id  可选参数,用来定义模块的标识,若是没有提供该参数,默认脚本文件名(去掉拓展名)

// dependencies 是一个当前模块用来的模块名称数组,(所依赖模块的数组)

// factory 工厂方法,模块初始化要执行的函数或对象,若是为函数,它应该只被执行一次,若是是对象,此对象应该为模块的输出值。


复制代码

require.js也采用require()语句加载模块,可是不一样于CommonJS,它要求二个参数

//导入和使用模块
require([module], callback);
 
// 第二个参数[module],是一个数组,里面的成员就是要加载的模块;

// 第二个参数callback,则是加载成功以后的回调函数

// 等到前面的module加载完成以后,这个回调函数才被调用。
// 加载的模块会以参数形式传入该函数,从而在回调函数内部就可使用这些模块
复制代码

3.2.二、看个例子

// demo.html
<body>
    //引入所依赖文件和主入口文件
    <script src="./require.js" data-main = './demo.js'></script>
</body>

// modules/m1.js
define(function(){
    var name = 'm1-amd';
    function getName(){
        return name;
    }
    return {getName} //暴露出的模块
})

// modules/m2.js
//在m2模块中,引用了m1模块
define(['m1'],function(m1){
    var msg = 'm2-amd';
    function show(){
        console.log(msg,m1.getName());
    }
    return {show}   //暴露的模块
})

//demo.js
(function(){
    //配置每一个变量对应的模块路径
    require.config({
        paths: {
            m1: './modules/m1',
            m2: './modules/m2',
        }
    })
    require(['m2'],function(m2){
        m2.show(); //结果:m2-amd m1-amd
    })
})()
复制代码

默认状况下,require.js假定这加载的模块与main.js在同一个目录,而后自动加载。若是不在同一目录,咱们可使用require.config()方法对模块的加载行为进行自定义

在上面的例子中也能够引用第三方库,只需在上面代码的基础稍做修改:

//demo.js
(function(){
    //配置每一个变量对应的模块路径
    require.config({
        paths: {
            m1: './modules/m1',
            m2: './modules/m2',
            jquery:'./jquery-3.3.1'
        }
    })
    require(['m2','jquery'],function(m2,$){
        m2.show(); //结果:m2-amd m1-amd
        $('body').css('backgroundColor','#000');
    })
})()
复制代码

不过须要注意的是:jquery对模块化作了各类不一样的规范,对每一个不一样模块都有暴露出的接口名字,对AMD暴露出的接口名字是小写jquery,所以不能把jquery写成大写的jQuery,这样会报错

//jquery 3.3.1.js
if(typeof define === 'function' && define.amd){
    define('jquery',[],function(){
        return jQuery;
    })
}
复制代码

3.2.三、AMD特色:

依赖前置:必须等到全部依赖的模块加载完成以后才会执行回调,即便在回调里根本没用到该模块。(在定义模块的时候就要声明其依赖的模块),不过目前在AMD2.0也能够动态加载模块了

requireJS优缺点

优势:

一、适合在浏览器环境中异步加载模块

二、能够并行加载多个模块

缺点:

一、提升了开发成本,代码的阅读和书写比较困难,模块定义方式的语义不畅

二、不符合通用的模块化思惟方式,是一种妥协的实现

3.三、CMD (Common Module Definition)

即通用模块定义,对应SeaJS,是阿里玉伯团队首先提出的概念和设计。跟requireJS解决一样问题,只是运行机制不一样。

3.3.一、CMD与AMD的不一样的在于

(1)AMD推崇依赖前置;CMD推崇依赖就近,只有在用到某个模块的时候再去require:

通俗来讲:

AMD在加载完成定义(define)好的模块就会当即执行,全部执行完成后,遇到require才会执行主逻辑。(提早加载)

CMD在加载完成定义(define)好的模块,仅仅是下载不执行,在遇到require才会执行对应的模块。(按需加载)

AMD用户体验好,由于没有延迟,CMD性能好,由于只有用户须要的时候才执行。

CMD为何会出现,由于对node.js的书写者友好,由于符合写法习惯,就像为什么vue会受人欢迎的一个道理。

3.3.二、CMD语法

Sea.js 推崇一个模块一个文件,遵循统一的写法。

define(id?, deps?, factory)

由于CMD推崇一个文件一个模块,因此常常就用文件名做为模块id CMD推崇依赖就近,因此通常不在define的参数中写依赖,在factory中写 factory有三个参数

function(require, exports, module)

  • require 是一个方法,接受 模块表示 做为惟一参数,用来获取其余模块提供的接口
  • exports 是一个对象,用来向外提供模块接口
  • module 是一个对象,上面存储了与当前模块相关联的一些属性和方法

定义暴露模块

//定义没有依赖的模块
define(function(require, exports, module){
  exports.xxx = value
  module.exports = value
})

复制代码
//定义有依赖的模块
define(function(require, exports, module){
  //引入依赖模块(同步)
  var module2 = require('./module2')
  //引入依赖模块(异步)
    require.async('./module3', function (m3) {
    })
  //暴露模块
  exports.xxx = value
})
复制代码

引入使用模块

define(function (require) {
  var m1 = require('./module1')
  var m4 = require('./module4')
  m1.show()
  m4.show()
})

复制代码

sea.js 简单使用教程

①下载sea.js, 并引入

官网: seajs.org/

github : github.com/seajs/seajs

而后将sea.js导入项目: js/libs/sea.js

②建立项目结构

|-modules
    |-m1.js
    |-m2.js
    |-m3.js
    |-m4.js
|-index.html
|-main.js
|-sea.js

复制代码

③定义sea.js的模块代码

// index.html
<body>
    <script src="./sea.js"></script>    //引入依赖文件
    <script>
        seajs.use('./main.js');         //设置主入口文件
    </script>
</body>

// modules/m1.js
define(function(require,exports,module){
    var msg = 'm1';
    function foo(){
        console.log(msg);
    }
    module.exports = {  //暴露的接口
        foo:foo
    }
});

// modules/m2.js
define(function(require,exports,module){
    var msg = 'm2';
    function bar(){
        console.log(msg);
    }
    module.exports = bar;   /暴露的接口
});

// modules/m3.js
define(function(require,exports,module){
    var msg = 'm3';
    function foo(){
        console.log(msg);
    }
    exports.m3 = { foo:foo} /暴露的接口
});

// modules/m4.js
define(function(require,exports,module){
    var msg = 'm4';
    // 同步引入
    var m2 = require('./m2');
    m2();
    // 异步引入
    require.async('./m3',function(m3){
        m3.m3.foo();
    });
    function fun(){
        console.log(msg);
    }
    exports.m4 = fun;   /暴露的接口
})


//main.js
define(function(require,exports,module){
    var m1 = require('./modules/m1');
    m1.foo();
    var m4 = require('./modules/m4');
    m4.m4();
})

复制代码

最后获得结果以下

3.3.三、CMD优缺点

优势: 一样实现了浏览器端的模块化加载。 能够按需加载,依赖就近。

缺点: 依赖SPM打包,模块的加载逻辑偏重

3.四、ES6 Module

ES6 模块的设计思想是尽可能的静态化,使得编译时就能肯定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时肯定这些东西。好比,CommonJS 模块就是对象,输入时必须查找对象属性。

3.4.1 语法

在 ES6 中,使用export关键字来导出模块,使用import关键字引用模块。可是浏览器尚未彻底兼容,须要使用babel转换成ES5的require。

// 导出
export function hello() { };
export default {
  // ...
};
// 导入
import { readFile } from 'fs';
import React from 'react';

复制代码

使用import导入模块时,须要知道要加载的变量名或函数名。

在ES6中还提供了export default,为模块指定默认输出.对应导入模块import时,不须要使用大括号。

//math.js
var num = 0;
var add = function (a, b) {
  return a + b;
};
export { num, add };

//导入
import { num, add } from './math';
function test(ele) {
  ele.textContent = add(1 + num);
}

复制代码

3.4.二、ES6与CommonJS的区别

CommonJS

  • 对于基本数据类型,属于复制。即会被模块缓存。同时,在另外一个模块能够对该模块输出的变量从新赋值。

  • 对于复杂数据类型,属于浅拷贝。因为两个模块引用的对象指向同一个内存空间,所以对该模块的值作修改时会影响另外一个模块。

  • 当使用require命令加载某个模块时,就会运行整个模块的代码。

  • 当使用require命令加载同一个模块时,不会再执行该模块,而是取到缓存之中的值。也就是说,CommonJS模块不管加载多少次,都只会在第一次加载时运行一次,之后再加载,就返回第一次运行的结果,除非手动清除系统缓存。

  • 循环加载时,属于加载时执行。即脚本代码在require的时候,就会所有执行。一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出。

ES6模块

  • ES6模块中的值属于【动态只读引用】。

  • 对于只读来讲,即不容许修改引入变量的值,import的变量是只读的,不管是基本数据类型仍是复杂数据类型。当模块遇到import命令时,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。

  • 对于动态来讲,原始值发生变化,import加载的值也会发生变化。不管是基本数据类型仍是复杂数据类型。

  • 循环加载时,ES6模块是动态引用。只要两个模块之间存在某个引用,代码就可以执行。

3.4.三、优缺点

优势:

一、容易进行静态分析

二、面向将来的 EcmaScript 标准

缺点:

一、浏览器尚未彻底兼容,必须经过工具转换成标准的 ES5 后才能正常运行。

二、全新的命令字,新版的 Node.js才支持

4、总结


  • CommonJS规范主要用于服务端编程,加载模块是同步的,这并不适合在浏览器环境,由于同步意味着阻塞加载,浏览器资源是异步加载的,所以有了AMD CMD解决方案。

  • AMD规范在浏览器环境中异步加载模块,并且能够并行加载多个模块。不过,AMD规范开发成本高,代码的阅读和书写比较困难,模块定义方式的语义不畅。

  • CMD规范与AMD规范很类似,都用于浏览器编程,依赖就近,延迟执行,能够很容易在Node.js中运行。不过,依赖SPM 打包,模块的加载逻辑偏重

  • ES6 在语言标准的层面上,实现了模块功能,并且实现得至关简单,彻底能够取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。

相关文章
相关标签/搜索