欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Javascript模块化规范之CommonJs,AMD,CMD

程序员文章站 2022-03-01 12:39:00
...

一、模块化编程背景

      对于计算机语言,模块化编程是必不可少的,对架构设计、代码复用起到至关重要的作用,工程中引入别人写好的库和模块能大大缩减开发周期。C/C++中,我们可以用include;Java中我们可以用import。可是在JS中,这些是不存在的,取而代之的是script标签。
      在浏览器端,我们可以通过script标签引入需要的库,然后加载自己的脚本,最后运行脚本。这样做似乎与include和import没什么区别,然而有几个重要的因素必须要考虑:JS是解释型语言,边加载边运行,后续脚本运行时,这些脚本所依赖的一切必须已经加载完毕;JS脚本加载时会阻塞浏览器,如果加载的JS很多很大,浏览器会卡住,带来很差的用户体验;通过调整script标签顺序可以修改JS模块之间的依赖,然而当模块很多时,这种做法就解决不了问题了。

1.什么是模块化编程?

      模块化编程是一种软件设计技术,它强调将程序的功能分为独立的,可互换的模块,以使每个模块都包含执行所需功能的一个方面所必需的一切。模块接口表示该模块提供和需要的元素。接口中定义的元素可由其他模块检测。该实现包含与接口中声明的元素相对应的工作代码。模块化程序设计与结构化程序设计和面向对象程序设计密切相关,它们的全部目标都是通过分解成较小的部分来促进大型软件程序和系统的构建。

2.Javascript模块化编程有哪些规范

@AMD 规范
@CMD 规范
AMD 是 RequireJS 在推广过程中对模块定义的规范化产出。
CMD 是 SeaJS 在推广过程中对模块定义的规范化产出。
类似的还有 CommonJS Modules/2.0 规范,是 BravoJS 在推广过程中对模块定义的规范化产出。

二、Javascript模块化编程

Javascript的模块化编程经历了从 控制script标签顺序 ⇒ CommonJs ⇒ AMD ⇒ CMD ⇒ ES6模块 的过程,

1.CommonJs

2009年Node.js横空出世,将JavaScript带到了服务器端领域。而对于服务器端来说,没有模块化那可是不行的,因此Common社区制定可CommonJs规范
CommonJs的规范主要如下:

  1. 模块的标识应遵循的规则(书写规范)。
  2. 定义全局函数require,通过传入模块标识来引入其他模块,执行的结果即为别的模块暴露出来的API。
  3. 如果被require函数引入的模块中也包含依赖,那么依次加载这些依赖。
  4. 如果引入模块失败,那么require函数应该报一个异常。
  5. 模块通过变量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;

CommonJs看起来是一个很不错的方案,拥有模块化所需要的严格的入口和出口,看起来一切都很美好,但它的一个特性却决定了它只能在服务器端大规模使用,而在浏览器端发挥不了太大的作用,那就是同步!这在服务器端不是什么问题,但放在浏览器端就出现问题了,因为文件都放在服务器上,如果网速不够快的话,前面的文件如果没有加载完成,浏览器就会失去响应!因此为了在浏览器上也实现模块化得来个异步的模块化才行!根据这个需求,我们的下一位主角——AMD就产生了!

2.AMD异步模块定义

AMD的全名叫做:Asynchronous Module Definition即异步模块定义。它采用了异步的方式来加载模块,然后在回调函数中执行主逻辑,因此模块的加载不影响它后面的模块的运行。它的规范如下:

define(id?, dependencies?, factory);
  1. 用全局函数define来定义模块;
  2. id为模块标识,遵从CommonJS Module Identifiers规范
  3. dependencies为依赖的模块数组,在factory中需传入形参与之一一对应
  4. 如果dependencies的值中有"require"、“exports"或"module”,则与commonjs中的实现保持一致
  5. 如果dependencies省略不写,则默认为[“require”, “exports”, “module”],factory中也会默认传入require,exports,module
  6. 如果factory为函数,模块对外暴漏API的方法有三种:return任意类型的数据、exports.xxx=xxx、module.exports=xxx
  7. 如果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定义非独立模块,那么它的语法就规定一定是这样的:

//比如里面某一个method 依赖Jquery,那么在define中就需要引入Jquery
define(['module1', 'module2'], function(m1, m2) { 

    return {
        method: function() {
            m1.methodA();
            m2.methodB();
        }
    }

});

define在这个时候接受两个参数,第一个参数是module是一个数组,它的成员是我们当前定义的模块所依赖的模块,只有顺利加载了这些模块,我们新定义的模块才能成功运行。第二个参数是一个函数,当前面数组内的成员全部加载完之后它才运行,它的参数m与前面的module是一一对应的。这个函数必须返回一个对象,以供其他模块调用,需要注意的是,回调函数必须返回一个对象,这个对象就是你定义的模块。
在加载模块方面,AMD和CommonJs都是使用require。require.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的时候,我们必须要提前加载所有的依赖,然后才可以使用,而不是需要使用时再加载,使得初次加载其他模块的速度较慢,提高了开发成本。

