前端模块化——完全搞懂AMD、CMD、ESM和CommonJS

咱们知道,在NodeJS以前,因为没有过于复杂的开发场景,前端是不存在模块化的,后端才有模块化。NodeJS诞生以后,它使用CommonJS的模块化规范。今后,js模块化开始快速发展。javascript

模块化的开发方式能够提供代码复用率,方便进行代码的管理。一般来讲,一个文件就是一个模块,有本身的做用域,只向外暴露特定的变量和函数。目前流行的js模块化规范有CommonJS、AMD、CMD以及ES6的模块系统。下面开始一一介绍:php

CommonJS

NodeJS是CommonJS规范的主要实践者,它有四个重要的环境变量为模块化的实现提供支持:moduleexportsrequireglobal。实际使用时,用module.exports定义当前模块对外输出的接口(不推荐直接用exports),用require加载模块。html

复制// 定义模块math.js var basicNum = 0; function add(a, b) { return a + b; } module.exports = { //在这里写上须要向外暴露的函数、变量 add: add, basicNum: basicNum } /** 必须加./路径,不加的话只会去node_modules文件找 **/ // 引用自定义的模块时,参数包含路径,可省略.js var math = require('./math'); math.add(2, 5); // 引用核心模块时,不须要带路径 var http = require('http'); http.createService(...).listen(3000);

CommonJS用同步的方式加载模块。在服务端,模块文件都存放在本地磁盘,读取很是快,因此这样作不会有问题。可是在浏览器端,限于网络缘由,更合理的方案是使用异步加载。前端

exportsmodule.export区别:java

exports:对于自己来说是一个变量(对象),它不是module的引用,它是{}的引用,它指向module.exports的{}模块。只能使用.语法 向外暴露变量。node

module.exportsmodule是一个变量,指向一块内存,exportsmodule中的一个属性,存储在内存中,而后exports属性指向{}模块。既可使用.语法,也可使用=直接赋值。jquery

AMD和require.js

AMD规范采用异步方式加载模块,模块的加载不影响它后面语句的运行。全部依赖这个模块的语句,都定义在一个回调函数中,等到加载完成以后,这个回调函数才会运行。这里介绍用require.js实现AMD规范的模块化:用require.config()指定引用路径等,用definde()定义模块,用require()加载模块。webpack

首先咱们须要引入require.js文件和一个入口文件main.js。main.js中配置require.config()并规定项目中用到的基础模块。es6

复制/** 网页中引入require.js及main.js **/ <script src="js/require.js" data-main="js/main"></script> /** main.js 入口文件/主模块 **/ // 首先用config()指定各模块路径和引用名 require.config({ baseUrl: "js/lib", paths: { "jquery": "jquery.min", //实际路径为js/lib/jquery.min.js "underscore": "underscore.min", } }); // 执行基本操做 require(["jquery","underscore"],function($,_){ // some code here });

引用模块的时候,咱们将模块名放在[]中做为reqiure()的第一参数;若是咱们定义的模块自己也依赖其余模块,那就须要将它们放在[]中做为define()的第一参数。web

复制// 定义math.js模块 define(function () { var basicNum = 0; var add = function (x, y) { return x + y; }; return { add: add, basicNum :basicNum }; }); // 定义一个依赖underscore.js的模块 define(['underscore'],function(_){ var classify = function(list){ _.countBy(list,function(num){ return num > 30 ? 'old' : 'young'; }) }; return { classify :classify }; }) // 引用模块,将模块放在[]内 require(['jquery', 'math'],function($, math){ var sum = math.add(10,20); $("#sum").html(sum); });

CMD和sea.js

AMD的实现者require.js在申明依赖的模块时,会在第一时间加载并执行模块内的代码:

