WdBly Blog

懂事、有趣、保持理智

周维的个人Blog

懂事、有趣、保持理智

站点概览

周维 | Jim

603927378@qq.com

推荐阅读

Node中模块机制

Node.js中的模块是CommonJS规范的一种实现,Node的成长发展离不开CommonJS规范,而CommonJS的推广应用离不开Node的优异表现,本文将带你深入Node.js的模块机制。

CommonJS中对模块的定义

CommonJS对模块的定义分为模块引用、模块定义和模块标识3个部分。

  • 模块引用: 在模块中存在require()方法,此方法接收模块标识作为参数,从而引入其它模块。
  • 模块定义: 在模块中存在exports属性,它是模块用于导出当前模块的方法和变量的唯一出口
  • 模块标识: 模块标识会作为参数传递给require()方法,它必须满足小驼峰命名规则。

核心模块和文件模块

  • 核心模块:核心模块是已经编译的二进制文件,Node在加载这类模块时省去了模块定位编译执行这两个步骤。
  • 文件模块:文件模块在Node运行期间动态加载,需要有路径分析编译执行等步骤。

模块缓存

在浏览器中,浏览器会将加载过的资源缓存在内存和硬盘中,缓存的是文件本身。 和浏览器类似,Node会对加载过的模块进行缓存,不同点在于Node缓存的是模块编译执行后的对象。 从缓存中加载模块是Node中模块加载的第一优先级。

Node中对模块的实现 - 文件模块

Node.js的模块机制设计遵循CommonJS中规定的模块规范,同时也新增了一些自身所需要的特性,下面一起来看一下。

模块标识分析(路径分析)

在Node中,基于模块标识查找模块,共有以下几类模块标识

  • 核心模块标识:http、path等。
  • 相当路径模块标识: .或..开头。
  • 绝对路径模块标识: /开头
  • 自定义模块: 三方包等,需要注意自定义模块不能与核心模块同名。

自定义模块的加载是根据模块路径从头到尾依次查找,直到根目录的方式,加载速度最慢。
通过module.paths可以获取模块的模块路径,它应该像这样:

[ '/Users/user/Documents/project/node_modules', '/Users/user/Documents/node_modules', '/Users/user/node_modules', '/Users/node_modules', '/node_modules' ]

image.png

模块文件定位

若模块标识中不含有文件扩展名,Node会按.js、.json、.node的次序补足扩展名,依次尝试。在这个过程中,需要fs模块同步阻塞判断。

可以通给标识符添加扩展名的方式优化。

Node在为不含文件扩展的模块标识添加后缀后,依然没有找到模块, 但是得到一个目录。Node会查找此目录下的package.json文件的main属性,对其指定的路径进行定位,如果此路径也缺少后缀,则重复此过程。
如果main属性指向文件错误或者没有package.json文件,会依次查找index.js、index.json、index.node。

image.png

模块编译

1、确定模块的编译方式
模块的编辑执行是Node模块加载的最后一阶段,在Node中每个模块都是一个对象。
对于不同扩展名的模块,采用的编译方式不一样

  • .js模块: 通过fs模块同步读取文件然后编译。
  • .node模块(扩展模块):通过dlopen()方法加载最后编译生成的文件。
  • .json模块:通过fs同步读取,然后使用JSON.parse()解析。
  • 其它后缀: 被当做js文件载入。

require.extensions可以获取系统以支持的扩展:{ '.js': [Function], '.json': [Function], '.node': [Function] }
对于其它扩展文件模块,应先将文件编译成js文件再加载。

2、js模块

Node在编译js模块时,会对模块的内容进行包装。

