什么是模块化?
将一个复杂的程序依据功能拆分成几个独立的块(文件),每个块都包含执行预期功能一方面的内容,其内部的数据或方法都是私有的。它们之间可以通过一些规范向外暴露一些接口(方法)与其他文件进行通讯或组合。
为什么前端需要模块化?
随着js能做的事越来越多,前端项目代码日益膨胀,主要体现在复杂的依赖关系、难以维护、代码不可复用和全局污染的问题。这些问题需要花费大量时间去管理, 而前端模块化能很好地解决这些问题。
发展演变
无模块化
就是把各种js文件一股脑放在script标签中。
<body>
<script src="module1.js"></script>
<script src="module2.js"></script>
<script src="module3.js"></script>
</body>
问题:
– 引入的文件都在全局作用域中,污染全局环境,可能导致命名冲突
– 引入的顺序很有讲究,先后顺序错了,会导致报错,依赖关系不明确。
命名空间
每个模块只暴露一个全局对象,我们所有的模块成员(数据/方法)都挂载在这个对象下面
// module.js
const module = {
name: 'jennifer',
age: 18,
};
<script src="module.js"></script>
<script>
console.log(module.name); // 'jennifer'
module.name = 'jie'; // module.name --> jie
</script>
解决:减少全局变量,解决命名冲突
问题:
– 模块间的依赖关系还是没解决
– 数据不安全,外部可以随意修改内部变量
IIFE(立即执行函数)
// module.js
(function(window) {
const name = 'jennifer';
function showName() {
console.log(`name: ${name}`);
}
window.module1 = { showName } ;
})(window);
<script src="module.js"></script>
<script>
module1.showName(); // 'jennifer'
console.log(module1.name); // 'undefined' 模块未导出该变量,无法访问,也无法修改
</script>
解决:数据是私有的, 外部只能通过暴露的方法进行操作,安全
问题:如果需要依赖另一个模块怎么处理?? 通过传参,把另一个模块传入进去~
// module.js
(function(window, ) {
const name = 'jennifer';
function showName() {
console.log(`name:{name}`);
$('body').css('background', 'cyan');
}
window.module1 = { showName } ;
})(window, jQuery);
html引入的js顺序要注意!jQuery要在module.js前
模块化规范
CommonJs
该规范最初是用在服务器端的node的。node应用由模块组成,每个文件都是一个模块,有自己的作用域。它有四个重要的环境变量为模块化的实现提供支持:
module、exports、require、global。实际使用时,用module.exports定义当前模块对外输出的接口(不推荐直接用exports,exports是对module.exports的引用,不能直接去赋值),用require加载模块。
// module.js
const x = 5;
const add = function (value) {
return value + x;
};
module.exports.x = x;
module.exports.add = add;
const module = require('./module.js'); // 自定义模块require的为路径,第三方模块为包名
console.log(module.x); // 5
console.log(module.add(1)); // 6
解决:全局污染,依赖关系不明确
问题:在浏览器端使用CommonJS规范会导致效率低下,在服务器端,模块的加载是运行时同步加载的;在浏览器端,模块需要提前编译打包处理。所以每次页面加载都会导致大量同步请求出现,造成页面阻塞,用户体验很不好。之后CommonJS中逐渐产生了一些分支,也就是AMD、CMD等。
AMD
James Burke提出了 AMD 规范,RequireJS 也是他的代表作。AMD规范是非同步加载模块,允许指定回调函数。
主要的三个API:
1. require([module], callback)
2. define(id, [depends], callback)
3. require.config()
通过define来定义一个模块,然后使用require来加载一个模块, 使用require.config()指定引用路径。
<!DOCTYPE html>
<html>
<head>
<title>Modular Demo</title>
</head>
<body>
<!-- 引入require.js, data-main不能省略,是用来指定js主文件的入口 -->
<script data-main="js/main" src="./require.js"></script>
<script>
require(['main'], function(main) {
console.log(main.getName); // jennifer
})
</script>
</body>
</html>
// main.js
define(function() {
let name = 'jennifer';
function getName() {
return name.toUpperCase()
};
return { getName }; // 暴露模块
})
如果有其他依赖,可以传一个数组作为define的第一个参数
在使用require.js的时候,我们必须要提前加载所有的依赖,然后才可以使用,而不是需要使用时再加载。
解决:(CommonJS的问题):同步导致的阻塞
问题:require.js2.0以前依赖不能延迟执行,在加载模块完成后就会执行改模块。
CMD(Sea.js的推出)
CMD规范整合了CommonJS和AMD规范的特点。CMD规范专门用于浏览器端,模块的加载是异步的,模块使用时才会加载执行。
// AMD,require.js 2.0以前
define(['a', 'b'], function(a, b) {
a.doSomething();
if (false) {
// 即使没有用到模块b,他还是被提前加载了
b.doSomething();
}
})
// AMD,require.js 2.0之后可以按需加载
define(['require', 'a'], function(require, a) {
a.doSomething();
if (false) {
const b = require('./b');
b.doSomething();
}
})
// CMD
define(function(require, exports, module) {
const a = require('./a'); // 在需要是加载模块
a.doSomething();
if (false) {
const b = require('./b');
b.doSomething();
}
})
AMD和CMD总结
AMD和CMD对依赖模块的执行时机处理不同
– AMD推崇依赖前置,在定义模块的时候就需声明其依赖的模块,并且加载完直接执行(requireJs 2.0后可以实现延迟执行)
– CMD推崇就近依赖,只有在用到某个模块的时候再去require,是延迟执行
ESM
ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。统一了浏览器和服务端。但是由于ES6目前无法在浏览器中执行,所以只能通过babel将不被支持的import编译为当前受到广泛支持的 require。
// math.js
const num = 0;
const add = function(a, b) {
return a + b;
};
export { num, add }; // 或者默认导出:export default { num, add }
import { num, add } from './math'; // 默认导入 import math from './math'
// 默认导入引用 math.add(99, math.num);
console.log(add(99, num)); // 99
总结CJS和ESM
- CJS 模块输出的是一个值的拷贝,ESM模块输出的是值的引用
- CJS 模块是运行时加载(加载的是一个对象,只会在脚本运行完才生成),ESM 模块是编译时输出接口(一种静态定义,代码解析阶段就会生成)
- CJS 加载模块是同步的,主要用于服务端编程,而ESM 有异步的特性,能够tree shaking,成为了浏览器和服务端通用的模块解决方案