前端模块化总结(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,那要尽量避免循环引用,或者保证循环引用的变量和方法是已经运行过的。

浙公网安备 33010602011771号