前端模块化总结(commonJs,AMD,CMD,ES6 Module)
前端模块化已经不是一个新技术了,但是在项目中还是碰到一些不太明白各种引入,导出模块的方法的使用和区别的小伙伴。所以还是想总结一下形成文档,来龙去脉搞清楚了,用起来自然不会混淆。
--------------------------------------------------------------
补充:在基于webpack的前端工程里,代码里使用了import做模块导入,module.exports 做导出,这个写法也是可以运行的,因为webpack的模块化遵循commonJs规范,而es6的import语句会被转换成__webpack_require__();
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
- 关于commonJs
对于javaScript语言来说,在ES6标准之前,它是没有模块化的概念的,commonJs规范的提出,也不是来解决JS模块化的问题。commonJs规范提出的本 意,是用来补充ES缺失的规范,希望JS可以能够在任何地方运行,并具备开发大型应用的基础能力,而不是单单作为web脚本来使用。其中,nodeJs的模块系统,就遵循了commonJs规范。
加载方式:同步加载,根据模块在代码中出现的先后顺序加载;也就是说,只有加载完成,才能执行后面的操作。这样就会对浏览器造成阻塞。而服务器端的文件相当于在本地存储,不存在请求网络等问题。故commonJs规范被认为不适合浏览器环境而适合服务器端;
执行特点:第一次加载时执行,并缓存执行结果,输出的是一个值的拷贝,即一旦输出一个值,模块内部的变化就影响不到这个值,自然也没有动态更新;
关键字:
require:加载外部模块,并返回模块的exports对象;该对象为一个值的拷贝。
module.exports:暴露模块方法和属性;
exports:exports其实是指向于module.exports的一个变量,相当于在模块的开始,定义了 var exports = module.exports; 故使用exports时需要注意不要给该变量重新赋值;
以遵循该规范的nodeJs模块化为例:
require('module'); // 通过模块名加载,需要注意的使,在配置文件中要配置该模块的具体路径; require('path'); // 通过路径加载
// 使用exports添加属性的方式暴露 exports.xxx = xxx; // 暴露xxx属性
// 或者使用给module.exports赋值的方法 module.exports = yyy; // require时得到的就是运行后yyy值的拷贝
exports和module.exports 不管谁被重新赋值,他们的关联关系都会断掉。模块最终return出去的是module.exports;
exports.a = 2; module.exports.b = 3; console.log(module.exports); //{ a: 2, b: 3 } console.log(exports); // { a: 2, b: 3 } console.log(exports === module.exports); // true
给module.exports重新赋值,exports中新添加的a属性不会返回
exports.a = 2;
module.exports = {"b": 3}; // moudle.exports 被重新指向新的内存,它与exports的关联关系断开
console.log(module.exports); // { b: 3 } console.log(exports); // { a: 2 } console.log(exports === module.exports); // false
给exports重新赋值,exports与module.exports断开关联
exports = 2; // exports重新赋值,切断了与module.exports的联系 module.exports.b= 3; console.log(module.exports); // { b: 3 } console.log(exports); // 2 console.log(exports === module.exports); // fasle
- 关于AMD
Asynchronous Module Definition,异步模块加载;异步的加载方式,更适用于浏览器。请求发出后,继续执行其他脚本,依赖于请求结果的代码,放到一个回调函数里;AMD规范的代表则是require.js;
特点:依赖前置,所有依赖都在模块定义时声明。不管是在define中声明的依赖,还是在callback中通过require加载的依赖,都会先加载完再去执行callback;故它属于运行时加载。这个特性就导致了在模块定义时被声明的依赖,虽然没有被用到,也做了加载执行的操作;更多requireJs的实现细节,可以参考这篇文章;
以require.js为例:define方法用来定义模块,require方法用来加载模块,config方法用来做配置
config配置
require.config({ baseUrl:'js/', paths:{ // 需要通过模块名来加载的模块,都定义到这里,支持网络资源,本地资源路径在baseUrl下 'jquery':'http://xxx.xxxx.com/jquery.js', 'index':'index/index.js', }, shim:{ 'aaa':{ // 不符合标准的文件,可以在shim中来定义 deps:['./a','./b'], init:function(){ return { // 这里定义非标准文件的返回 } } } } });
模块定义:define([id,deps,] callback);
// define 的模块id是唯一的,为避免麻烦,一般可省略,require.js会自动生成一个唯一标识 define(['jquery','index','./utils'], function($,index,utils){ // dosth... return { // 这里定义模块向外暴露的方法和属性,没有可以省略; 'aaa': 1, 'bbb': 1 } });
define 也可以定义一组键值对并返回,这种用法常用于动态的config配置;
define({ 'aaa': 1, 'bbb': 2, 'ccc': 3 });
模块载入:require(deps[,callback]);
require(["moduleName","path","url"], function (module) { // dosth; });
如果要在define中使用require,那需要加入require的依赖,简写也可以省略
define(function(require, exports, module ) { // 这种定义模块的方式可以兼容commonJs规范,但实质上还是被转换为requireJs的规范来实现;
var a = require('a'), // 通过这种方式可以实现按需加载
b = require('b');
// 模块需要暴露的方法也可以通过 exports向外暴露
exports.eee = 123;
});
// 等价于:
define(['require'], function(require){
var a = require('a'),
b = require('b');
})
- 关于CMD
Common Module Definition 通用模块定义,CMD规范的概念是随着sea.js的推出产生的。同时,sea.js也借鉴了很多require.js的东西。它与 CommonJS 和 Node.js 的 Modules 规范保持了很大的兼容性。通过 CMD 规范书写的模块,可以很容易在 Node.js 中运行;
特点:同AMD一样,CMD也属于运行时加载。但是CMD规范中,依赖模块可以通过require.async在需要的地方引入并执行(懒加载)。这个特性使它可以实现按需加载。
以sea.js为例:通过define定义模块,通过require加载模块,通过exports或return向外提供API;
模块定义:define(id?, deps?, factory);factory可以是函数,也可以是对象或者字符串
// define 函数的标准使用方法。但是官方强烈推荐不传入 id 和 deps,模块加载器会自动获取这两个参数,id默认为模块所在文件的访问路径,
// deps数组模块加载器会从factory.toString()
中解析。同时,function的第一个参数,必须是require,这是seajs的使用规则;
define('hello', ['jquery'], function(require, exports, module) { //dosth...
// 向外暴露模块的API有三种方式: exports.aaa = sth; // seajs中exports也是module.exports的引用;
module.exports = {}
return {}
});
// factory 为对象,相当于定义一个json数据模块,加载该模块时得到的就是这组json数据;这个用法跟require是一致的 define({ "foo": "bar" });
// factory为字符串时,相当于定义一个字符串模板 define('I am a template. My name is {{name}}.');
模块引用:require(id); require 在seajs中可以看作是语法关键字,不可重新赋值,不可引用;id为要引用模块的唯一标识,且必须是字符串直接量;
define(function(require, exports, module) { // 同步加载模块,通过这种方式加载的文件,会在静态分析阶段就被下载好; var a = require('./a'); a.doSomething(); // 异步加载一个模块,通过这种方式加载的文件,在用的时候才会下载; require.async('./b', function(b) { b.doSomething(); }); // 异步加载多个模块 require.async(['./c', './d'], function(c, d) { // do something }); // 条件加载模块;PS:如果这里依然使用require来加载模块,那模块加载器会把两个模块都下载下来 if (todayIsWeekend){ require.async("play"); }else{ require.async("work"); } });
无论是AMD还是CMD,都是module2.0 的一个分支,正所谓条条大路通罗马,也没有哪个解决方案就明显优于哪个。作为一个前端开发,内心永远向往大一统。ES6的模块化,在目前看来,已经算是前端模块化的大一统了。
- 关于ES6 Module
就目前考虑浏览器的兼容性来说,ES6的模块化,还是需要进行兼容性转化的,当然,这个在前端自动化构建的大潮中已经不需要再被提起了。
特点:与AMD和CMD不同的是,它的设计理念是尽量的静态化,在编译阶段就能确定模块的依赖关系,输入输出等,即编译时加载(静态加载);与commonJs提供的是值的拷贝不同得是,export向外提供的是一个只读的动态引用,故通过import加载的模块,是不会被缓存的。这一特点也说明ES6是支持动态更新的。
关键字:通过import引用其他模块,通过export对外提供接口。
export default anything; // 模块的默认输出,一个模块只能有一个默认输出; // 本质上,export default就是输出一个叫做default的变量或方法,然后系统允许你为它取任意名字。所以default后面不能跟变量声明语句。 import anyName from 'path'; export var a = 1; //这里要说明一点,export是对外的接口,所以它必须与模块内部的变量建立一一对应关系; export function foo(){}; // 也可以写成下面的形式: var a = 1; function foo(){}; export {a, foo} // 然而下面的这两种写法都是错误的,因为它没有提供对外的接口 export 1; var a = 1; export a; // 在import中,就要指出要加载的方法和属性的具体名字 import {a, foo} from 'path'; // 通过 as关键字,可以给对外输出的方法重新命名。 function v1() { ... } function v2() { ... } export { v1 as streamV1, v2 as streamV2, v2 as streamLatestVersion // 同一个方法可以输出多次 }; // import 中也支持as关键字 import { streamV1 as newName } from 'path'; // import也可以整体加载 import * as newName from 'path'; // import也可以用来加载并执行一个js文件,并且没有任何输入。 import 'path'; import 'path'; // 即使多次加载,该文件也只执行一次, // 对于同一个模块中的多次加载,import也只执行一次,因为import语句是单例的(singleton) import {foo} from 'path'; import {bar} from 'path'; //相当于 import {foo, bar} from 'path';
commonJs规范和es6模块化规范对循环加载的处理
本来想再总结下这个知识点,但是看了大神阮一峰的总结文章,感觉已经很清晰明了了,这里贴出链接:http://www.ruanyifeng.com/blog/2015/11/circular-dependency.html;
总之一句话,commonJs中遇到循环引用,是执行了多少返回多少,因为它是值的拷贝。ES6里是值的引用,方法或者属性真正被使用的时候才去取。所以如果你的打包构建是基于webpack,那要尽量避免循环引用,或者保证循环引用的变量和方法是已经运行过的。