模块机制
1. CommonJS
1. 模块规范
a. 模块引用
- 在CommonJS中,存在require()方法,接收模块标识,以此引入一个模块的API到当前模块中。
b. 模块定义
- 对应引入功能,上下文提供了exports对象用于导出当前模块的方法或者属性,并且是唯一的出口。
- 在模块中,module对象代表模块自身,而exports是module的属性。
- 在Node中,一个文件就是一个模块,将方法挂载在exports对象上作为属性即可定义导出的方式。
c. 模块标识
- 模块标识其实就是传递给require()方法的参数,它必须是小驼峰命名法的字符串,可以没有后缀.js。
CommonJS构建的这套模块导出和引入机制使得用户无需考虑全局污染的问题。
2. Node的模块实现
-
Node在引入模块时需要经历如下三个步骤:
-
路径分析
-
文件定位
-
编译执行
-
-
Node的模块分为两类:核心模块和用户自定义的文件模块。
-
核心模块在Node源代码编译过程中已经被编译进了二进制的执行文件。在Node进程启动时,部分核心模块就直接被加载进内存中,所以这部分核心模块引入时可以省略前两个步骤,加载速度是最快的。
-
文件模块实在运行时动态加载,需要完整地经历上面三个步骤,速度比加载核心模块慢。
2.1 优先从缓存加载
- Node对引入过的模块都会进行缓存,以减少第二次引入时的开销。
- Node缓存的时编译和执行后的对象。
- 不论是核心模块还是文件模块,require方法对相同模块的二次加载都采用缓存优先的方式,这是第一优先级的。且核心模块的缓存检查优先于文件模块。
2.2 路径分析
-
核心模块
- 核心模块的优先级仅次于缓存加载,在Node的源代码编译过程中已经编译进了二进制代码,加载过程最快。
- 如果文件模块的标识符与核心模块相同,如果试图加载之是不会成功的。解决方法可以采用路径引入。
-
路径形式的文件模块
-
在分析文件模块时,require方法会将路径转化为真实路径,以真实路径为索引,将编译执行后的结果存放到缓存中,以使第二次加载更加迅速。
-
由于指定了路径,查找过程中可以节约大量的时间。
-
-
自定义模块
- 自定义模块指的是非核心模块,也不是路径形式的标识符。
- 这是一种最费时间的查找方式。
- 它的加载方式与原型链或者作用域链的方式十分相似。在加载过程中,Node会从当前文件目录开始,往外一层一层查找,直到找到目标文件为止。
- 这种方式是最耗时间的,是自定义模块加载速度最慢的原因。
2.3 文件定位
-
文件扩展名分析
-
当标识符不包含文件扩展名时,Node会按照.js、.json、.node的顺序补全扩展名依次尝试。
-
在尝试的过程中,会调用fs模块以同步阻塞的形式判断文件是否存在。
-
如果是.node或者.json文件,在引入时加上扩展名,会提高速度。
-
同步配合缓存,可以大幅度环节Node单线程中阻塞式调用的缺陷。
-
-
目录分析和包
- 当Node在查找时没有找到对应模块,但是得到了一个目录,此时Node就会把他当成包来处理。
- 首先,Node在当前目录下查找package.json文件,通过JSON.parse方法解析出包的描述对象,取出main属性指定的文件名进行扩展名分析定位。
- 如果main属性指定的文件名错误,或者没有package.json,则额Node会将index作为默认的文件名,进行扩展名分析定位。
- 如果目录分析过程失败了,则自定义模块就会进入下一个模块路径进行查找。如果所有路径都遍历却没有找到,就抛出查找失败异常。
2.4 模块编译
-
编译和执行式引入文件模块后的最后一步。
-
定位到文件后,Node会新建一个模块对象,然后根据路径载入编译。
-
不同的拓展名有着不同的载入方法:
js:通过fs模块同步读取文件后进行编译执行
json:通过fs模块同步读取文件后,用JSON.parse()方法解析返回的结果。
node:这是C/C++编写的扩展文件,通过dlopen()方法加载编译后生成的文件。
其他:当作js文件导入。
-
每个编译成功后的模块会将其路径作为索引缓存在
Module._cache
对象上,提高二次引用的性能。 -
JavaScript模块编译
- 在编译过程中,Node对获取的JavaScript文件进行头尾包装。将整个JS模块用
function(exports,require,module,__filename,__dirname){}
进行包围; - 这样做,每个模块文件之间都进行了作用域隔离;
- 包装后的代码会通过vm原生模块的runInThisContext方法执行,返回一个具体的function对象;
- 最后,将当前对象的exports属性、require方法、module和文件定位中得到的完整文件路径和文件目录作为参数传入。
这就是为什么模块里没有定义却有exports、require等。执行后,模块的exports属性被返回给了调用方,exports属性上的任何方法和属性都可以被外部调用到,但模块里的其余变量却不能直接调用。
- 在编译过程中,Node对获取的JavaScript文件进行头尾包装。将整个JS模块用
-
C/C++模块编译
- Node调用process.dlopen()方法进行加载执行。
- .node模块不需要编译,因为他是编写C/C++模块之后生成的,所以只需要加载和执行。
- 执行过程中,模块的exports对象与.node模块产生联系,然后返回给调用者
-
JSON文件的编译
- Node利用fs模块同步读取JSON文件里的内容后,调用JSON.parse方法得到对象,然后将他赋值给模块对象的exports,供外部调用。
- JSON文件适合作项目的配置文件。当你用JSON文件作为配置文件时,无需再调用fs模块去异步读取和解析,直接进行require既可。这么做可以享受到模块缓存带来的优势。
2.5 包与NPM
-
CommonJS的包规范有包结构和包描述文件两个部分组成,前者用于组织包内的各种文件,后者用于描述包的相关信息。
-
包结构
包是一个存档文件,即将一个目录直接打包成.zip或者tar.gz文件,安装后解压文件至目录。
完整的包目录需要包含以下文件
package.json:包描述文件
bin:用于存放可执行的二进制文件的目录
lib:用于存放JavaScript代码的目录
doc:用于存放文档的目录
test:用于存放单元测试用例的代码
-
包描述文件
包描述文件用于表达非代码相关的信息——package.json。
包的规范定义可以帮助Node解决依赖包安装的问题,而NPM正是基于此规范进行了实现。
在包描述文件中,NPM需要的字段有:name、version、description、keywords、repositories、author、bin、main、scripts、engines、dependencies、devDependencies。
3. AMD规范
-
AMD规范是CommonJS模块规范的一个延申,适用于前端场景。
-
它的模块id和依赖实可选的,与Node模块相似的地方在于factory内容就是实际代码的内容。
define(id?,dependencies?,factory); // 举个例子 define(function(){ var exports = {}; exports.sayHello = function() { return 'hello'; }; return exports; });
-
与Node不一样的是,AMD模块需要用define来明确定义一个模块,而Node是隐式包装的,目的是进行作用域隔离;
-
还有一个区别就是AMD模块需要显示地返回exports对象。
4. CMD规范
-
CMD规范与AMD的主要区别在于定义模块和依赖引入的部分。
-
AMD规范需要在声明的时候指定所有的依赖:
define(['dep1','dep2'],function(dep1,dep2){ return function(){}; })
-
而CMD规范支持动态引入依赖,可以随时调用require进行引入。
define(function(require,exports,module){ // 模块代码 })