3.CMD 通用模块定义

CMD的全称是Common Module Definition,即通用模块定义。它是由蚂蚁金服的前端大佬——玉伯提出来的,实现的JavaScript库为sea.js。它和AMD的require.js很像,但加载方式不同,它是按需就近加载的,而不是在模块的开始全部加载完成。它有以下两大核心特点:

简单友好的模块定义规范:Sea.js 遵循 CMD 规范,可以像 Node.js 一般书写模块代码。
自然直观的代码组织方式:依赖的自动加载、配置的简洁清晰,可以让我们更多地享受编码的乐趣。

在CMD规范中,一个文件就是一个模块,代码书写的格式是这样的:

define(factory);

当factory为函数时,表示模块的构造方法,执行该方法,可以得到该模块对外提供的factory接口,factory 方法在执行时,默认会传入三个参数:require、exports 和 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要快一些。

4.ES6模块特性

CommonJs,由于是同步的,所以主要应用于服务器端,以Node.js为代表。
AMD,异步模块定义,预加载,推荐依赖前置。以require.js为代表。
CMD,通用模块加载,懒加载,推荐依赖就近。以Sea.js为代表。
而在ES6已经大行其道的今天,ES6中所提供的模块化的方法也自然而然成了我们进行JavaScript模块化编程的标准,因此ES6模块的语法虽然在一些较为老的浏览器上不能直出,需要进行转译,但却代表着未来的JavaScript发展趋势。

4.1 ES模块特性

在ES6中将模块认为是自动运行在严格模式下并且没有办法退出运行的JavaScript代码。在一个模块中定义的变量不会自动被添加到全局共享的作用域之中,这个变量只能作用在这个作用域中。此外模块还必须导出一些外部文件可以访问的元素,以供其他模块或代码使用。

除了这个基本特性,ES6模块还有两大特性也十分重要,需要额外注意:

