JavaScript的模块化

175 阅读4分钟

模块化解决了什么问题

想要知道模块化解决了什么问题,需要了解在没有模块化之前,前端工程师在引入 JavaScript 代码采用什么方式,如下

<script scr="./js1.js"></script>
<script scr="./js2.js"></script>
<script scr="./js3.js"></script>

由于 JavaScript 语言自有的特性,这样引入JavaScript 文件会存在以下几个问题。

  • 污染全局变量
  • 依赖关系不明显
  • 文件引入顺序会有限制

至于为什么会存在这几个问题,这里就不多说了,常识问题。

由于出现的这几个问题,我们就需要把 JavaScript 进行模块化,让每一个 JavaScript 文件有单独的作用域,不会让多个文件之间变量产生冲突,模块之间还要保留相互的导入导出,保证模块之间能够相互通讯,保证彼此之间的依赖关系。

在 JavaScript 语言中,ES6 规范之前没有在语言层面定义模块化的导入导出,只会在不同的 JS 运行环境中存在着自己的解决方案。有着以下几个常用的方案 **

  1. 在服务端运行的 CommonJS
  2. 适合 WEB 开发的 AMD
  3. ESModule 规范

commonJS

我们在上面已经说过了,ES6 规范之前,想要对JavaScript代码进行模块化,是存在不同的 JS 环境中的,CommonJS 就是node.js 环境定义的一套模块化规范,基本语法如下

const module1 = require('./module1') // 引入 module1 文件
const name = "Lius"
module.exports = name // 把当前文件导出

再来看一段代码

// index.js
require("./moduleA");
var m = require("./moduleB");
console.log(m);

// moduleA.js
var m = require("./moduleB");
setTimeout(() => console.log(m), 1000);

// moduleB.js
var m = new Date().getTime();
module.exports = m

这段代码在 moduleA.js 和 index.js 文件里面同时打印 moduleB.js 文件导出的 m 变量的值,在moduleA.js 文件中延迟打印,执行 node index.js 可以看出不管在 index.js 还是 moduleA.js 文件其结果都是一样的。由此可见

  1. 模块之间及时拥有相同的变量名,但是并不会具有冲突
  2. 通过 **require **和 **module.exports **处理模块之间的导入导出
  3. 在不同模块加载统一模块 moduleB,其结果是相同的,保证了模块单例

根据以上三点,我们可以得出一个结论,如果能够满足以上三点,就说明他是一个模块化规范。

需要注意的是CommonJS 只能够在 node.js 环境中运行,因为只有在node环境中才能够解析出来 require 和 module.exports 语法,如果是其他环节就会报错。

AMD

浏览器也是一个JS的运行环境,所有的前端工程师都应该知道,浏览器的运行环境并没有提供像CommonJS 规范里面的 require 和 module.exports ,所以web端有着自己的一套规范 AMD ** AMD(Asynchronous module definition)意为**异步模块定义。**根据名称可以知道AMD加载所有的模块都是异步进行加载。修改一下上面的代码

// index.js
require(['moduleA', 'moduleB'], function(moduleA, moduleB) {
	console.log(moduleB);
});

// moduleA.js
define(function(require) {
	var m = require('moduleB');
	setTimeout(() => console.log(m), 1000);
});

// moduleB.js
define(function(require) {
	var m = new Date().getTime();
	return m;
});

// index.html
<html>
  <!-- 此处必须加载 require.js 之类的 AMD 模块化库之后才可以继续加载模块-->
  <script src="/require.js"></script>
  <!-- 只需要加载⼊⼝模块即可 -->
  <script src="/index.js"></script>
</html>

如果想要使⽤ AMD 规范,我们还需要添加⼀个符合 AMD 规范的加载器脚本在⻚⾯中,符合 AMD 规范实现的库很多,⽐较有名的就是 require.js。

define()定义模块,用require()加载模块,模块ID默认为文件名。通过chrome 打开index.html 在控制台我们可以看到输出的结果一样。在 Network 栏也能看到各个模块之间的加载时异步的。

esModules

以上两种规范有以下两个特点

  1. 都不是在语言层面实现模块化,都是有运行环境自己定义
  2. 只能在单独的环境中运行,不能够共用模块(不能再浏览器里面使用CommonJS定义的模块,不能在node环境使用AMD模块)

ES6 规范出来之后,在语言层面定义了模块化,不在受环境的限制。在ES6中,我们可以通过 import 和 export 两个关键字对模块进行导入和导出。ESModule 是有JS解释器实现,CommonJS 和 AMD是由运行环境实现。

继续修改以上代码

// index.js
import './moduleA';
import m from './moduleB';
console.log(m);

// moduleA.js
import m from './moduleB';
console.log(m)

// moduleB.js
const m = new Date().getTime();
export default m

盗⽤⼀张图来表示各种模块规范语法和它们所处环境之间的关系: image.png 每一个js的运行环境都会有一个js的解析器,他的作用就是使用 ECMAScript 的规范去解释 JS 语法;如上图所示,在不同的环境都会有不同的全局对象。这个和模块化很相似,在不同的JS运行环境有着不同的模块化规范,但是ESModule 是定义在 JS Core 层面的模块化规范,无论在什么运行环境都可以支持。

关于打包和编译的区别:

  • 打包⼯具主要处理的是 JS 不同版本间模块化的区别
  • 编译⼯具主要处理的是 JS 版本间语义的问题