复制define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) { // 等于在最前面声明并初始化了要用到的全部模块 if (false) { // 即使没用到某个模块 b,但 b 仍是提早执行了。**这就CMD要优化的地方** b.foo() } });

CMD是另外一种js模块化方案,它与AMD很相似,不一样点在于:AMD推崇依赖前置、提早执行,CMD推崇依赖就近、延迟执行。此规范实际上是在sea.js推广过程当中产生的。

复制/** AMD写法 **/ define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) { // 等于在最前面声明并初始化了要用到的全部模块 a.doSomething(); if (false) { // 即使没用到某个模块 b,但 b 仍是提早执行了 b.doSomething() } }); /** CMD写法 **/ define(function(require, exports, module) { var a = require('./a'); //在须要时申明 a.doSomething(); if (false) { var b = require('./b'); b.doSomething(); } }); /** sea.js **/ // 定义模块 math.js define(function(require, exports, module) { var $ = require('jquery.js'); var add = function(a,b){ return a+b; } exports.add = add; }); // 加载模块 seajs.use(['math.js'], function(math){ var sum = math.add(1+2); });

ES6 Module

ES6 在语言标准的层面上,实现了模块功能,并且实现得至关简单,旨在成为浏览器和服务器通用的模块解决方案。其模块功能主要由两个命令构成:exportimportexport命令用于规定模块的对外接口,import命令用于输入其余模块提供的功能。

复制/** 定义模块 math.js **/ var basicNum = 0; var add = function (a, b) { return a + b; }; export { basicNum, add }; /** 引用模块 **/ import { basicNum, add } from './math'; function test(ele) { ele.textContent = add(99 + basicNum); }

如上例所示,使用import命令的时候,用户须要知道所要加载的变量名或函数名。其实ES6还提供了export default命令,为模块指定默认输出,对应的import语句不须要使用大括号。这也更趋近于ADM的引用写法。

复制/** export default **/ //定义输出 export default { basicNum, add }; //引入 import math from './math'; function test(ele) { ele.textContent = math.add(99 + math.basicNum); }

ES6的模块不是对象,import命令会被 JavaScript 引擎静态分析,在编译时就引入模块代码,而不是在代码运行时加载,因此没法实现条件加载。也正由于这个,使得静态分析成为可能。

ES6 模块的特征:

  • 严格模式:ES6 的模块自动采用严格模式
  • import read-only特性: import的属性是只读的,不能赋值,相似于const的特性
  • export/import提高: import/export必须位于模块顶级,不能位于做用域内;其次对于模块内的import/export会提高到模块顶部,这是在编译阶段完成的

ES6 模块与 CommonJS 模块的差别

1. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用
  • CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值
  • ES6 模块的运行机制与 CommonJS 不同。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的import有点像 Unix 系统的“符号链接”,原始值变了,import加载的值也会跟着变。所以,ES6 模块是动态引用,而且不会缓存值,模块里面的变量绑定其所在的模块。
2. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口
  • 运行时加载: CommonJS 模块就是对象;即在输入时是先加载整个模块,生成一个对象,而后再从这个对象上面读取方法,这种加载称为“运行时加载”。
  • 编译时加载: ES6 模块不是对象,而是经过 export 命令显式指定输出的代码,import时采用静态命令的形式。即在import时能够指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”。模块内部引用的变化,会反应在外部。

CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

废话很少说,直接看代码:

首先看个CommonJS输出拷贝的例子:

复制// a.js let a = 1; let b = { num: 1 } setTimeout(() => { a = 2; b = { num: 2 }; }, 200); module.exports = { a, b, }; // main.js // node main.js let {a, b} = require('./a'); console.log(a); // 1 console.log(b); // { num: 1 } setTimeout(() => { console.log(a); // 1 console.log(b); // { num: 1 } }, 500);

所谓输出拷贝,若是了解过 NodeJS 或者 webpack 对 CommonJS 的实现(不了解能够看这篇文章),就会知道:exports对象是模块内外的惟一关联, CommonJS 输出的内容,就是exports对象的属性,模块运行结束,属性就肯定了

再看ES6 Module输出的例子:

复制// a.mjs let a = 1; let b = { num: 1 } setTimeout(() => { a = 2; b = { num: 2 }; }, 200); export { a, b, }; // main.mjs // node --experimental-modules main.mjs import {a, b} from './a'; console.log(a); // 1 console.log(b); // { num: 1 } setTimeout(() => { console.log(a); // 2 console.log(b); // { num: 2 } }, 500);

以上就是 ES6 Module 输出引用和 CommonJS 输出值的区别,模块内部引用的变化,会反应在外部,这是 ES6 Module 的规范。

总结

  1. AMD/CMD/CommonJs 是js模块化开发的规范,对应的实现是require.js/sea.js/Node.js

  2. CommonJs 主要针对服务端,AMD/CMD/ES Module主要针对浏览器端,容易混淆的是AMD/CMD。(顺便提一下,针对服务器端和针对浏览器端有什么本质的区别呢?服务器端通常采用同步加载文件,也就是说须要某个模块,服务器端便停下来,等待它加载再执行。这里若是有其余后端语言,如java。而浏览器端要保证效率,须要采用异步加载,这就须要一个预处理,提早将所须要的模块文件并行加载好。)

  3. AMD/CMD区别,虽然都是并行加载js文件,但仍是有所区别,AMD是预加载,在并行加载js文件同时,还会解析执行该模块(由于还须要执行,因此在加载某个模块前,这个模块的依赖模块须要先加载完成);而CMD是懒加载,虽然会一开始就并行加载js文件,可是不会执行,而是在须要的时候才执行。

  4. AMD/CMD的优缺点.一个的优势就是另外一个的缺点, 能够对照浏览。
    AMD优势:加载快速,尤为遇到多个大文件,由于并行解析,因此同一时间能够解析多个文件。
    AMD缺点:并行加载,异步处理,加载顺序不必定,可能会形成一些困扰,甚至为程序埋下大坑。

    CMD优势:由于只有在使用的时候才会解析执行js文件,所以,每一个JS文件的执行顺序在代码中是有体现的,是可控的。

    CMD缺点:执行等待时间会叠加。由于每一个文件执行时是同步执行(串行执行),所以时间是全部文件解析执行时间之和,尤为在文件较多较大时,这种缺点尤其明显。(PS:从新看这篇文章,发现这里写的不是很准确。确切来讲,JS是单线程,全部JS文件执行时间叠加在AMD和CMD中是同样的。可是CMD是使用时执行,无法利用空闲时间,而AMD是文件加载好就执行,每每能够利用一些空闲时间。这么来看,CMD比AMD的优势仍是很明显的,毕竟AMD加载好的时候也未必就是JS引擎的空闲时间!)

  5. CommonJS 和 ES Module 区别:CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用

  6. 如何使用?CommonJs 的话,由于 NodeJS 就是它的实现,因此使用 node 就行,也不用引入其余包。AMD则是经过<script>标签引入require.js,CMD则是引入sea.js

文章来源 : https://www.cnblogs.com/chenwenhao/p/12153332.html

相关文章
相关标签/搜索