CommonJS 定义了 module、exports 和 require 模块规范,Node.js 为了实现这个简单的标准,从底层 C/C++ 内建模块到 JavaScript 核心模块,从路径分析、文件定位到编译执行,经历了一系列复杂的过程。简单的了解 Node 模块的原理,有利于咱们从新认识基于 Node 搭建的框架。前端
CommonJS 规范或标准简单来讲是一种理论,它指望 JavaScript 能够具有跨宿主环境执行的能力,不只能够开发客户端应用,还能够开发服务端应用、命令行工具、桌面图形界面应用等。node
CommonJS 规范对模块的定义分为三个部分:json
模块定义后端
在模块中存在module
对象表明模块自己,模块上下文提供exports
属性 ,将方法挂载在exports
对象上便可以定义导出方式,例如:数组
// math.js
exports.add = function(){ //...}
复制代码
模块引用浏览器
module
提供require()
方法引入外部模块的 API 到当前的上下文中:缓存
var math = require('math')
复制代码
模块标识bash
模块标识实际就是传递给require()
方法中的参数,能够是按小驼峰(camelCase)命名的字符串,也能够是文件路径。服务器
Node.js 借鉴了 CommonJS 规范的设计,特别是 CommonJS 的 Modules 规范,实现了一套模块系统,同时 NPM 实现了 CommonJS 的 Packages 规范,模块和包组成了 Node 应用开发的基础。网络
上述模块规范看起来十分简单,只有module
、exports
和require
,但 Node 是如何实现的呢?
须要经历路径分析(模块的完整路径)、文件定位(文件扩展名或目录)、编译执行三个步骤。
回顾require()
接收 模块标识 做为参数来引入模块,Node 就是基于这个标识符进行路径分析。不一样的标识符采用的分析方式是不一样的,主要分为一下几类:
核心模块在 Node 源码编译时存为二进制执行文件,在 Node 启动时直接加载到内存中,路径分析中优先判断,因此加载速度很快,并且也不用后续的文件定位和编译执行。
若是想加载与核心模块同名的自定义模块,如自定义 http 模块,那必须选用不一样标志符或改用路径方式。
.、..
相对路径模块和/
绝对路径模块以.、..或/
开始的标识符都会当成文件模块处理,Node 会将require()
中的路径转为真实路径做为索引,而后编译执行。
因为文件模块明确了文件位置,因此缩短了路径分析时间,加载速度仅慢与核心模块。
即不是核心模块,也不是路径形式的文件模块,自定义文件是特殊的文件模块,在路径查找时 Node 会逐级查找该模块路径中的路径。
模块路径查找策略示例以下:
// paths.js
console.log(module.paths)
// Terminal
$ node paths.js
[ '/Users/tong/WebstormProjects/testNode/node_modules',
'/Users/tong/WebstormProjects/node_modules',
'/Users/tong/node_modules',
'/Users/node_modules',
'/node_modules' ]
复制代码
从上述示例输出的模块路径数组能够看出,模块的查找时沿当前路径向上逐级查找node_modules
目录,直到目标路径为止,相似 JS 原型链或做用域链。路径越深速度越慢,因此自定义模块加载速度最慢。
缓存优先机制:Node 会对引入过的模块进行缓存以提升性能,不一样于浏览器缓存的是文件,Node 缓存的是编译和执行后的对象,因此
require()
对相同模块的二次加载采用缓存优先的方式。这个缓存优先是第一优先级的,比核心模块的优先级要高!
模块路径分析完成后是文件定位,主要包括文件扩展名的分析、目录和包的处理。为了表达的更清晰,将文件定位分为四个步骤:
一般require()
中的标识符是不包含文件扩展名的,这种状况下,Node会按照 .js、.json、.node 的顺序尝试补充扩展名。
在尝试补充扩展名时,须要调用 fs 模块同步阻塞式判断文件是否存在,因此这里提高性能的小技巧,就是 .json 和 .node 文件传递给require()
时带上扩展名会加快一些速度。
若是补充扩展名后没有找到对应文件,可是获得了一个目录,此时 Node会将目录当作一个包处理。依据 CommonJS 包规范的实现,Node 会在目录下查找pakage.json
(包描述文件),经过JSON.parse()
解析成包描述对象,从中取main
属性指定的文件名定位。
若是没有pakage.json
或者main
属性指定的文件名错误,那 Node 会将 index 当作默认文件名,依次查找 index.js、index.json、index.node
在上述目录分析过程当中没有成功定位时,自定义模块按路径查找策略进入上一层node_modules
目录,当整个模块路径数组遍历完毕后没有定位到文件,则会抛出查找失败异常。
缓存加载的优化策略使得二次引入不须要路径分析、文件定位、编译执行这些过程,并且核心模块也不须要文件定位的过程,这大大提升了再次加载模块时的效率
Node 中每一个模块都是一个对象,在具体定位到文件后,Node 会新建该模块对象,而后根据路径载入并编译。不一样的文件扩展名载入方法为:
JSON.parse()
解析并返回结果process.dlopen()
方法加载最后编译生成的载入成功后 Node 会调用具体的编译方式将文件执行后返回给调用者。对于 .json 文件的编译最简单,JSON.parse()
解析获得对象后直接赋值给模块对象的exports
,而 .node 文件是C/C++编译生成的,Node 直接调用process.dlopen()
载入执行就能够,下面重点介绍 .js 文件的编译:
在 CommonJS 模块规范中有module
、exports
和 require
这3个变量,在 Node API 文档中每一个模块还有 __filename
、__dirname
这两个变量,可是在模块中没有定义这些变量,那它们是怎么产生的呢?
事实上在编译过程当中,Node 对每一个 JS 文件都被进行了封装,例如一个 JS 文件会被封装成以下:
(function (exports, require, module, __filename, __dirname) {
var math = require('math')
export.add = function(){ //... }
})
复制代码
首先每一个模块文件之间都进行了做用域隔离,经过vm原生模块的runInThisContext()
方法(相似 eval)返回一个具体的 function 对象,最后将当前模块对象的exports
属性、require()
方法、模块对象自己module
、文件定位时获得的完整路径__filename
和文件目录__dirname
做为参数传递给这个 function 执行。模块的exports
属性上的任何方法和属性均可以被外部调用,其他的则不可被调用。
至此,module
、exports
和 require
的流程就介绍完了。
曾经困惑过,每一个模块均可以使用exports
的状况下,为何还必须用module.exports
。
由于exports
只是module.exports
的一个地址引用,如module.exports
已经具有一些属性和方法,Node 会忽略exports
只导出 module.exports
。因此直接赋值给module.exports
会更准确。
编译成功的模块会将文件路径做为索引缓存在
Module._cache
对象上,路径分析时优先查找缓存,提升二次引入的性能。
总结来讲 Node 模块分为Node提供的核心模块和用户编写的文件模块。文件模块是在运行时动态加载,包括了上述完整的路径分析、文件定位、编译执行这些过程,核心模块在Node源码编译成可执行文件时存为二进制文件,直接加载在内存中,因此不用文件定位和编译执行。
核心模块分为 C/C++ 编写的和 JavaScript 编写的两部分,在编译全部 C/C++ 文件以前,编译程序须要将全部的 JavaScript 核心模块编译为 C/C++ 可执行代码,编译成功的则放在 NativeModule._cache
对象上,显然和文件模块 Module._cache
的缓存位置不一样。
在核心模块中,有些模块由纯 C/C++ 编写的内建模块,主要提供 API 给 JavaScript 核心模块,一般不能被用户直接调用,而有些模块由 C/C++ 完成核心部分,而 JavaScript 实现封装和向外导出,如 buffer、fs、os 等。
因此在Node的模块类型中存在依赖层级关系:内建模块(C/C++)—> 核心模块(JavaScript)—> 文件模块。
使用require()
十分的方便,但从 JavaScript 到 C/C++ 的过程十分复杂,总结来讲须要经历 C/C++ 层面内建模块的定义、(JavaScript)核心模块的定义和引入以及(JavaScript)文件模块的引入。
对比先后端的 JavaScript,浏览器端的 JavaScript 须要经历从同一个服务器端分发到多个客户端执行,经过网络加载代码,瓶颈在于宽带;而服务器端 JavaScript 相同代码须要屡次执行,经过磁盘加载,瓶颈在于 CPU 和内存,因此先后端的 JavaScript 在 Http 两端的职责彻底不用。
Node 模块的引入几乎是同步的,而前端模块若是同步引入,那脚本加载须要太长的时间,因此 CommonJS 为后端 JavaScript 制定的规范不适合前端。然后出现 AMD 和 CMD 用于前端应用场景。
AMD 即异步模块定义(Asynchronous Module Definition),模块定义为:
define(id?, dependencies?, factory);
复制代码
AMD 模块须要用define
明肯定义一个模块,其中模块id
与依赖dependencies
是可选的,factory
的内容就是实际代码的内容。例如指定一些依赖到模块中:
define(['dep1', 'dep2'], function(){
// module code
});
复制代码
require.js 实现 AMD 规范的模块化,感兴趣的能够查看 require.js 的文档。
CMD 模块的定义更加简单:
define(factory);
复制代码
定义的模块同 Node 模块同样是隐式包装,在依赖部分支持动态引入,例如:
define(function(require, exports, module){
// module code
});
复制代码
require
、exports
、module
经过形参传递给模块,须要依赖模块时直接使用require()
引入。
sea.js 实现 AMD 规范的模块化,感兴趣的能够查看 sea.js 的文档。