前端模块化
为什么需要模块化
在有模块化以前
我们来看下面一段代码
1 |
|
在这段 html 中,有 a.js、b.js 和之后一段嵌入的 JavaScript 代码。硬要说其是“模块”也不是不可以,只不过它们在未经特殊处理的前提下,是会互相污染的。比如,在 a.js 中写 window.alert = function() {} 是会实实在在影响到后面的 b.js 和最后一段脚本中的 alert 的。
模块化的意义
JavaScript 模块化就是将代码分解为独立小块,每个块都有自己的接口、依赖和功能,每个模块都是一份密闭空间,同时为了使代码更加可维护、可重用和可读性更高。
在有 CommonJS、 ES6、AMD 等模块化之前如何解决模块化
全局函数
通过将不同的功能封装成不同的全局函数
1 | function m1() { |
缺点:污染全局命名空间且无法保证命名冲突 、如果使用变量名很长,导致调用不方便,模块之间看不出直接关系
iife(自执行函数)
1 | (function () { |
利用的是函数作用域特性。通过闭包来做到内部变量的隔离,然后通过立即执行该闭包来得到相应的结果。这样就可以很方便地通过执行一些复杂逻辑来得到一个所谓的“模块”,而把逻辑变成内部私有形式给隔离开来。
1 | // module.js文件 |
1 | // index.html文件 |
这样做除了保证模块的独立性,还使得模块之间的依赖关系变得明显
现代常用的模块化方式
在 iife 之后,主要出现了 4 大体系:即 CommonJS、AMD 、CMD 、UMD。AMD/CMD/UMD,只是首字母不一样,后两个字母是 Module Defintion 的缩写。CommonJS ,也叫 cjs,是一种同步加载模块。前面几种都是第三方规范和实现,与 JavaScript 无关。随着 ECMAScript 规范的不断规范和完善,出现了 ECMAScript modules,即 ES Modules (缩写 ESM)模块规范,因其在 ECMAScript 6 中提出,所以也叫 ES6 Modules 模块化。相较于之前的几种方式都是通过三方实现函数和对象来模拟模块,ESM 只要宿主支持,该语法就能直接使用。
Node.js 的 ESM 最开始是在 v8.5.0 中,只不过当时还是 Experimental 特性,如果要使用需要加上 --experimental-modules
参数才能正常启动程序;在 v12.17.0 版本后才移除了这个参数并使之成为了正常使用的方式。
AMD/CMD/UMD 简介
因为这些模块化历史久远,已不适用于近些年的开发习惯,但又是历史产物,可能在某些老项目或特定场景中还在使用,所以还是提一下其主要特征和基本概念。
- AMD
AMD(Asynchronous Module Definition)模块化规范。采用异步加载模块,适用于浏览器端开发。可以在不影响后面代码执行的情况下加载模块。AMD 最开始在 require.js 中被使用,其首个提交在 2009 年出现,AMD 推荐依赖前置,提前执行。
AMD 规范中,一个模块可以通过 define 函数来定义。define 函数的第一个参数是一个数组,用于声明该模块的依赖项。第二个参数是一个函数,用于定义模块的功能。模块的输出通过 return 语句来实现。
在 AMD 规范中,通过 require 函数来引入模块。require 函数的第一个参数是依赖项的数组,第二个参数是一个回调函数,用于获取模块的输出。
1 | // 定义模块 |
- CMD
CMD 是 Common Module Definition,即一般模块定义,产生于 Sea.js 中。虽然 Common 也含有通用的意思,与 AMD 不同的是, 模块的加载是异步的,在使用模块时才加载。CMD 推崇依赖就近、延迟执行,也是通过 define 定义模块,require 引入模块
1 | // 定义模块 |
- UMD
UMD 意为通用模块,内部会分析是 AMD 还是 CMD 还是 CommonJs 规范,都不是的话会把引入的模块内容挂载到全局上。从而使不同规范的代码都可以正常使用。主要是通过像 rollup 或 webpack 等打包工具配置输出 umd 的格式。最后打包结果中去做具体是运行环境支持哪种规范。
1 | // 定义模块 |
CommonJS 规范
简介
CommonJS 模块规范发布于 2009 年,由 Mozilla 工程师 Kevin Dangoor
起草,他于当年 1 月发表了一篇文章《What Server-side JavaScript Needs》。最初的主要目的是为在浏览器环境之外的 JavaScript 环境建立模块生态系统公约。因而最初叫做 ServerJS。后来觉得显得太有局限性,就把名字改成了 CommonJS,又把浏览器包括了回来。在 CommonJS 的官网,是这么一句口号:
JavaScript: not just for browsers any more!
简单概括 CommonJS,即它是 JavaScript 语言的一种模块化规范。规定了模块的定义、引入和使用方式。 主要特点是同步加载,即只有加载完成后才能执行后面的代码。
模块定义
在 CommonJS 规范中,一个模块就是一个单独的文件。每个文件都是一个模块,文件内部的所有变量、函数和对象都属于该模块的私有作用域。要在其他模块中使用该模块的内部变量和方法,需要通过 module.exports 或 exports 对象进行导出。
exports 对象,是一个用于导出模块内容的通道
modlue 对象,是一个包含该模块元信息的执行结果,里面包含的有模块的 id,modlue,exports 等信息。
1 | // 定义一个模块,计算圆的面积和周长 |
在上面的代码定义了一个模块,包含了计算圆的面积和周长的方法。最后通过 module.exports 对象将这些方法导出,以便其他模块可以调用。
问题 1:module.exports 和 exports 的关系是什么
exports = 值,不会改变 modlue.exports 的内容空间
但如果通过 exports.xxx = 值,会改变 modlue.exports 的内容
同时 module.exports === this 为 true
模块引入
在 CommonJS 规范中,通过 require 函数来引入模块。require 函数的参数是模块的路径,返回值是模块导出的对象。
1 | //引入上面代码 |
简介
ECMAScript Modules 模块化
ECMAScript Modules 又称 ES Modules,缩写 ESM。因为其首次在 ECMAScript 6 中被提出,也称其为 ES6 Modules。下面我们统称 ES6 Modules。ES6 Modules 规范中引入了一种新的模块化机制。它的设计非常“精简”与“官方”,从语法层面就完成了对模块的定义。像 CommonJS 也好,AMD、CMD 等也罢,都是通过三方实现函数和对象来模拟模块,而 ESM 则直接通过 import 与 export 语法来导入和导出模块。只要宿主支持,那么该语法就直接能用。
模块定义
ES6 模块化中,一个模块可以通过 export 关键字来导出变量、函数或对象,也可以通过 import 关键字来引入其他模块的内容。
1 | // export.js |
模块引入
ES6 模块化中,通过 import 关键字导入一个或多个模块,也可以使用 import * as 命令将所有导出的模块作为一个对象导入。import 关键字的参数是模块的路径,返回值是模块导出的对象。
1 | // module.js |
也可以在一个文件夹下的 index.js 中。批量导出所引入的模块。
1 | //utils/index.js |
在所需要的模块中就可以直接使用,而不需要每个文件都通过 import 来引入
1 | //xxx.js |
ESM 还支持动态导入模块,这意味着我们可以在运行时动态地加载模块。
1 | // app.js |
问题 2:ES6 Modules 和 CommonJS 的区别
- CommonJS 模块输出的是一个值的拷贝,ES6 Modules 模块输出的是值的引用,因而当模块内部发生变化,ES6 Modules 可以跟踪到变化,而 CommonJS 不能。
- CommonJS 模块是运行时做的模块加载和运行,可以在代码执行一半的时候以动态的方式加载,这种方法在一些静态分析的时候会造成阻碍,ES6 Modules 模块是在模块顶部以语法的形式加载模块,完全可以做静态分析。