模块化的理解
1、什么是模块化
- 将一个复杂的程序,依据一定的规则(规范)封装成一个或多个块(文件), 并进行组合在一起。
- 块的内部数据与实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信。
2、模块化的进化过程
无模块时代
在 ajax 还未提出之前,js 还只是用来在网页上进行表单校验、提交,对 DOM 渲染操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 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(); }
|
1 2 3 4
| <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 的概念被提出,前端拥有了主动向服务端发送请求并操作返回数据的能力,传统的网页向“富客户端”发展,出现了简单的功能对象封装。
1 2 3 4 5 6 7 8
| var myModule = { first_name: "www.", second_name: "baidu.com", getFullName: function () { return this.first_name + this.second_name; }, };
|
1 2 3 4
| console.log(myModule.getFullName()); myModule.first_name = "img."; console.log(myModule.getFullName());
|
优点: 减少了全局变量,解决命名冲突
缺点: 数据不安全(外部可以直接修改模块内部的数据),模块名称会暴露在全局,存在命名冲突,依赖顺序问题
1 2 3 4 5 6 7 8 9 10 11
| (function (window) { let _moduleName = "module"; function setModuleName(name) { _moduleName = name; } function getModuleName() { return _moduleName; } window.moduleA = { setModuleName, getModuleName }; })(window);
|
1 2 3 4
| moduleA.setModuleName("html-module"); console.log(moduleA.getModuleName()); console.log(moduleA._moduleName);
|
优点:变量、方法全局隐藏,模块私有化
缺点:模块名称会暴露在全局,存在命名冲突,依赖顺序问题
3、面临的问题
从以上的尝试中,可以归纳出 js 模块化需要解决那些问题:
- 如何安全的包装一个模块的代码?(不污染模块外的任何代码)
- 如何唯一标识一个模块?
- 如何优雅的把模块的 API 暴漏出去?(不能增加全局变量)
- 如何方便的使用所依赖的模块?
模块化的规范
1、CommonJS
2009 年 Nodejs 发布,采用 CommonJS 模块规范。
特点:
- 每个文件都是一个模块实例,代码运行在模块作用域,不会污染全局作用域。
- 文件内通过 require 对象引入指定模块,通过 exports 对象来向往暴漏 API,文件内定义的变量、函数,都是私有的,对其他文件不可见。
- 每个模块加载一次之后就会被缓存。
- 所有文件加载均是同步完成,加载的顺序,按照其在代码中出现的顺序。
- 模块输出的是一个值的拷贝,模块内部的变化不会影响该值。
1 2 3 4 5 6 7 8 9
| let _moduleName = "module"; function setModuleName(name) { _moduleName = name; } function getModuleName() { return _moduleName; } module.exports = { setModuleName, getModuleName };
|
1 2 3 4
| 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 方法实现代码的模块加载。
1 2 3 4 5 6 7 8
| define(function () { let _moduleName = "module"; function getName() { return _moduleName; } return { getName }; });
|
1 2 3 4 5 6 7 8 9 10 11 12
| define(["module1"], function (module1) { let _firstName = "AMD"; function getFullName() { return _firstName + " " + module1.getName(); } function setFirstName(name) { _firstName = name; } return { _firstName, getFullName, setFirstName }; });
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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>"); });
|
1 2
| <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 优点。
1 2 3 4 5 6
| define(function (require, exports, module) { module.exports = { msg: "I am module1", }; });
|
1 2 3 4 5 6 7 8
| define(function (require, exports, module) { var module2 = require("./module1"); function show() { console.log("同步引入依赖模块1 " + module2.msg); } exports.showModule = show; });
|
1 2 3 4 5
| define(function (require) { var m2 = require("./modules/module2"); m2.showModule(); });
|
1 2 3 4 5
| <script type="text/javascript" src="./libs/sea.js"></script> <script type="text/javascript"> seajs.use('./main') </script>
|
AMD、CMD 区别
1 2 3 4 5 6 7 8
| define(['./a', './b'], function(a, b) { a.doSomething() ... b.doSomething() ... })
|
1 2 3 4 5 6 7 8 9 10
| define(function(require, exports, module) { var a = require('./a') a.doSomething() ... var b = require('./b') b.doSomething() ... }
|
4、ES6
2015 年,ES6 规范中,终于将模块化纳入 JavaScript 标准,从此 js 模块化被 ECMA 官方扶正,也是后来 js 的标准。
ES6 中的模块化在 CommonJS 的基础上有所不同,关键字有 import,export,default,as,from。
1 2 3 4 5 6 7 8 9
| let _moduleName = "module"; function setModuleName(name) { _moduleName = name; } function getModuleName() { return _moduleName; } export { setModuleName, getModuleName };
|
1 2 3 4
| 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 规范,成为浏览器和服务器通用的模块解决方案。