node的模块机制

首先,在node出现之前,JavaScript由于缺少模块系统,标准库和包管理工具,在服务端一直没有市场,为了让JavaScript达到和Java,PHP等语言一样开发大型应用的能力。在经过十多年的发展之后 ,提出了CommonJS规范。

CommonJS模块规范主要分为三大类:
其一:模块引用
const request = require(‘request’);
在CommonJS中,存在require方法,这个方法接受模块标识,以此引入一个模块的API到当前的上下文中

其二:模块定义
在模块中,上下文提供require方法来引入外部模块,对应引入的功能,并引入了exports对象用于导出当前模块的方法或者变量。并且在模块中,还存在一个module对象,它代表模块自身,而exports是module的属性。在node中,一个文件就是一个模块,将方法挂在在module.exports上即为导出


class method{
    testMethod(index){
        index++;
        return index;
    }
}

module.exports = new method();

在另一个文件中,再通过require方法引入该模块,就能够调用其定义的属性和方法了

const methods = require('./test');
console.log(methods.testMethod(99));
//返回结果为 100

其三:模块标识
模块标识其实就是传递给require方法的参数,它必须是小驼峰命名的字符串,或者以. …开头的相对路径,或者是绝对路径,也可以没有后缀名.js。

所以,CommonJS构建的这一套模块的导入导出使得用户完全不用考虑像global那样的变量污染的情况。‘

global
它和它所属的所有属性在任何程序中都能访问,

在test.js中创建全局属性name=‘zzw’


class method{
    testMethod(index){
        global.name = 'zzw';
        index++;
        return index;
    }
}

module.exports = new method();

再到另外一个文件中引入该模块

const methods = require('./test');
console.log(methods.testMethod(99),name);
//所得结果:100,zzw

node的模块实现:
node在引入模块中,需要经历一下三个步骤:
1:路径分析
2:文件定位
3:编译执行

在node中,模块分为两类,一类是node提供的模块,称为核心模块(path,fs,util)。另一类是用户编写的模块,称为文件模块。
在node进程启动时,核心模块会被直接加载进内存中,所以在核心模块引入时,文件定位和编译执行这两个步骤会被忽略,并且在路径分析中优先判断,所以它的加载速度是最快的。

文件模块则是在运行时加载的,需要完整的路径分析,文件定位,编译执行过程,速度要比核心模块慢,并且与前端不用的是,前端浏览器会缓存静态脚本文件提高性能,而node对引入过的模块都会进行缓存,以减少二次引入时的开销,无论是核心模块还是文件模块,require方法都会对相同的模块都会采用缓存优先的方式。
假设我们自定义了一个文件模块,test.js

console.log('模块加载了');
class method{
    testMethod(index){
        global.name = 'zzw';
        index++;
        return index;
    }
}

module.exports = new method();

然后在另一个模块中引入该模块:

const methods = require('./test');
const methods2 = require('./test');
const methods3 = require('./test');

通常来讲,它会打印三次’模块加载了‘这句话,但运行后只出现了一句,
然后再尝试在一个node项目中在不同的文件中引入多个相同的模块,然后npm start启动node服务

//user.js
const models = require('../../../data/model/index');
const core = require('../../../core');
const test1 = require('./test');
const test2 = require('./test');
const test3 = require('./test');
//test2.js
const methods = require('./test');
const methods2 = require('./test');
const methods3 = require('./test');

同样也只打印了一句console

在这里插入图片描述

1:路径分析
由于标识符的形式有多种,所以对不同的标识符,模块的查找和定位都会有不同的差异,但主要分为一下几大类:
核心模块:如fs,http,util,path等
.或…开始的相对路径文件模块
以/开始的绝对路径文件模块

核心模块的优先级高于文件模块,如果加载一个标识符和模块模块相同的自定义模块,会失败,想要加载成功,必须更换标识符,并且在node中,以.或者…和/开始的标识符都会被当做文件模块来处理,在分析模块时,require方法会将路径转换为真实路径,并以真实路径作为索引,将编译执行后的结果存放在缓存中,使二次加载时性能得以提升。

2:文件定位
require方法在分析标识符的过程中,会出现标识符中不包含文件扩展名的情况,而且在CommonJS中也允许在标识符中不包含文件的扩展名,所以在这种情况下,node会按照.js,.json,.node的顺序补全扩展名。
例如:
创建两个文件,zzw.js和zzw.json

//zzw.js
console.log('我是js文件');
//zzw.json
{
title:"我是json文件"
}```

//在该文件中引入不带后缀名的同名文件
const file = require('zzw');
console.log(file);
//得到结果是:我是js文件

而且如果带上后缀名,可以加快速度,
例如:

console.time('json文件');
const method = require('./zzw');
console.timeEnd('json文件');

console.time('json文件');
const method2 = require('./zzw.json');
console.timeEnd('json文件');



console.time('js文件');
const method3 = require('./tag');
console.timeEnd('js文件');

console.time('js文件');
const method4 = require('./tag.js');
console.timeEnd('js文件');

//使用console计时器所得结果:
json文件: 0.585ms
json文件: 0.078ms
js文件: 663.707ms
js文件: 0.026ms

3:模块编译

在node中,每一个模块都是一个对象
编译和执行是引入文件模块的最后一个阶段,定位到具体的文件后,Node会新建一个模块对象,然后根据路径加载并编译,对于不同的文件扩展名,载入的方法也不同
.js文件:通过fs模块同步读取文件后编译执行
.json文件:也是通过fs模块同步读取后,再使用JSON.parse()解析返回结果
.node文件:使用C++编写的扩展文件,通过dlopen()方法加载最后编译生成的文件
其他文件:都会被当作.js文件载入

并且每一个编译成功的模块都会将其文件路径作为索引缓存再Module._cache对象,用于提高二次引入的性能;
根据不同的文件扩展名,Node会调用不同的读取方式,如.json文件:

Module._extensions['.json'] = function(module,filename){
	var conent = NativeModule.require('fs').readFileSync(filename,'utf-8');
	try{
	module.exports = JSON.parse(stripBOM(content));
	}catch(err){
		err.message = filename + ': '+err.message;
		throw err;
}
}

其中Module._extensions会被赋值给require的extensions属性,在程序中访问require.extensions可以知道系统中已经存在的扩展加载方式:

console.log(require.extendsions);;
//执行结果:{'.js':[Function],'.json':[Function],'.node':[Function]}

猜你喜欢

转载自blog.csdn.net/qq_42427109/article/details/88383123