随着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会失效。
PS:小白初文,文笔不好,望大佬轻喷,俺只是单纯为了混缸子来的而已。