您的当前位置:首页正文

关于Node模块机制的解析

2020-11-27 来源:品趣旅游知识分享网

CommonJS 规范了 模块二进制Buffer二进制I/0,进程环境文件系统web服务器网管接口包管理等

二、CommonJS和Node的关系

Node的出现离不开CommonJS规范的影响,而CommonJS能以一种独寻常的姿态出现在各大公司的代码中,离不开Node优异的表现。

1894707805-5b4055ccaccd9_articlex[1].png

三、CommonJS的模块规范

1. 模块引入

var http = require("http");

2. 模块导出

// math.js
exports.add = function(){
 ...
}
// program.js
Var math = require('math.js');

exports.increament = function(val){
 return math.add(val);
}

3. 模块标识

模块标识就是require()的括号中的参数,必须是小驼峰结构,可以是相对路径也可以是绝对路径,还可以没有后缀名。


四、Node的模块实现

node 的模块实现实际上是借鉴了CommonJS的部分,并不是全部照搬,而且也增加了一些自己需要的东西进去。

nodejs-require[1].jpg

1. 模块引入

node中模块引入需要经过:

1. 路径分析
2. 文件定位
3. 编译执行

2. 模块分类

1. 核心模块 Node提供的
2. 文件模块 用户编写的

3. 核心模块

核心模块部分在Node源码加载中就编译完毕了,编译成了二进制执行文件,在Node进程启动后,部分核心模块就直接加载进内存中,所以可以不用`文件定位`和`编译`,因此部分核心模块的加载时最快的。

4. 文件模块

文件模块则是在运行时动态加载,要经过完整的路径分析、文件定位、编译执行,一次一般比较慢。

5. 缓冲加载

node对二次加载的核心模块,一律采用的时缓冲优先的原则。

6. 路径分析和文件定位