首先是在模块的顶部this值是undefined,这是由于在ES6中的模块的代码是在严格模式下执行的。(如果对this不是很熟悉的可以去看:深入浅出this关键字
其次,模块不支持HTML风格的代码注释,这是早期浏览器所遗留下的JavaScript特性,在ES6的语法里不予支持。

4.2 模块加载

首先我们来看浏览器是如何加载模块的。其实在ES6规范出来之前,web浏览器就规定了三种方式来引入JavaScript文件:

在没有src属性的

// 第一种方式
<script type=""module>
    import { add } from "./example";
    let num = add(1, 1);
</script>
//  第二种方式
<script type="module" src="example.js">
// 第三种方式,以脚本的方式加载example.js
let worker = new Worker("example.js");

当HTML解析器遇到

4.3 导出

在ES6中我们可以使用export关键字将一部分代码暴露给其他模块,以供其他模块或代码使用。先让我们来看看export关键字在MDN的定义吧:
export语句用于在创建JavaScript模块时,从模块中导出函数、对象或原始值,以便其他程序可以通过 import 语句使用它们。(
此特性目前仅在 Safari 和 Chrome 原生实现。它在许多转换器中实现,如Traceur Compiler,Babel或Rollup。)
通过MDN的定义我们可以知道:export关键字可以将其放在任何函数、对象或原始值前面,从而将它们从模块中导出。示例如下:

//   ./example.js
// 导出变量
export var a = 1;
// 导出函数
export function addA(value) {
    return value + a;
}
//导出类
export class add1 {
    constructor(value) {
        this.value = value + a;
    }
}
//这个函数就是这个模块所私有的,在外部不能访问它
function say1() {
    console.log('我是不是很帅');
}
//这又是个函数
function say2() {
    console.log('没错我就是很帅');
}
//在后面对函数进行导出,它就不是私有的了
export say2;

需要注意的是:使用export导出的函数和类都需要一个名称,除非使用default关键字,否则就不能用这个方法导出匿名函数或类。所以当我们需要导出匿名的函数或者类时,我们可以这么做:

//   ./example.js
//导出匿名函数
export default function(a, b) {
    return a + b;
}
//或者导出匿名的类
export default class {
consturctor(value) {
    this.value = value + 1;
    }
}

具体关于default关键字的用法我会在后面做具体介绍,现在只需记住:当我们需要导出匿名的函数或者类时要使用export default语法。

4.4 导入

在ES6中,从模块中导入的功能可以通过import关键字。import语句由两部分组成:要导入元素的标识符和元素应当从哪个模块导入。

//  ./say.js
import { say2 } from "./example.js";
console.log(say2()); // '没错我就是很帅'

import 后面的大括号中的say2表示从规定模块导入的元素的名称。关键字from后面的字符串则表示要导入的模块的路径,这通常是包含模块的.js文件的相对或绝对路径名,需要注意的是只允许使用单引号和双引号的字符串来包裹路径,浏览器使用的路径格式与传给

除此之外,我们还可以导入多个元素或者直接导入整个模块:

// 导入多个元素
improt { a, addA, say2 } from "./example.js";
console.log(a); // 1
sonsole.log(addA(1); // 2

// 导入整个模块
import * as example from "./example.js"
console.log(example.a); // 1
sonsole.log(example.addA(1); // 2
console.log(example.say2()); // '没错我就是很帅'

上面的导入整个模块就是把example.js中导出的所有元素全部加载到一个叫做example的对象中,而所导出的元素就会作为example的属性被访问。因为example对象是作为example.js中所导出成员的命名空间对象而被创建的,所以这种导入方式被称为命名空间导入(name space import)。

还有一点要注意的是,不管import语句把一个模块写了多少次,该模块只执行一次。意思就是,在首次执行导入模块后,实例化的模块就会被保存在内存中,只要使用import语句引用它就可以重复使用它:

// 首次导入需要加载模块example.js
import { a } from "./example.js"
// 下面的两个import将无需加载example.js了
import { addA } from "./example.js"
import { say2 } from "./example.js"

当从模块中导入一个元素时,它与const是一样无法定义另一个同名变量和导入一个同名元素,也无法在import语句前使用元素或者改变导出的元素的值:

//接上面的代码

say2 = 1 ;  //会抛出一个错误

由于ES6的import语句为导入的元素创建的是只读绑定的标识符,而不是原始绑定。因此元素只有在被导出的模块中才可以被修改,即使是将该模块的全部导入也无法修改其中的元素。

//   ./example.js
// 这是一个函数
export function setA(newA) {
    a = newA;
}
//  ./say.js
import { a, setA } from "./example";
console.log(a);  // 1
a = 2;   //抛出错误

// 所以我们得这么做
setA(2); 
console.log(a);  // 2

调用setA(2)时会返回到example.js中去执行,将a设置为2。由于say.js导入的只是a的只读绑定的标识符而已,因此会自动进行更改。

4.5 语法限制

export和import在语法上还有一个重要的限制,那就是他们必须在条件语句和函数之外使用,例如:

if (ture) {
    export var a = 1;      //语法错误
}
function imp() {
    import a from "./example.js"; //语法错误
}

由于模块语法存在的其中一个原因是让JavaScript引擎可以静态地确定哪些代码是可以导出的,因此export和import语句被设计成静态的,不能进行任何形式的动态导出或导入。

4.6 重命名

有时在开发中,我们在导入一些元素后不想使用它们的原始名称了,我们就可以在导出过程或者导入过程中去改变导出元素的名称:

// 导出过程
function add(a, b) {
    return a + b;
}
export { add as add1 };  //在导入过程中必须使用add1作为名称 

// 导入过程
import {add as add1 } from "./example"
console.log(add1(1,1));  // 2
console.log(typeof add); //undefined

4.7模块默认值

在CommonJS等其他的模块化规范中,从模块中导出或导入默认值是一个常见的用法,因此在ES6中也延用了这种用法并进行了优化。在ES6中我们可以使用default关键字来指定默认值,并且一个模块只能默认一个导出值:

// ./example.js
// 第一种默认导出语法
export default function(a, b) {
    return a + b;
}
// 第二种默认导出语法
function add(a, b) {
    return a + b;
}
export default add;
// 第三种默认导出语法
function add(a, b) {
    return a + b;
}
export { add as default };

需要注意的是第三种语法,default关键字虽然不能作为元素的名称,但可以作为元素的属性名称,因此可以使用as语法将add函数的属性设置为default。

导入默认值的语法则是这样的:

//  第一种语法
import add from "./example";
//  第二种语法
import { default as add } from "./example";

有些朋友可能会发现,我们的第一种语法中import关键字后面并没有加大括号,认为这是错误的。其实这是导入默认值的独特语法,在这的本地名称add用于表示模块导出的任何默认函数,这种语法是最纯净的,ES6标准创建团队的大佬们也希望这种语法能成为web主流的模块导入形式。
匿名导入函数也是如此:

//   ./example.js
//导出匿名函数
export default function(a, b) {
    return a + b;
}
// ./say.js
import add from "./example";
console.log(add(1,1));  // 2

4.7 导出已导入元素

我们同样可以在本模块内导出我们在本模块内导入的元素,有以下几种语法:

//  第一种语法
import { add } from ./example.js;
export { add };

//  第二种语法
export { add } from ./example.js;

//换一个名称导出
export { add as add1 } from ./example.js; //以add这个名称导入,再以add1的名称导出

// 导出整个模块
export *  from ./example.js;

参考资料:
@Javascript模块化编程一
@Javascript模块化编程二
@Javascript模块化规范
@JS模块化编程
@AMD、CMD、CommonJs、ES6的对比

上一篇: 合并单元格等

下一篇: ECharts入门