在早期编写 JavaScript 时, 我们只需在 <script> 标签内写入 JavaScript 的代码就可以满足我们对页面交互的需要了但随着时间的推移, 时代的发展, 原本的那种简单粗暴的编写方式所带来的诸如逻辑混乱, 页面复杂, 可维护性差, 全局变量暴露等问题接踵而至, 前辈们为了解决这些问题提出了很种的解决方案, 其中之一就是 JavaScript 模块化编程总的来说, 它有以下四种优点:
解决项目中的全局变量污染的问题
开发效率高, 有利于多人协同开发
职责单一, 方便代码复用和维护
解决文件依赖问题, 无需关注引用文件的顺序
先行者 CommonJs
2009 年 Node.js 横空出世, 将 JavaScript 带到了服务器端领域而对于服务器端来说, 没有模块化那可是不行的因此 CommonJs 社区的大牛们开始发力了, 制定了一个与社区同名的关于模块化的规范 CommonJs 它的规范主要如下:
模块的标识应遵循的规则(书写规范)
定义全局函数 require, 通过传入模块标识来引入其他模块, 执行的结果即为别的模块暴露出来的 API
如果被 require 函数引入的模块中也包含依赖, 那么依次加载这些依赖
如果引入模块失败, 那么 require 函数应该报一个异常
模块通过变量 exports 来向外暴露 API,exports 只能是一个对象, 暴露的 API 须作为此对象的属性
根据 CommonJS 规范的规定, 每个文件就是一个模块, 有自己的作用域, 也就是在一个文件里面定义的变量函数类, 都是私有的, 对其他文件是不可见的通俗来讲, 就是说在模块内定义的变量和函数是无法被其他的模块所读取的, 除非定义为全局对象的属性
- // addA.js
- const a = 1;
- const addA = function(value) {
- return value + a;
- }
上面代码中, 变量 a 和函数 addA, 是当前文件 addA.js 私有的, 其他文件不可见如果想在多个文件中分享变量 a, 必须定义为 global 对象的属性:
global.a = 1;
这样我们就能在其他的文件中访问变量 a 了, 但这种写法不可取, 输出模块对象最好的方式是 module.exports:
- // addA.js
- var a = 1;
- var addA = function(value) {
- return value + x;
- }
- module.exports.addA = addA;
上面代码通过 module.exports 对象输出了一个函数, 该函数就是模块外部与内部通信的桥梁加载模块需要使用 require 方法, 该方法读取一个文件并执行, 最后返回文件内部的 module.exports 对象
- var example = require('./addA.js');
- console.log(example.addA(1)); //2
CommonJs 看起来是一个很不错的选择, 拥有模块化所需要的严格的入口和出口, 看起来一切都很美好, 但它的一个特性却决定了它只能在服务器端大规模使用, 而在浏览器端发挥不了太大的作用, 那就是同步! 这在服务器端不是什么问题, 但放在浏览器端就出现问题了, 因为文件都放在服务器上, 如果网速不够快的话, 前面的文件如果没有加载完成, 浏览器就会失去响应! 因此为了在浏览器上也实现模块化得来个异步的模块化才行! 根据这个需求, 我们的下一位主角 AMD 就产生了!
AMD 异步模块定义
AMD 的全名叫做: Asynchronous Module Definition 即异步模块定义它采用了异步的方式来加载模块, 然后在回调函数中执行主逻辑, 因此模块的加载不影响它后面的模块的运行它的规范如下:
define(id?, dependencies?, factory);
用全局函数 define 来定义模块;
id 为模块标识, 遵从 CommonJS Module Identifiers 规范
dependencies 为依赖的模块数组, 在 factory 中需传入形参与之一一对应
如果 dependencies 的值中有 "require""exports"或"module", 则与 commonjs 中的实现保持一致
如果 dependencies 省略不写, 则默认为["require", "exports", "module"],factory 中也会默认传入 require,exports,module
如果 factory 为函数, 模块对外暴漏 API 的方法有三种: return 任意类型的数据 exports.xxx=xxxmodule.exports=xxx
如果 factory 为对象, 则该对象即为模块的返回值
具体分析 AMD 我们通过 require.js 来进行 require.js 是一个非常小巧的 JavaScript 模块载入框架, 是 AMD 规范最好的实现者之一, require.js 的出现主要是来解决两个问题:
实现 JavaScript 文件的异步加载, 避免网页失去响应
管理模块的依赖性, 管理模块的相互独立性, 也就是我们常说的低耦合, 这有利于代码的编写与维护
使用 require.js 我们首先要加载它, 为了避免浏览器未响应, 我们在后面可以加上 async, 告诉浏览器这个文件需要异步加载(IE 不支持该属性, 所以需要把 defer 也加上):
<script src="js/require.js" defer async="true"></script>
定义模块时, 在 require.js 中我们可以使用 define, 但 define 对于需要定义的模块是否是独立的模块的写法是不同; 所谓的独立模块就是指不依赖于其他模块的模块, 而非独立模块就是指不依赖于其他模块的模块
define 在定义独立模块时有两种写法, 一种是直接定义对象; 另一种是定义一个函数, 在函数内的返回值就是输出的模块了:
- define({
- method1: function() {},
- method2: function() {},
- });
- // 等价于
- define(function () {
- return {
- method1: function() {},
- method2: function() {},
- }
- });
如果 define 定义非独立模块, 那么它的语法就规定一定是这样的:
- define(['module1', 'module2'], function(m1, m2) {
- return {
- method: function() {
- m1.methodA();
- m2.methodB();
- }
- }
- });
define 在这个时候接受两个参数, 第一个参数是 module 是一个数组, 它的成员是我们当前定义的模块所依赖的模块, 只有顺利加载了这些模块, 我们新定义的模块才能成功运行第二个参数是一个函数, 当前面数组内的成员全部加载完之后它才运行, 它的参数 m 与前面的 module 是一一对应的这个函数必须返回一个对象, 以供其他模块调用, 需要注意的是, 回调函数必须返回一个对象, 这个对象就是你定义的模块
在加载模块方面, AMD 和 CommonJs 都是使用 requirerequire.js 也同样如此, 它要求两个参数: module,callback:
require([module], callback);
第一个参数[module], 是一个数组, 里面的成员就是需要加载的模块; 第二个参数 callback, 则是加载成功之后的回调函数 require 方法本身也是一个对象, 它带有一个 config 方法, 用来配置 require.js 运行参数 config 方法接受一个对象作为参数
- // 别名配置
- requirejs.config({
- paths: {
- jquery: [ // 如果第一个路径不能完成加载, 就调到第二个路径继续进行加载
- '//cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js',
- 'lib/jquery' // 本地文件中不需要写. js
- ]
- }
- });
- // 引入模块, 用变量 $ 表示 jquery 模块
- requirejs(['jquery'], function ($) {
- $('body').CSS('background-color','black');
- });
虽然 require.js 实现了异步的模块化, 但它仍然有一些不足的地方, 在使用 require.js 的时候, 我们必须要提前加载所有的依赖, 然后才可以使用, 而不是需要使用时再加载, 使得初次加载其他模块的速度较慢, 提高了开发成本
CMD 通用模块定义
CMD 的全称是 Common Module Definition, 即通用模块定义它是由蚂蚁金服的前端大佬玉伯提出来的, 实现的 JavaScript 库为 sea.js 它和 AMD 的 require.js 很像, 但加载方式不同, 它是按需就近加载的, 而不是在模块的开始全部加载完成它有以下两大核心特点:
简单友好的模块定义规范: Sea.js 遵循 CMD 规范, 可以像 Node.js 一般书写模块代码
自然直观的代码组织方式: 依赖的自动加载配置的简洁清晰, 可以让我们更多地享受编码的乐趣
在 CMD 规范中, 一个文件就是一个模块, 代码书写的格式是这样的:
define(factory);
当 factory 为函数时, 表示模块的构造方法, 执行该方法, 可以得到该模块对外提供的 factory 接口, factory 方法在执行时, 默认会传入三个参数: requireexports 和 module:
- // 所有模块都通过 define 来定义
- define(function(require, exports, module) {
- // 通过 require 引入依赖
- var $ = require('jquery');
- var Spinning = require('./spinning');
- // 通过 exports 对外提供接口
- exports.doSomething = ...
- // 或者通过 module.exports 提供整个接口
- module.exports = ...
- });
它与 AMD 的具体区别其实我们也可以通过代码来表现出来, AMD 需要在模块开始前就将依赖的模块加载出来, 即依赖前置; 而 CMD 则对模块按需加载, 即依赖就近, 只有在需要依赖该模块的时候再 require 就行了:
- // 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()
- // ...
- });
需要注意的是 Sea.js 的执行模块顺序也是严格按照模块在代码中出现 (require) 的顺序
从运行速度的角度来讲, AMD 虽然在第一次使用时较慢, 但在后面再访问时速度会很快; 而 CMD 第一次加载会相对快点, 但后面的加载都是重新加载新的模块, 所以速度会慢点总的来说, require.js 的做法是并行加载所有依赖的模块, 等完成解析后, 再开始执行其他代码, 因此执行结果只会 "停顿"1 次, 而 Sea.js 在完成整个过程时则是每次需要相应模块都需要进行加载, 这期间会停顿是多次的, 因此 require.js 从整体而言相对会比 Sea.js 要快一些
来源: https://juejin.im/entry/5aab121a6fb9a028b6174152