模块化
- Commonjs, AMD, CMD, UMD, ES Module, import()
模块化的几个原则
- 可复用性, 可组合型, 中心化, 独立性
- 方便开发者维护和复用,模块之间又可以通信和调用
“模块化”的实质
- 立即执行函数结合闭包, 通过立即执行函数(IIFE),我们构造一个私有的作用域,再通过闭包,将需要对外暴露的数据和接口输出,我们称此为 IIFE 模式
- 再结合顶层 window 对象,将需要暴露的数据和接口挂载到window的一个module对象上即可
CommonJS
- 在 Node.js 中,每一个文件就是一个模块,具有单独的作用域,对其他文件是不可见的
- 几个容易被忽略的特点
- 文件即模块,文件内所有代码都运行在独立的作用域,因此不会污染全局空间。
- 模块可以被多次引用、加载。在第一次被加载时,会被缓存,之后都从缓存中直接读取结果。
- 加载某个模块,就是引入该模块的 module.exports 属性。
- module.exports 属性输出的是值的拷贝,一旦这个值被输出,模块内再发生变化不会影响到输出的值。
- 模块加载顺序按照代码引入的顺序。
- 注意 module.exports 和 exports 的区别
AMD
- AMD 规范,全称为:Asynchronous Module Definition,看到 “Asynchronous”,我们就能够反映到它的模块化标准不同于 CommonJS,是异步的,完全贴合浏览器的。
- 它规定了如何定义模块,如何对外输出,如何引入依赖。这一切都需要代码去实现,因此一个著名的库 —— require.js 应运而生,
- require.js 实现很简单:通过 define 方法,将代码定义为模块;通过 require 方法,实现代码的模块加载。
define 和 require 就是 require.js 在全局注入的函数。
- require.js 在全局定义了 require 和 define 两个方法,也是利用立即执行函数,将全局对象(this)和 setTimeout 传入函数体内, 而 require方法的主要作用是完成创建 script 标签去请求相应的模块,对模块进行加载和执行
CMD
- CMD 规范整合了 CommonJS 和 AMD 规范的特点。它的全称为:Common Module Definition,类似 require.js,CMD 规范的实现为 sea.js。
- AMD 和 CMD 的两个主要区别如下。
- AMD需要异步加载模块,而CMD在require依赖的时候,可以通过同步的形式(require),也可以通过异步的形式(require.async)。
- CMD遵循依赖就近原则,AMD遵循依赖前置原则。也就是说,在 AMD 中,我们需要把模块所需要的依赖都提前在依赖数组中声明。而在 CMD 中,我们只需要在具体代码逻辑内,使用依赖前,把依赖的模块 require 进来。
UMD
- 它允许在环境中同时使用 AMD 与 CommonJS 规范,相当于一个整合。
- 该模式的核心思想在于利用立即执行函数根据环境来判断需要的参数类别
模块化的原生解决方案:ES Module
- ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西
- CommonJS, 运行时加载, 动态加载, 因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”
- ES Module, 编译时加载, 静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入
- 由于 ES6 模块是编译时加载,使得静态分析成为可能。有了它,就能进一步拓宽 JavaScript 的语法,比如引入宏(macro)和类型检验(type system)这些只能靠静态分析实现的功能
- import和export命令只能在模块的顶层,不能在代码块之中(比如,在if代码块之中,或在函数之中),因为JS引擎处理import语句是在编译时,这时不会去分析或执行if语句,所以import语句放在if代码块之中毫无意义,因此会报句法错误,而不是执行时错误。
- 这样的设计,固然有利于编译器提高效率,但也导致无法在运行时加载模块。在语法上,条件加载就不可能实现。如果import命令要取代 Node 的require方法,这就形成了一个障碍。因为require是运行时加载模块,import命令无法取代require的动态加载功能
- import()函数,完成动态加载,import命令能够接受什么参数,import()函数就能接受什么参数,两者区别主要是后者为动态加载。import()返回一个 Promise 对象
- import()函数可以用在任何地方,不仅仅是模块,非模块的脚本也可以使用。它是运行时执行,也就是说,什么时候运行到这一句,就会加载指定的模块。另外,import()函数与所加载的模块没有静态连接关系,这点也是与import语句不相同。import()类似于 Node 的require方法,区别主要是前者是异步加载,后者是同步加载
- import()的一些适用场合
- 按需加载,import()可以在需要的时候,再加载某个模块
- 条件加载,import()可以放在if代码块,根据不同的情况,加载不同的模块
- 动态的模块路径,import()允许模块路径动态生成
- ES6 模块与 CommonJS 模块完全不同,它们有两个重大差异
- CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口
- ES 模块化为什么要设计成静态的?
- 一个明显的优势是:通过静态分析,我们能够分析出导入的依赖。如果导入的模块没有被使用,我们便可以通过 tree shaking 等手段减少代码体积,进而提升运行性能。这就是基于 ESM 实现 tree shaking 的基础。