无模块时代主要运用的模块加载方式
文件划分
// module-a.js
let data = "data";
// module-b.js
function method() {
console.log("execute method");
}
// index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script src="./module-a.js"></script>
<script src="./module-b.js"></script>
<script>
console.log(data);
method();
</script>
</body>
</html>
缺点:
命名空间
命名空间是模块化的另一种实现手段,它可以解决上述文件划分方式中全局变量定义所带来的一系列问题。下面是一个简单的例子:
// module-a.js
window.moduleA = {
data: "moduleA",
method: function () {
console.log("execute A's method");
},
};
// module-b.js
window.moduleB = {
data: "moduleB",
method: function () {
console.log("execute B's method");
},
};
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script src="./module-a.js"></script>
<script src="./module-b.js"></script>
<script>
// 此时 window 上已经绑定了 moduleA 和 moduleB
console.log(moduleA.data);
moduleB.method();
</script>
</body>
</html>
IIFE(立即执行函数)
不过,相比于命名空间的模块化手段,IIFE实现的模块化安全性要更高,对于模块作用域的区分更加彻底。你可以参考如下IIFE 实现模块化的例子:
// module-a.js
(function () {
let data = "moduleA";
function method() {
console.log(data + "execute");
}
window.moduleA = {
method: method,
};
})();
// module-b.js
(function () {
let data = "moduleB";
function method() {
console.log(data + "execute");
}
window.moduleB = {
method: method,
};
})();
// index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script src="./module-a.js"></script>
<script src="./module-b.js"></script>
<script>
// 此时 window 上已经绑定了 moduleA 和 moduleB
console.log(moduleA.data);
moduleB.method();
</script>
</body>
</html>
优点:
总结:
无论是命名空间还是IIFE,都是为了解决全局变量所带来的命名冲突及作用域不明确的问题, 但是模块加载的问题没有解决, 于是后面就衍生出了 CommonJS、AMD 和 ES Module 三大模块化解决方案.
模块化发展的历程
CommonJs
采用 module.exports方式导出, 采用 require 方式导入
// module-a.js
var data = "hello world";
function getData() {
return data;
}
module.exports = {
getData,
};
// index.js
const { getData } = require("./module-a.js");
console.log(getData());
代码中使用 require 来导入一个模块,用module.exports来导出一个模块。实际上 Node.js 内部会有相应的 loader 转译模块代码,最后模块代码会被处理成下面这样:
(function (exports, require, module, __filename, __dirname) {
// 执行模块代码
// 返回 exports 对象
});
特性:
- 最早运用于node.js, 由于原生浏览器不支持,社区出了一些loader, 比如browserify ,从而顺利在浏览器中执行.
- 以同步的方式进行模块加载, 在浏览器端 **模块请求会造成浏览器 JS 解析过程的阻塞, **导致页面加载速度缓慢。
总结,CommonJS 是一个不太适合在浏览器中运行的模块规范。因此,业界也设计出了全新的规范来作为浏览器端的模块标准,最知名的要数 AMD 了。
AMD
AMD全称为Asynchronous Module Definition, ,即异步模块定义规范, 采用异步的方式来加载模块, 解决了CommonJs同步加载模块的的缺点.
使用方式:
// print.js
define(function () {
return {
print: function (msg) {
console.log("print " + msg);
},
};
});
// main.js
define(["./print"], function (printModule) {
printModule.print("main");
});
在AMD的规范中, 通过define去定义或者加载一个模块,模块导出成员需要使用return语句return方法或者变量出去, 这样在引用该模块的时候, 可以通过函数的第一个参数拿到模块对象, 从而调用该模块对象上面的方法或者变量.
同时, 根据AMD规范,调用模块对象上面的方法之前, 浏览器会先加载依赖模块.
当然, 你也可以通过require 关键字来引入模块, 如上述的 man.js改写
// main.js
reuqire(["./print"], function (printModule) {
printModule.print("main");
});
require 与 define 的区别在于前者只能加载模块,而不能定义一个模块。
同CommonJs规范一样, 没有获得浏览器的原生支持, 所以需要第三方loader来支持运行在浏览器端, 如 requireJs 库等.
当然同时期出现的还有CMD规范, 解决的问题和AMD一样, 比较有代表性的作品如 seaJS, 随着社区的发展, 目前SeaJS 已经被requireJS兼容了。
AMD具有的特性:
- 异步加载模块, 解决了CommonJs同步加载的问题.
- 规范使用起来稍显复杂,代码阅读和书写都比较困难.
后面社区其实还出现了一种UMD规范, UMD其实并不算是一种规范, 只是时代的一个产物, 希望提供一个前后端跨平台的解决方案(支持AMD与CommonJS模块方式), 可以同时支持运行在浏览器环境和Node.js环境.
ES6 Module
ES6 Module 也被称作 ES Module(或 ESM),是由 ECMAScript 官方提出的模块化规范,作为一个官方提出的规范,ES Module 已经得到了现代浏览器的内置支持。在现代浏览器中,如果在 HTML 中加入含有type="module"属性的 script 标签,那么浏览器会按照 ES Module 规范来进行依赖加载和模块解析.并且, 一直遵循CommonJS规范的Node.js也紧跟 ES Module 的发展步伐,从 12.20 版本开始正式支持原生 ES Module。也就是说,如今 ES Module 能够同时在浏览器与 Node.js 环境中执行,拥有天然的跨平台能力.
目前ESM的兼容性在 CanIUse 上的详情数据如下图所示:
主流的浏览器基本都支持这个特性了.
举个简单ESM的例子:
// main.js
import { methodA } from "./module-a.js";
methodA();
//module-a.js
const methodA = () => {
console.log("a");
};
export { methodA };
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="./main.js"></script>
</body>
</html>
在Node.js环境中, 需要配置package.json中type: module 如下:
// package.json
{
"type": "module"
}
然后 Node.js 便会默认以 ES Module 规范去解析模块:
node main.js
// 打印 a
总结 EMS 优势 :
总结
本文从头到尾梳理了模块化历程.从无模块化标准的时代开始谈起,跟你介绍了文件划分的模块化方案,并分析了这个方案潜在的几个问题。随后又介绍了命名空间和IIFE两种方案,但这两种方式并没有解决模块自动加载的问题。由此展开对前端模块化规范的介绍,我主要给你分析了三个主流的模块化标准: CommonJS、AMD 以及 ES Module,针对每个规范从模块化代码标准、模块自动加载方案这两个维度给你进行了详细的拆解,最后得出 ES Module 即将成为主流前端模块化方案的结论。
历史的年轮滚滚向前, 时代也一直进步, 各种技术的发展是每一个时代最好的产物,大浪淘沙, 我们应该保持本心, 去追逐,去探究技术的本质,紧跟技术的发展,不要被时代所抛弃.