前言
初期的web端交互还是很简单,不需要太多的js就能实现。随着时代的的发展,用户对Web浏览器的性能也提出了越来越高的要求,浏览器也越来越多的承担了更多的交互,不再是寥寥数语的js就能解决的,那么就造成了前端代码的日益膨胀,js之间的相互依赖也会越来越多,此时就需要使用一定的规范来管理js之间的依赖。
本文主要是什么是模块化,为什么需要模块化以及现下流行的模块化规范:AMD,CMD,CommonJs,ES6。
什么是模块化
要想理解模块化是什么可以先理解模块是什么?
模块:能够独立命名并且能够独立完成一定功能的集合。
因此在js中就可以理解为模块就是能够实现特定功能独立的一个个js文件。
模块化:就可以简单的理解为将原来繁重复杂的整个js文件按功能或者按模块拆成一个个单独的js文件,然后将每一个js文件中的某些方法抛出去,给别的js文件去引用和依赖。
为什么需要模块化
一、模块化的进程
1、全局function模式:将不同的功能封装为不同的函数
缺点:污染全局命名空间,容易引起命名冲突,看不出模块间的依赖
2、namespace模式:封装为对象模式
作用:减少全局变量,解决命名冲突
缺点:数据不安全(外部函数可以修改模块内的数据),看不出模块之间的依赖
const module = {
data:1,
getData(){console.log(this.data)}
}
module.data = 2; //这样会直接修改模块内部的数据
3、IIFE模式:匿名函数自调用(闭包)
作用:解决了数据安全,数据是私有的,外部只能调用暴露的信息
缺点:需要绑定到一个全局变量上例如window向外暴露,这样也会有命名冲突的问题
4、IIFE增强模式:引入依赖
作用:解决了模块直接的依赖问题
缺点:引入js的时候需要注意引入的顺序,并且当依赖很多的时候也会有弊端
// IIFE模式:匿名函数自调用(闭包)
(function(window){
let data = '这是IIFE模式';
getData(){
console.log(data);
}
window.module = { getData }
})(window)
// IIFE增强模式
(function(window,$){
let data = '这是IIFE模式';
getData(){
console.log(data);
$('body').css('background', 'red')
}
window.module = { getData }
})(window,jQuery);
//index.html
//需要注意引入的顺序
<script type="text/javascript" src="jquery-1.10.1.js"></script>
<script type="text/javascript" src="module.js"></script>
<script type="text/javascript">
myModule.foo()
</script>
//当<script>过多的时候的缺点:
// 1. 请求过多
// 2. 依赖模糊:不清楚依赖直接是什么关系,很容易因为引入的顺序导致出错
// 3. 难以维护
从以上的发展历程来看虽然模块化还不是那么的完善,但是也不难能发现模块化的优点:
二、模块化的优点:
- 避免命名冲突
- 更好的分离模块,按需加载
- 高复用性
- 高维护性
模块的规范
AMD
AMD(Asynchronous Module Definition):异步模块定义。采用异步方式加载模块,模块的加载不影响后续语句的执行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。浏览器环境要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用AMD规范。
RequireJs是AMD规范的最佳实践,所以我们用AMD规范的时候要引入requirejs。
AMD的语法:
define用来定义模块;
require用来加载模块,通常AMD框架会以require方法作为入口,进行依赖关系分析并依次有序地进行加载。
AMD是依赖前置,就是说,在define或require中方法里传入的模块数组会在一开始就下载并执行
//define定义模块
//第一个参数:依赖的模块,没有可以不写
define(['module1','module2'],function(m1,m2){
//如果引入了module,但是没有使用,模块的内容也会执行
return 模块;
})
//引入模块,相当于主函数的引用
require(['module1','module2'],function(m1,m2){
//使用m1,m2
//如果引入了module,但是没有使用,模块的内容也会执行
})
AMD具体使用流程
- 引入requirejs
a. requirejs官网下载:requirejs
b. github:requirejs
c. 或者直接用require的js链接放在index.html中引用(只建议在学习时候用): https://requirejs.org/docs/release/2.3.6/minified/require.js - 定义模块
//module1.js
define(function(){
const msg='module1';
return {
msg
}
})
//module2.js
define(function(){
const msg="module2";
return {
msg
}
})
//module3.js
define(['module2'],function(m2) {
const msg="module3";
const msg2 = m2.msg
return {
msg,
msg2
};
});
//main.js,入口文件不需要module2,因此就不需要引入
require.config({
paths:{
jquery:'jquery' //用于引入第三方库,此处填写的是路径,文件的路径;当然也可以在index.html用script标签引入
}
})
require(['module1','module3','jquery'],function(m1,m3,$){
console.log(m1,'module1');
console.log(m3,'module3');
console.log($,'第三方库');
});
console.log('模块加载'); //会优先打印
- index.html的引入
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!--data-main:文件入口-->
<script data-main="main" src="https://requirejs.org/docs/release/2.3.6/minified/require.js"></script>
</body>
</html>
-
输出结果
CMD
CMD(Common Module Definition):通用模块定义。用于浏览器端,是除AMD以外的另一种模块组织规范。结合了AMD与CommonJs(后面会讲到)的特点。也是异步加载模块。
与AMD不同的是:AMD推崇的是依赖前置,而CMD是依赖就近,延迟执行。
依赖前置&&依赖就近,延迟执行
//依赖前置:AMD
require(['module1','module2'],function(m1,m2){
//依赖的模块首先加载,无论后续是否会用到
})
//依赖就近,延迟执行
define(funciton(require){
const module1 = require('./module1'); //用到的时候再申明,不需要就不用申明,也就不会加载进来
})
CMD具体使用步骤
- 引入sea.js
a. 官网:https://seajs.github.io/seajs/docs/#downloads
b. github:https://github.com/seajs/seajs - 模块定义
//module1.js
define(function(require,exports) {
const msg='这是模块1';
// const module2 = require('./module2');
exports.msg = msg; // 注意这里是用的exports
// exports.module2 = module2; // 如果是多个就得这么写,所以如果暴露多个接口不建议用exports
});
//module2.js
define(function(require,exports,module) {
const msg="这是模块2";
module.exports = { //这里用的是module.exports,跟exports作用一样
msg
}
});
//module3.js
define(function(require,exports,module) {
const msg="这是模块3";
const module2 = require('./module2');
module.exports = { //这里用的是module.exports,跟exports作用一样
msg,
module2
}
});
//main.js
define(function(require, exports,module) {
const module1 = require('./module1');
const module3 = require('./module3');
console.log(module1);
console.log(module3);
});
console.log('模块加载');
- index.html引入
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="sea.js"></script>
<script>
seajs.use('main');
</script>
</body>
</html>
- 输出结果
CommonJS
Node 应用由模块组成,采用 CommonJS 模块规范。每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。在服务器端,模块的加载是运行时同步加载的;在浏览器端,模块需要提前编译打包处理。
CommonJs有4个毕竟重要的变量:module
、require
、exports
、global
CommonJs的特点:
- 所有代码都运行在模块作用域,不会污染全局作用域;如果想要多个模块共享一个变量,需要给global添加属性(不建议这么用)
//module.js
global.data = '共享变量'; //添加到global的属性是多个文件可以共享的
let x = {
a:5
}
let b=0;
const add = function(val){
x.a+=x.a;
b=++b;
}
module.exports.x=x; //只有对外暴露了变量,在外部引用的时候才能获取到,否则x,b就是模块内部的私有变量
module.exports.add = add;
- 模块可以多次加载,但是只有再第一次加载的时候才会执行,后续的加载都是使用的缓存结果;如果想要再次加载需要清除缓存,或者对外暴露一个函数,加载之后执行暴露的函数
// test1.js
let msg="测试缓存";
console.log(msg);
exports.msg=msg;
// test2.js
const msg = require('./test1');
const msg2 = require('./test1');
// 上面的输出结果是:测试缓存
// 会发现只打印了一次结果,以此可以证明只有第一次加载的时候才会执行,第二次加载的时候使用的是缓存的结果
// 删除指定模块的缓存
delete require.cache[moduleName] // moduleName必须是绝对路径
// 删除所有模块缓存
Object.keys(require.cache).forEach(function(key){
delete require.cache[key]
})
- 模块加载的顺序是按照再代码中书写的顺序加载的,同步加载模块。
- 引入的值其实是输出值的拷贝(浅拷贝)。也就是说一旦值输出之后,模块内的改变就不会影响这个值的改变(引用类型除外)
// example.js
let x= {
a:5
};
let b=0;
const add = function(val){
x.a+=x.a;
b=++b;
// return x.a++;
}
module.exports.x = x;
module.exports.b=b;
module.exports.add = add;
// main.js
const example = require('./example');
const add = require('./example').add;
example.add();
console.log(example.x) // {a:10}
console.log(example.b) // b:0
CommonJs的语法
//对外暴露接口
module.exports
exports
//引入模块
require(模块的路径) //模块的路径有多种写法,后续补充
module.exports&&exports
module.exports: 每个模块内部,module
代表了当前这个模块,module
是一个对象,对外暴露的就是exports
这个属性,加载某个模块其实就相当于加载的module.exports
这个属性
exports: 其实就是module.exports
的引用。为了使用方便,node为每个模块创建了一个export
s变量,这个变量就指向了module.exports
。因此以下两种做法是错误的。
//错误一
exports='msg'; // 此时相当于改变了exports的指向,失去了与module.exports的联系,也就失去了对外暴露接口的能力
//错误二
exports.msg='msg';
module.exports = 'Hello world'; // 上面的msg是无法对外暴露的,因为module.exports被重新赋值了;此时对外暴露的就是『Hello world』
ES6
ES modules(ESM)是 JavaScript 官方的标准化模块系统。ES6模块设计的思想是尽量的静态化,使得编译时就能知道模块的依赖关系,以及输入和输出的变量。有两个主要的命令:export和import。export用于对外暴露接口,import用于引入其他模块。
ES6模块的特点:
- 严格模式:ES6 的模块自动采用严格模式
- import read-only特性: import的属性是只读的,不能赋值,类似于const的特性
- export/import提升: import/export必须位于模块顶级,不能位于作用域内;其次对于模块内的import/export会提升到模块顶部,这是在编译阶段完成的
- 兼容在node环境下运行
- ES modules 输出的是值的引用,输出接口动态绑定,而 CommonJS 输出的是值的拷贝
//ES6模块值的引用 .mjs 主要是为了能用node环境运行:node --experimental-modules
// example.mjs
let x= {
a:5
};
let b=0;
const add = function(val){
x.a+=x.a;
b=++b;
}
export {x,b,add}
// main.mjs
import { x,b, add} from './example.mjs';
add();
console.log(x,b); // {a:10} 1
export&&import用法
export:用于向外暴露接口
import:用于引入外部接口
// 方法一:
//export单个向外暴露接口
export const x = 1;
export const y = {a:1}
export const add = function(){console.log(123)}
//export一起向外暴露接口
const x=1;
const y={a:1};
const add = function(){console.log(123)};
export {x,y,add}
//import引入外部接口
//针对以上两种方式import可以写成如下两种情况
import {x,y,add} from './exmaple'; //使用哪个就引入哪个
import * as moduleName from './exmaple'; // 全部引入,使用的时候使用moduleName.x
// 方法二:
//除了使用export 向外暴露接口外还可以使用export default向外暴露接口:同一个模块中export可以有多个,但是export default只能有一个
const x=1;
const y={a:1};
export default { x,y}
// 对应的import
import moduleName from './exmaple'; //全部引入,使用方式moduleName.x
总结
- AMD:异步加载模块,允许指定回调函数。AMD规范是依赖前置的。一般浏览器端会采用AMD规范。但是开发成本高,代码阅读和书写比较困难。
- CMD:异步加载模块。CMD规范是依赖就近,延迟加载。一般也是用于浏览器端。
- CommonJs:同步加载模块,一般用于服务器端。对外暴露的接口是值的拷贝
- ES6:实现简单。对外暴露的接口是值的引用。可以用于浏览器端和服务端。
后记
模块化的一次有一次的变更,让系统模块化变得也越来越好,但是响应的也引起了一些问题。例如使用模块的时候,发现有些模块引入了但是并没有真正的使用到,这样就造成了代码的冗余,多了一些不必要的代码。这些模块有时通过检查是很难发现的,因此就要想如何能够快速的去除这些无用的模块呢,此时Tree Shaking就出现了;又比如如何能够在开发代码的时候比较便捷,然后在生产中又有高强度的兼容性呢,此时就出现了babel;又比如如何预处理模块,此时出现了webpack……
出现了解决问题的办法,就得继续学习啦……
参考文章
前端模块化的十年征程
彻底理清 AMD,CommonJS,CMD,UMD,ES6 modules
前端模块化—CommonJS、CMD、AMD、UMD和ESM
前端模块化详解(完整版)