模块机制
CommonJs规范
-
模块引用
上下文提供require()方法来引人外部模块var math = require('math')
-
模块定义
- exports 对象用于到处当前模块中的方法和变量
- module代表模块自身
exports.add = function() {...}
- 在另一个模块中使用require()方法进行导入。就可以使用
区别和联系 module对象: 在每个js自定义模块中都存在module对象。 Module { id: '', path: '', exports: {}, ... } module.exports: 在module对象中,可以使用module.exports来进行共享 exports是modeule.exports的简写
-
模块标识
模块标识其实就是传递给require()方法的参数,它必须是符合小驼峰命名的字符串,或者…开头的相对路径,或者绝对路径。可以没有文件名后缀.is。
将类聚的方法和变量等限定在私有的作用域中,同时支持引人和导出功能以顺畅地连接上下游依赖。(如上图所示) -
node 的模块实现
- 在node中引入模块,需要经历路劲分析,文件定位,编译执行。
-
文件分类
- 核心模块
核心模块部分在node源代码的编译过程中,编译进了二进制执行文件。在node进程启动的时候,部分核心模块被加载进入内存中,所以这部分模块引入到时候,文件定位和编译执行这个两个步骤可以省略掉,在路径分析中优先判断。加载速度是最快的。
在核心模块中,有些模块由c/c++进行编写,有的模块由c/c++完成核心部分。其他的部分由JavaScript实现包装或者向外导出。以满足性能需求。c++模块主内完成核心,JavaScript主外实现封装是node能够实现高性能的常见方式。 - 文件模块
文件模块是在运行的时候动态加载,需要完整的路劲分析,文件定位,编译执行过程,速度比核心模块慢。
- 核心模块
-
优先从缓存加载
- node对于引入过的模块回进行缓存。减少二次引入的开销。浏览器只是缓存文件,但是node缓存的是编译和执行后的对象。
3. 路径分析和文件定位
1. 模块标识符分析
dotnetcli node中的模块标识符 1. 核心模块,如http,fs,path 2. . 或者是..开始的相对路径文件模块 3. 以/开始的绝对路劲文件模块 4. 非路径形式的文件模块。 以. 或者..或者/开始的标识符,被当作文件模块来处理,分析路劲模块的时候,require()会将路径转换为真实路径,并且以真实路径作为索引。将编译执行后的结果放到缓存中,使得二次加载的时候更快。
2. 模块路径
+ 模块路径是node在定位文件规模的具体文件的时候,指定的查找策略,具体表现为一个路径组成的数组。
+ 模块路径的生成规则:在加载过程中,node会逐个尝试模块路径中的路径,知道找到目标文件为止。当前文件的路径越深,模块加载耗时越多。自定义模块在速度是最慢的
3. 文件定位
1. 文件拓展名分析
在分析标识符的时候,会出现标识符不包含文件拓展名的情况,正常情况下,node会按照js json node的顺序补足拓展名
在尝试为文件加上后缀的时候,需要调用fs模块同步阻塞是判断文件是否存在,提升性能: node和json文件,在传递个require的时加上后缀名。
2. 目录分析和包
1. node在当前目录下查找package.json(commonjs规范的包描述文件)
2. 通过json.parse()解析出包描述文件,从中取出main属性指定的文件名进行定位
3. 如果文件名缺少拓展,会进入拓展名分析步骤 、
4. 如果main属性指定的文件名错误,或者压根没有package.json文件,node会将index当作默认文件名。然后依次查找index.js, index.json, index.node
- node对于引入过的模块回进行缓存。减少二次引入的开销。浏览器只是缓存文件,但是node缓存的是编译和执行后的对象。
-
模板编译
// 模板对象 function Module(id, parent) { this.id = id; this.exports = {}; this.parent = parent; if (parent && parent.children) { parent.children.push(this) } this.filename = null; this.loaded = false; this.children = []; }
编译和执行是引入文件模块的最后一个阶段,定位到具体的文件之后,node会新建一个模块对象。根据路径进行载入并且编译,对于不同的文件拓展名,载入方法也有所不同。
js: fs模块 node: dlopen() json: fs模块=> JSON.parse() 其余: 当作js文件
每一个编译成功的模块都会将其文件路径作为索引缓存在Module._cache对象上,以提高二次引入的性能。
Module._extensions['json'] = function(module, filename) { var content = NativeModule.require('fs').readFileSync(filename, 'utf8') try { module.exports = JSON.parse(stripBOM(content)) } catch(err) { err.message = filename + ': ' + err.message throw err }
其中Module._extensions会被赋值为require()的extensions属性,在代码中require.extensions可以知道系统的已有的加载方式
如果想对自定义的托张敏进行特殊的加载,可以通过类似的require.extensions[‘.ext’]的方式来实现。官方建议将文件编译成js文件以后再进行执行- JavaScript模板的编译
- 编译的过程中,node对获取的JavaScript文件内容进行了头尾包装,在头部添加了(function (exports, require, module, __filename, __dirname) {\n 再尾部添加了\n}) 一个正常的JavaScript文件背包装成如下的样子
这样的每个模块文件之间都进行了作用域隔离,包装之后的代码会通过vm原生模块的runInThisContext()方法进行执行,类似于eval,有明确的上下文,不会污染环境,返回一个具体的function对象,最后将当前的模块对象的exports属性,requires方法,module以及再文件定义中得到的完整文件路径喝文件目录作为参数传递给这个function()执行(function (exports, require, module, __filename, __dirname) { var math = require('math') exports.area = function(radius) { return Math.PI * radius * radius } })
这些变量虽然没有再模块文件中定义,却会在文爱中存在。执行之后,exports会被返回给调用方。exports属性上的任何方法属性都可以外部调用到,但是模块中的其余变量或属性不可以杯直接调用- 理想情况: 只需要赋值给exports对象。如果需要达到require引入一个类的结果,赋值给module.exports对象。
- 编译的过程中,node对获取的JavaScript文件内容进行了头尾包装,在头部添加了(function (exports, require, module, __filename, __dirname) {\n 再尾部添加了\n}) 一个正常的JavaScript文件背包装成如下的样子
2. c/c++ 1. 使用通过process.dlopen()方法进行加载和执行。再node架构下,dlopen方法再window和方法下有不同的实现。 2. node的模块文件不需要编译,node文件实在编写c/c++之后编译生成的。然后返回给调用值。 3. JSON文件的编译 1. json的文件的编译是3种编译方式中最简单的。node利用fs模块同步读取json文件之后,调用json.parse()方法获得对象。然后将他赋值给对象的exports。如果定义了一个json文件作为配置。就不需要调用fs模块去异步读取或者编译,直接调用require()引入就可以。
- JavaScript模板的编译
-
模块间的调用关系