随着Javascript语言的发展,前端世界对于模块化的定义也是越来越趋向于成熟了,本文来探索一下前端模块化的一些常见的概念。(AMD、CMD、CommonJS、ES Module、UMD)
前言
Javascript作为嵌入式的脚本语法,属于弱类型语言,没有像Java那些强类型语言,拥有类的概念,更不用说模块(module)了。而我们常说的“模块化”其实是开发者模仿Java世界的一个重要概念——package拟出的抽象概念,主要是来隔离、组织复杂的JS代码,模块内是一个相对独立的空间,不用担心全局环境污染、命名冲突什么的,如果外部要使用也只需做一个导入操作即可。
对于学前端的同学,我想应该会经常看到AMD、CMD、CommonJS、ES Module这几个名词吧,对模块化规范了解深一点的同学可能还听过UMD以及其他一些模块化发展史的东西。反正呢,这些东西究其缘由也没啥本质区别,无非就是语法、用途有所区别,只要记住他们就是用来规范你的代码。
简介
AMD
AMD 全称即Asynchronous Module Definition,中文名是异步模块定义的意思,它是一个作用在浏览器端模块化开发的规范。由于不是JavaScript原生支持,使用AMD规范进行开发需要用到对应的库函数,也就是大名鼎鼎RequireJS,实际上AMD是RequireJS在推广过程中对模块定义的规范化的产出。RequireJS其核心是定义了一个define函数:define(id?, dependencies?, factory);
基本用法:
define('moduleA',function() {
return {
a: 1
}
});
复制代码
CMD CMD 全称即Common Module Definiton,中文名为通用模块定义规范的意思,它也是运行于浏览器之上的。CMD规范是国内发展出来的,就是AMD有个RequireJS一样,CMD在浏览器端也有个SeaJS的经典实现,其作者是在淘系的玉伯。CMD推崇的是就近原则,所以一般不在define的参数中写依赖,在factory中写。其也推广一个模块就是一个文件。
核心函数:define(factory)
基本用法:
define(function(require, exports, module) {
module.exports = {
a: 1
}
})
复制代码
CommonJS
CommonJS是一种JavaScript模块化规范,它是一个超级大的概念,和ESMAScript规范一样,它是整个语言层面上规范。和前两种相比主要区别是它应用在服务端,我们最熟悉的NodeJS就是使用了它。CommonJS规范中,每一个文件就是一个模块,拥有自己独立的作用域、变量、以及方法等,对其他的模块都不可见。CommonJS规范规定,每个模块内部,module 变量代表当前模块。这个变量是一个对象,它的 exports 属性(module.exports)是对外的接口。加载某个模块,其实是加载该模块的 module.exports 属性。基本用法:
// a.js
module.exports = {
a: 1
}
// b.js
var moduleA = require('./a.js');
console.log(moduleA.a)
复制代码
ES Module
ES6之前Javascript一直没有属于自己的模块规范,所以社区制定了 CommonJs规范,所以在ES6出来时提出自己的模块化规范,也就是ES Module。基本用法:
// a.js
let a = 1;
export { a }
// b.js
import { a } from './a.js';
复制代码
UMD
UMD 全称即Universal Module Definition,中文名为通用模块定义规范的意思。也是随着大前端的趋势所诞生,它可以通过运行时或者编译时让同一个代码模块在使用 CommonJs、CMD 甚至是 AMD 的项目中运行。未来同一个 JavaScript 包运行在浏览器端、服务区端甚至是 APP 端都只需要遵守同一个写法就行了。它没有自己专有的规范,是集结了 CommonJs、CMD、AMD 的规范于一身。基本用法:
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define(['jquery'], factory);
} else if (typeof exports === 'object') {
// CommonJS
module.exports = factory(require('jquery'));
} else {
// 全局变量
root.returnExports = factory(root.jQuery);
}
}(this, function ($) {
// ...
}));
复制代码
UMD大致就是一个大集合吧,有兴趣的同学可以深入去研究一下,现在也比较少会谈及它了,连AMD、CMD都渐渐的淡出了人们的视野了。
运行时加载 与 编译时加载
尽管模块化的处理操作能使我们很好的处理我们日益膨胀的代码量,在巨大的项目里妥善的管理不能功能模块的代码,这些非常好。但是我们在使用模块话处理时,还是应该有很多需要注意的事情,其中比较重要的就是“模块的导入”了,当一个页面或者一个完整的项目导入过多的模块引来的效率、性能方面的问题,这就不是我们想要看到的。下面我们介绍一下在CommonJS和ES Module中两个比较重要的概念“运行时加载”与“编译时加载”。
运行时加载
// CommonJS模块, 运行时才会得到该变量
let { stat, exists, readFile } = require('fs');
// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;
复制代码
大家可以仔细瞧一下上面代码,代码实质是加载fs模块(即加载fs的所有方法),生成一个对象(_fs),然后再从这个对象上面读取 3 个方法。这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。
编译时加载
ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。
// ES6模块
import { stat, exists, readFile } from 'fs';
复制代码
上面代码的实质是从fs模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。
优点
由于 ES6 模块是编译时加载,使得静态分析成为可能。有了它,就能进一步拓宽 JavaScript 的语法,比如引入宏(macro)和类型检验(type system)这些只能靠静态分析实现的功能。除了静态加载带来的各种好处,ES6 模块还有以下好处:(1)不再需要UMD模块格式了,将来服务器和浏览器都会支持 ES6 模块格式。目前,通过各种工具库,其实已经做到了这一点。(2)将来浏览器的新 API 就能用模块格式提供,不再必须做成全局变量或者navigator对象的属性。(3)不再需要对象作为命名空间(比如Math对象),未来这些功能可以通过模块提供。
对比ES Module 与 CommonJS 导入和导出
ES Module | CommonJS | |
---|---|---|
导入 | import | require |
导出 | export、export default | exports、module.exports |
ES Module的导入和导出
import是在编译过程中执行,也就是在代码执行前执行,因为import有提升效果,提升到整个模块的头部,所以import不一定要写到最前面,但一般都写在最前面吧。import是静态执行,不能使用变量或者表达式,因为呢,代码都还没执行呢。
//错误1
var url = './test';
import { a,b } from url;
//错误2
let status= true;
if(status){ import { a,b } from url; }
复制代码
import可以使用as进行重命名,但是这和导出的时候有很大关联。
一个模块可以有多个export,但是只能有一个export default。export default可以和多个export共存。我个人更喜欢将export理解为单个导出,export default为批量导出。两者能在同个文件中混用,但一般不建议怎么用。
export default a的含义是将变量a的值赋给变量default。
//实例1
//test.js
const a = 'yd';
export {a}
//app.js
import {a} from './test.js'// 这里两个文件中的a就是同一个
console.log(a);// yd
//实例2
//test.js
const a = 'yd';
export default {a}
//app.js
import a from './test.js'// 这里两个文件中的a是不同的
console.log(a);// { a: 'yd' }
复制代码
as重命名:两者都支持重命名,但是也有点区别。
//test.js
const a = 'aaa';
const b = 'bbb';
const c = 'ccc';
const d = 'ddd';
function fn() {
console.log(`fn执行`);
}
export {a}
export {b}
export default { c, d, fn}
//app.js
import {a as A} from './test'; // aaa
import {* as A} from './test'; // 这是不支持的
import * as obj from './test';
//obj => {a:'aaa',b:'ccc',default:{c:'ccc',d:'ddd',fn: [Function: fn] }}
//结果就是在import {} 中不支持使用*
复制代码
CommonJS 导入和导出
require是运行时调用,所以require理论上可以运用在代码的任何地方。require支持动态引入。
//dome1
let flag = true;
if (flag) {
const a = require('./test.js');
console.log(a); //这是被支持
}
//dom2
let flag = true;
let url = './test.js';
if (flag) {
const a = require(url);
console.log(a) //这是被支持
}
复制代码
根据规范,每个文件就是一个模块,有自己的作用域,文件里面每个变量、函数、类都是私有的,对其他文件不可见。
每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性是对外的接口,加载某个模块实际是加载该模块的module.exports属性。每个模块其实是被一个匿名函数包裹着,这是模块私有的本质。
(function (exports, require, module, __filename, __dirname) { }))
exports 等价于 module.exports,exports是一个空对象,可以被覆盖,但这容易切断两者的联系。
//test.js;
const a = 'aaa';
const b = 'bbb';
const c = 'ccc';
module.exports = { c } //切断了联系,exports失效了
exports.b = b ; //没有了
module.exports.a = a;
//app.js
const obj = require('./test.js');
console.log(obj); //{ c: 'ccc', a: 'aaa' }
复制代码
简单总结
(1)require,exports,module.export属于CommonJS规范,import,export,export default属于ES Module规范。
(2)require支持动态导入,动态匹配路径,import对这两者都不支持。
(3)require是运行时调用,import是编译时调用。
(4)require是赋值过程,import是解构过程。
(6)对于export和export default 不同的使用方式,import就要采取不同的引用方式,主要区别在于是否存在 {},export导出的,import导入需要{},导入和导出一一对应,export default默认导出的,import导入不需要{}。
(7)exports是module.export一种简写形式,不能直接给exports赋值当直接给module.export赋值时,exports会失效。