模块标识符分析
  • 核心模块

  • 核心模块的加载仅次于缓冲加载
  • 路径形式的文件模块

  • 首先将其转换为真实的路径,然后以真实路径作为索引,将其编译后放进缓冲中,等待调用。
    由于是通过确切的文件地址找到的,所以需要花费一定的时间,
  • 自定义模块

  • 这是非核心模块,也不是路径形式的标识,它是一种特殊的文件模块,可能是一个文件或者是一个包。这种查找是最费时间的。首先需要知道一下/模块路径/的概念/
    
    > 例子
    
    比如你要加载一个包,这个包放在了node_modules文件夹下,你要引入的话可以不以路径的形式写,可以是只写名称。(也就是引入一个自己npm的包)
    
    console.log(module.paths);
    // 会得到下面的数组
    [ 'D:\\myweb\\node\\node\\module\\node_modules',
     'D:\\myweb\\node\\node\\node_modules',
     'D:\\myweb\\node\\node_modules',
     'D:\\myweb\\node_modules',
     'D:\\node_modules' ]
    
    这就是模块路径了,查找机制是: 首先在当前路径下找是否有node_modules文件夹下的该包,如果没有就查找上一层目录,依次类推,直到根目录下的node_modules,如果依旧没有找到,那么就报错了。
    
    // 显然这种情况就是导致它速度较慢的主要原因了,(查找的路径越深就越慢),但是我们可以通过一些小技巧来经可能的减少这种情况哦~~
    文件定位

    文件定位中还需要注意的包括有文件扩展名目录包的处理

    node 的模块引入的时候是可以不写扩展名的,node会按照js json node的顺序来分析。依次尝试。由于尝试使用的是node中fs模块的同步文件查找,因此可能会导致阻塞情况发生,因此这里我们需要注意两个小技巧了:

    小技巧:

    1. json node文件最好加扩展名

    2. 同步配合缓冲可以环节Node单线程阻塞调用的缺陷

    另外,如果没有找到对应的文件,确实找到了一个目录,那么将会将其当作是一个包来处理了。

    如何在这个包下找到我们需要引入的入口文件对呢?

    1. 首先找是否含有package.json,如果有,则分析它的main属性,找到main属性对应的那个文件。

    2. 如果没有package.json或者是main解析失败了,那么就找文件名为index的文件,依次从index.js index.json index.node查找。

    3. 如果在该目录下依旧没有找到,那么就查找写一个匹配的目录,如果仍然没有找到,那么就报错了。


    6. 模块编译

    node中每一个模块都是一个对象,当定位到一个文件的时候,node就会将其包装成一个module对象,然后根据不同的文件名,其载入方法也不同的。

    1. js文件: 通过fs的同步读取文件来执行

    2. json文件,通过fs同步读取文件,用Json.parse()来得到对象,将其赋值给Module.exports

    3. node文件,这事c/c++编写的扩展文件,通过Process.dlopen()方法来加载执行。(不需要编译)

    2307702596-5b408e8838bfc_articlex[1].png


    7. module.exports和exports的区别和联系

    exports = module.exports = {};的区别就和 var a = {}; b = a;的区别一样.

    首先要明确的一点,module是一个对象 {Object}。
    当你新建一个文件,比如mo.js,文件内容如下:

    console.log(module);

    然后在CMD里执行这个文件node mo.js,就能看到module其实是一个Module实例,你可以这么理解,NodeJS中定义了一个Module类,这个类中有很多属性和方法,exports是其中的一个属性:

    function Module {
     id : 'blabla',
     exports : {},
     blabla...
    }

    当每一个文件被执行或者时require的时候,node就会创建一个module实例。var module = new Module();

    console.log(module); //你会看到Module中的exports为空对象{}
    module.exports = {
     print : function(){console.log(12345)}
    }
    console.log(module); //你会看到Module中的exports对象已经有了print()方法

    module.exports 其实就是module实例中的module添加方法或者属性。

    console.log(module); //你会看到Module中的exports为空对象{}
    console.log(exports); //你会看到Module中的exports为空对象{}
    module.exports = {
     print : function(){console.log(12345)}
    }
    console.log(module); //你会看到Module中的exports对象有了print()方法
    exports.name = '小白妹妹';
    console.log(module); //你会看到Module中的exports对象不仅有了print()方法,还有了name属性

    不难看出exports其实就是module.exports的一个引用。

    // 你可以这么理解
    var module = new Module();
    var exports = module.exports;

    当你require的时候,返回的就是module.exports的内容。

    // 常用场景分析
    module.exports.name = '小白妹妹';
    exports.age = 10;
    module.exports.print = function(){console.log(12345)};
    //如果只是添加属性和方法,两者可以混用。
    
    // 也可以
    module.exports = {
     name = '小白妹妹';
    };
    exports.age = 10;
    module.exports.print = function(){console.log(12345)};
    
    // 【X】但是不可以
    module.exports = {
     name = '小白妹妹';
    };
    exports = {age:10}; // exports现在是{age:10}这个对象的引用,不再是module.exports的引用了
    console.log(module); //你会看到Module的exports中只有name属性!!!
    
    // 【X】
    exports.age = 10; 
    console.log(module); //你会看到Module的exports中多了age属性
    module.exports = {
     name = '小白妹妹';
    };
    // 直接改变了module.exports的引用,之前的属性值也被覆盖掉了。
    console.log(module); //你会看到Module的exports中还是只有name属性!!!

    总结

    1. 改变exports的指向后所添加的exports.xxx都是无效的。因为require返回的只会是module.exports

    2. 不能在使用了exports.xxx之后,改变module.exports的指向。因为exports.xxx添加的属性和方法并不存在于module.exports所指向的新对象中。

    3. 对于要导出的属性,可以简单直接挂到exports对象上

    4. 对于类,为了直接使导出的内容作为类的构造器可以让调用者使用new操作符创建实例对象,应该把构造函数挂到module.exports对象上,不要和导出属性值混在一起

    Node2[1].png

    显示全文