runoops.com

JavaScript模块化(Module)

模块化的理解

1、什么是模块化

  • 将一个复杂的程序,依据一定的规则(规范)封装成一个或多个块(文件), 并进行组合在一起
  • 块的内部数据与实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信

2、模块化的进化过程

  • 无模块时代

在ajax还未提出之前,js还只是用来在网页上进行表单校验、提交,对DOM渲染操作。

var str,num;
//......
function submit(){
	str = document.getElementById("xx").value;
	if(str){
     //......
	}
	else{
	 //......
	}
	num = 1;
	for(var i=0; i<10; i++){
		num++
	     //......
	}
	//......
	form.submit()
}
<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="main.js"></script>

缺点:

  • 全局变量污染
  • 函数命名冲突
  • 文件依赖顺序

模块雏形时代
2006年,ajax的概念被提出,前端拥有了主动向服务端发送请求并操作返回数据的能力,传统的网页向“富客户端”发展,出现了简单的功能对象封装。

  • namespace模式
//模块js
var myModule = {
	first_name: 'www.',
	second_name: 'baidu.com',
	getFullName: function() {
		return this.first_name + this.second_name;
	}
}
//调用js
console.log(myModule.getFullName());
myModule.first_name = 'img.';
console.log(myModule.getFullName());

优点: 减少了全局变量,解决命名冲突
缺点: 数据不安全(外部可以直接修改模块内部的数据),模块名称会暴露在全局,存在命名冲突,依赖顺序问题。

  • 自执行匿名函数(闭包)模式
//模块js
(function (window) {
    let _moduleName = 'module';
    function setModuleName(name) {
        _moduleName = name;
    }
    function getModuleName() {
        return _moduleName;
    }
    window.moduleA = { setModuleName, getModuleName }
})(window)
//调用js
moduleA.setModuleName('html-module');
console.log(moduleA.getModuleName());
console.log(moduleA._moduleName);//模块不暴露,无法访问模块内属性方法

优点:变量、方法全局隐藏,模块私有化
缺点:模块名称会暴露在全局,存在命名冲突,依赖顺序问题。

3、面临的问题

从以上的尝试中,可以归纳出js模块化需要解决那些问题:

  • 如何安全的包装一个模块的代码?(不污染模块外的任何代码)
  • 如何唯一标识一个模块?
  • 如何优雅的把模块的API暴漏出去?(不能增加全局变量)
  • 如何方便的使用所依赖的模块?

模块化的规范

1、CommonJS

2009年Nodejs发布,采用 CommonJS 模块规范。
特点:

  • 每个文件都是一个模块实例,代码运行在模块作用域,不会污染全局作用域。
  • 文件内通过require对象引入指定模块,通过exports对象来向往暴漏API,文件内定义的变量、函数,都是私有的,对其他文件不可见。
  • 每个模块加载一次之后就会被缓存。
  • 所有文件加载均是同步完成,加载的顺序,按照其在代码中出现的顺序。
  • 模块输出的是一个值的拷贝,模块内部的变化不会影响该值。
//模块js
let _moduleName = 'module';
function setModuleName(name) {
    _moduleName = name;
}
function getModuleName() {
    return _moduleName ;
}
module.exports = { setModuleName, getModuleName }
//调用js
import { getModuleName, setModuleName } from './es6.module';
setModuleName("es6 Module");
console.log(getModuleName());

缺点:模块同步加载,资源消耗和等待时间,适用于服务器编程

2、AMD/RequireJS

Commonjs局限性很明显:
基于Node原生api在服务端可以实现模块同步加载,但是仅仅局限于服务端,客户端如果同步加载依赖的话时间消耗非常大,所以需要一个在客户端上基于CommonJS,但是对于加载模块做改进的方案,于是AMD规范诞生了。

AMD是"Asynchronous Module Definition"的缩写,意思就是"异步模块定义"。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到所有依赖加载完成之后(依赖前置),这个回调函数才会运行。

RequireJS是一个工具库,主要用于客户端的模块管理。它的模块管理遵守AMD规范,RequireJS的基本思想是,通过define方法将代码定义为模块,通过require方法实现代码的模块加载。