(function(exports, require, module, __filename, __dirname) { ... // 真实模块内容 });

这样每个模块文件的作用域得到隔离,并且将此模块的exports属性、全局的require()方法、和模块本身module作为参数传入。这就是这些变量并没有定义在每个模块文件中却存在的原因。
模块执行完成后,exports属性被返回给调用方var fn = require('./fn');

3、c/c++扩展模块
Node调用process.dlopen()方法进行加载和执行.node模块。
在windows和linux中的dlopen实现方式不一样,通过libuv层进行兼容。

C/C++模块给Node使用者带来的优势主要是执行效率方面的,劣势则是C/C++模块的编写门槛比JavaScript高。

4、json模块

json模块的编译比较简单, 通过fs读取文件并,然后使用JSON.parse()解析即可。

至此,Node的整个文件模块加载结束了,这就是Node对CommonJS模块规范的实现。

关于exports和module.exports

exports是module的一个属性,是输出当前模块的唯一出口。exports默认为空对象{}, 可以通过向exports对象上添加属性方法来导出。但是直接为exports赋值的方式通常会得到一个错误的结果。

// a.js console.log(require('b.js')); // {} // b.js exports = 1;

这是因为exports是作为一个形参传入的,不能通过修改形参来改变原数据。此时应该使用modlue.exports = 1;

Node中对模块的实现 - 核心模块

在Node中,核心模块其实分为两部分,一是使用Javascript编写的模块,二是使用c/c++编写的模块。Node对这两种核心模块处理是不一样的。

Javascript编写的模块

Javascript编写的模块存在于Node项目的lib目录下lib

核心模块是已经编译的二进制文件,Node在编译时,会将所有Javascript核心模块转存为c/c++代码。

Node采用了V8附带的js2c.py工具,将所有内置的JavaScript代码(src/node.js和lib/*.js)转换成C++里的数组, 此时javascript代码以字符串的方式存在于内存中。

当Node首次加载这些核心模块时,会直接从内存中获得模块代码,然后在对代码进行包装编译。

Node通过process.binding('natives')方法加载内存中的Js模块,此方法返回一个包含所有Js模块的对象:

{ "async_hooks": `code`, "fs": `code`, "http": `code`, ... }

Javascript编写的核心模块大多只是一个包装和导出,其核心功能通常使用c/c++完成的。这样处理是为了利用JS静态语言的优势提升性能。

Javascript编写的核心模块和文件模块的区别

  • 获取源代码方式: 核心模块从内存中获取, 文件模块从磁盘中。
  • 缓存位置: 核心模块缓存在NativeModule._cache对象上, 文件模块缓存在Module._cache对象上。

c/c++编写的模块

全部由c/c++编写的模块通常被称之为内建模块

内建模块的结构定义:

struct node_module_struct { int version; void *dso_handle; const char *filename; void (*register_func) (v8::Handle<v8::Object> target); const char *modname; };

Node将所有内建模块统一放到node_module_list数组中(c/c++层面),通过get_builtin_module()可以取出。

核心模块的加载流程

1、Node启动时将 核心模块加载到内存中。
image.png

2、Node加载一个核心模块
require('os')为例:

image.png

os本身是一个内建模块,但是当直接require去加载时,是去加载经过包装后的JS核心模块process.binding('natives')中的os对象。此对象再去调用c/c++中提供的方法。

在Node中可以使用process.binding('os')直接加载一个内建模块。此方法返回一个js对象:

{ getHostname: [Function: getHostname], getLoadAvg: [Function: getLoadAvg], getUptime: [Function: getUptime], getTotalMem: [Function: getTotalMem], getFreeMem: [Function: getFreeMem], getCPUs: [Function: getCPUs], getOSType: [Function: getOSType], getOSRelease: [Function: getOSRelease], getInterfaceAddresses: [Function: getInterfaceAddresses], getHomeDirectory: [Function: getHomeDirectory], getUserInfo: [Function: getUserInfo], isBigEndian: false }

总结

以上提到了文件模块,核心模块,扩展模块,内建模块这就几种模块,我们有必要再清理一遍各模块的调用关系。

  • C/C++内建模块属于最底层的模块,它属于核心模块,主要提供API给JavaScript核心模块和第三方JavaScript文件模块调用。

  • 如果你不是非常了解要调用的C/C++内建模块,请尽量避免通过process.binding()方法直接调用,这是不推荐的。

  • JavaScript核心模块主要扮演的职责有两类:一类是作为C/C++内建模块的封装层和桥接层,供文件模块调用;一类是纯粹的功能模块,它不需要跟底层打交道,但是又十分重要。

  • C/C++扩展模块通过预先编译为.node文件,然后调用process.dlopen()方法加载执行,扩展模块属于文件模块。
    image.png

参考资源:
《深入浅出Node.js》

提交

全部评论0

暂时没有评论...