# 31、模块化历程
模块化的引入主要是用于解决命名冲突、代码复用、代码可读性、依赖管理等,可以提高项目的维护性和开发效率
# AMD
异步模块定义
AMD
AMD
全称 Asynchronous Module Definition
异步模块定义,是为浏览器环境设计的,因为 CommonJS
模块系统是同步加载的,当前浏览器环境还没有准备好同步加载模块的条件。
AMD
定义了一套 JavaScript
模块依赖异步加载标准,来解决同步加载的问题。
AMD
并非原生js
支持,是RequireJs
模块化开发当中的产物,AMD
依赖于RequireJs
函数库,打包生成对应效果的js
代码
主要由require.config()
、define()
、require
三个函数实现。
require.config()
:用于声明基本路径和模块名称;define(id?,dependencies?,factory)
:用于定义模块对象,参数如下id
:可选,为定义模块的标识,默认为模块文件名不包括后缀dependencies
:可选,是 当前模块依赖的模块路径数组factory
:为工厂方法,初始化模块的函数或者对象,如果为函数将会只执行一次,如果是对象将作为模块的输出
// 定义math.js 模块
define(function(){
var basicNum = 0;
var add = function(x,y){
return x + y
}
return {
add : add,
basicNum : basicNum
}
})
require()
:则用于加载模块并使用。
AMD
规范处于浏览器环境中,是一种异步模块加载规范,在使用时,首先要加载模块化规范实现文件require.js
及JS
主文件,示例如下:
<!-- js/require.js 是实现AMD规范的类库文件,是任何使用AMD规范的网页都需要加载的; -->
<!-- js/main 是开发者的代码主文件,在这个文件中加载并使用自定义模块 -->
<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','math'],function($,_,math){
// $代表jquery _代表underscore
var sum = math.add(10,20);
$('#sum').html(sum);
})
优点:
- 解决了模块同步加载的问题
- 可以并行加载多个模块
缺点:
- 对于依赖的模块无论实际需要与否,都会先加载并执行
- 提高了开发成本,代码的阅读和书写比较困难,模块定义方式的语义不顺畅
为了解决AMD
规范强制 前置加载并执行的问题,CMD
规范应运而生
# CMD
通用模块定义
CMD
CMD
全称Common Module Definition
通用模块定义,CMD
与 AMD
规范一样,同样是一种 JS
语言自实现的模块化方案。
同AMD
,CMD
也有一个函数库SeaJS
与RequireJS
类似的功能
与AMD
的不同:
AMD
规范是依赖前置、模块提前加载并执行;CMD
规范是依赖后置、模块懒加载再执行;
CMD
推崇一个文件一个模块,使用定义模块define(factory)
,
factory
可以是函数,可以是其他有效的值,如果factory
是函数,则该函数的参数依次为require,exports,module
require
:内部引入模块的时候调用exports
:用来向外部导出本模块的API
module
:一个对象,上面存储了当前模块相关的属性和方法
// CMD写法
define(function(require,exports,module){
// 在需要的时候声明、加载
var a = require('./a'),
c = require('./c');
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 sun = math.add(1,2)
})
# CommonJS
CommonJS
模块规范(同步),通常用于Nodejs
中的模块化, 服务器端的Node.js
遵循CommonJS
规范,该规范的核心思想是允许模块通过require
方法来同步加载所要依赖的其他模块,然后通过exports
或module.exports
来导出需要暴露的接口。
# CommonJS
模块的加载原理
原理
CommonJS
模块都是一个脚本文件,require
命令第一次加载该模块时就会执行整个脚本,然后在内存中生成该模块的一个说明对象。
{ id: "", //模块名,唯一 exports: { //模块输出的各个接口 ... }, loaded: true, //模块的脚本是否执行完毕 ...}
以后用到这个模块时,就会到对象的exports
属性中取值。即使再次执行require
命令,也不会再次执行该模块,而是到缓存中取值。
CommonJS
模块是加载时执行,即脚本代码在require
时就全部执行了。一旦出现某个模块被‘循环加载’,就只输出已经执行的部分,没有执行的部分不会输出
CommonJS
的模块是动态加载的。如果你 require
了一个模块,那就相当于你执行了该文件的代码并最终获取到模块输出的 module.exports
对象的一份浅拷贝,并且重复引入的模块并不会重复加载,再次获取模块只会获取之前加载的模块的拷贝。
拥有4个环境变量modul、exports、require、global
// 模块定义: lib.js
var num = 3
function incNum(){
num++
}
module.exports = {
num:num,
incNum:incNum
}
// lib.js的 exports对象 在加载后已经成了
module.exports = {
num:3,
incNum: incNum // 这里是incNum函数的引用
}
// 在main.js中 使用lib模块
var mod = require('./lib')
// 而 mod 从 module.exports对象浅拷贝而来,调用 mod.incNum() 相当与调用了lib.js模块中的 incNum函数,更改的是lib.js中 num 的值,与mod对象无关
console.log(mod.num) // 3
mod.incNum() // 这将改变模块lib.js中的count值;
console.log(mod.num) // 3
var mod2 = require('./lib)
console.log(mod2.num) // 3
// 为什么输出都是 3
缺陷:
CommonJS
是一种动态加载、拷贝值对象执行的模块规范。每个模块在被使用时,都是在运行时被动态拉取并拷贝使用,模块定义是唯一的,但是输出的都是拷贝对象。
# ES6
模块化
js模块化
ES6
中提供了js
的模块化规范:esmodule
。
ES6 esmodule
规范是使用import
语句导入模块,export
语句导出模块,输出的是对值的引用
客户端可以基于ES6
来使用一些模块(要使用谷歌60+版本以上的浏览器)
esmodule
是一个规范:规定了如下规范
- 如何定义模块( 一个
JS
就是一个模块) - 如何导出模块(使用
export
) - 如何导入模块(
import
)
# ES6 module
加载原理
原理
ES6 module
与CommonJS
有本质上的区别。ES6 module
是静态编译(在编译时就能够确定模块的依赖关系,以及输入和输出的变量,完成模块的加载),动态引用(导出和导入的都是对值的引用,模块中的值变了之后,引用到的值的地方也会改变),遇到模块加载命令import
时不会立即执行模块,只是生成一个指向被加载模块的引用。
//odd.js
import {even} from "./even.js";
export function odd(n){
return n != 0 && even(n-1);
}
//even.js
import {odd} from "./odd";
var counter = 0;
export function even(n){
counter ++;
console.log(counter);
return n == 0 || odd(n-1);
}
//index.js
import * as m from "./even.js";
var x = m.even(5);
console.log(x);
var y = m.even(4);
console.log(y);
// 运行index.js 结果
1
2
3
false
4
5
6
true
# 如何在HTML中引入一个模块
依然用
script
标签导入,不过需要规定type
类型
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<!--需要type为模块类型-->
<script src="main.js" type="module"></script>
</body>
</html>
# 如何导出一个模块
导出
一个JS
就相当于一个模块,把需要用的导出用export
导出也有两种方式:
- 1、以默认的形式导出(必须加
default
)
let str='我很帅';
let str1='我很英俊';
export default {
str,str1
}
- 2、分别导出
//此模块为a.js(下面导入时会用到)
//会将str和str1放到一个对象内导出{str:'我很帅',str1:'我很英俊'}
export let str='我很帅';
export let str1='我很英俊';
# 如何导入一个模块
导入
使用import
来导入一个模块,最好在需要模块的顶部导入
import
具有声明作用(之后再重复声明会报错),而且声明会进行变量提升。
两种写法:
- 1、用
ES6
的解构赋值:把需要的解构出来
console.log(str);//我很帅(最好不要这样用)
//导入时可用ES6中解构赋值的形式从另一个模块中解构出需要用的内容(一个个的导入)
import {str,str1} from './a.js';//自定义模块路径前必须要加 ./ ,后缀必须要加.js,不然会报错
console.log(str,str1);//我很帅 我很英俊
- 2、用
* as obj form xxx
把另一个模块中的东西批量导入到我需要的文件中
//批量导入
import * as obj from './a.js';
console.log(obj.str, obj.str1);//我很帅 我很英俊
- 3、把默认的导入(只有以默认的形式导出才能这样导入)
import val from './a.js';
console.log(val);//{str: "我很帅", str1: "我很英俊"}
# ES6:export default 和 export 区别
区别
- 1.
export
与export default
均可用于导出常量、函数、文件、模块等 - 2.你可以在其它文件或模块中通过
import
+(常量 | 函数 | 文件 | 模块)名的方式,将其导入,以便能够对其进行使用 - 3.在一个文件或模块中,
export、import
可以有多个,export default
仅有一个 - 4.通过
export
方式导出,在导入时要加{ }
,export default
则不需要 - 5、当使用
export
导出多个的时候,导入时可以使用import * as xxx form './aaa'
;
// a.js 模块
//存放action的类型
export const ADD_TODO='ADD_TODO';
export const CHANGE_SELECTED='CHANGE_SELECTED';
export const DELETE='DELETE';
export const CHANEG_TYPE='CHANEG_TYPE';
// b.js 模块要引入a模块导出的内容
//把从a模块中导出的所有内容作为一个对象赋值给obj
import * as obj form './a'
//用的时候可以用obj.ADD_TODO来调用
obj.ADD_TODO
# ES6
模块规范 和 CommonJS
规范的区别
区别
ES6 module
是解析(是解析 不是编译)时静态加载、运行时动态引用,所有引用出去的模块均指向同一个模块对象。CommonJS
规范是运行时动态加载、拷贝值对象使用,每一个引用出去的模块都是一个独立的对象。
ES6 module
的运行机制和CommonJS
不一样,遇到模块加载命令import
时不去执行这个模块,只会生成一个动态的只读引用,等到需要用到这个值时,再到模块中取值,也就是说模块中的值变了,那输入的值也会发生变化