// module1.js 定义没有依赖的模块
define(function () {
  let _moduleName = 'module';
  function getName() {
    return _moduleName;
  }
  return { getName } // 暴露模块
})
// module2.js 定义有依赖的模块
define(['module1'], function (module1) {
  let _firstName = 'AMD'
  function getFullName() {
    return _firstName + ' ' + module1.getName();
  }
  function setFirstName(name) {
    _firstName = name;
  }
  // 暴露模块
  return { _firstName, getFullName, setFirstName }
})
//main.js
require.config({
  paths: {
    module1: './modules/module1',
    module2: './modules/module2',
    // 第三方库模块
    jquery: './libs/jquery.min'
  }
})
require(['module2','jquery'], function(module2,jquery) {
  console.log(module2.getFullName());
  module2.setFirstName('AMD-AMD');
  console.log(module2.getFullName());
  console.log(module2._firstName);
  jquery('#moduleId').html("<i>My name is jquery-module</i>");
})
//html中引入工具库,并定义js主文件
<script data-main="./main" src="./libs/require.js"></script>

特点:浏览器直接运行无需编译,异步加载,依赖关系清晰。

3、CMD/SeaJS

CMD规范专门用于浏览器端,同样是受到Commonjs的启发,国内(阿里)诞生了一个CMD(Common Module Definition)规范。该规范借鉴了Commonjs的规范与AMD规范,在两者基础上做了改进。

与AMD相比非常类似,CMD规范(2011)具有以下特点:

  • define定义模块,require加载模块,exports暴露变量。
  • 不同于AMD的依赖前置,CMD推崇依赖就近(需要的时候再加载)
  • 推崇api功能单一,一个模块干一件事。

SeaJs是CMD规范的实现,跟RequireJs类似,CMD是SeaJs推广过程中诞生的规范。CMD借鉴了很多AMD和Commonjs优点。

//module.1
define(function (require, exports, module) {
  module.exports = {
    msg: 'I am module1'
  }
//module.2
define(function (require, exports, module) {
  var module2 = require('./module1')
    function show() {
        console.log('同步引入依赖模块1 ' + module2.msg)
    }
    exports.showModule = show
})
//main.js
define(function (require) {
  var m2 = require('./modules/module2')
  m2.showModule();
})
//html中引入工具库,并定义js主文件
<script type="text/javascript" src="./libs/sea.js"></script>
<script type="text/javascript">
  seajs.use('./main')
</script>

AMD、CMD区别

  • AMD 推崇依赖前置
  • CMD 推崇依赖就近
//AMD
define(['./a', './b'], function(a, b) { // 依赖必须一开始就写好
  a.doSomething()
  ...
  // 此处略去 100 行
  b.doSomething()
  ...
})
//CMD
define(function(require, exports, module) {
  var a = require('./a')
  a.doSomething()
  ...
  // 此处略去 100 行
  var b = require('./b') // 依赖可以就近书写
  b.doSomething()
  ...
}

4、ES6

2015年,ES6规范中,终于将模块化纳入JavaScript标准,从此js模块化被ECMA官方扶正,也是后来js的标准。
ES6中的模块化在CommonJS的基础上有所不同,关键字有import,export,default,as,from。

//模块js
let _moduleName = 'module';
function setModuleName(name) {
    _moduleName = name;
}
function getModuleName() {
    return _moduleName;
}
export { setModuleName, getModuleName }
//调用js
import { getModuleName,setModuleName } from './es6.module';
setModuleName("es6 Module");
console.log(getModuleName());

CommonJS和ES6区别

  • CommonJS 模块输出的是一个值的拷贝,即原来模块中的值改变不会影响已经加载的该值。
  • ES6 模块输出的是值的只读引用,模块内值改变,引用也改变。
  • CommonJS 模块是运行时加载,加载的是整个模块,即将所有的接口全部加载进来。
  • ES6 模块是编译时输出接口,可以单独加载其中的某个接口。

总结

CommonJS规范主要用于服务端编程,加载模块是同步的,不适合在浏览器环境,存在阻塞加载,浏览器资源是异步加载的,因此有了AMD、CMD解决方案。
AMD规范在浏览器环境中异步加载模块,而且可以并行加载多个模块。
CMD规范与AMD规范很相似,都用于浏览器编程,依赖就近,代码更简单。
ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。


参考:

https://blog.csdn.net/jianren0518/article/details/106099103