浅析node.js的模块加载机制
在node.js中,模块使用CommonJS规范,一个文件是一个模块
node.js中的模块可分为三类
- 内部模块-node.js提供的模块如fs,http,path等
- 自定模块-我们自己写的模块
- 第三方模块-通过npm安装的模块
node.js提供了大量的模块供我们使用,比如想解析一个文件的路径,可以使用path模块下的相应方法实现:
constpath=require('path');
//返回目标文件的绝对路径
console.log(path.resolve('./1.txt'));
运行结果:
/Users/cuiyue/workspace/test/1.txt
使用require引入相应的模块,即可使用。
__dirname和__filename
node.js的每个模块都有这两个参数,它们都是一个绝对路径的地址,区别是__filename存放了从根目录到当前文件名的路径,__dirname只存放从根目录到模块的所在目录:
console.log(__dirname); console.log(__filename);
运行结果:
/Users/cuiyue/workspace/test
/Users/cuiyue/workspace/test/module.js
vm模块
vm模块是node.js提供在V8虚拟机中编译和运行的工具,node.js中的模块内部实现就是通过此模块完成。
说说vm的基本用法。
在js环境中有一个eval函数,它可以运行js的代码字符串,比如:
eval('console.log("Hellojavascript.")');//输出Hellojavascript.
可以看到,eval函数的参数是一段字符串,它可以运行字符串形式的js代码,但它可以使用上下文环境中的变量:
varnum=100;
eval('console.log(num)');//输出100
以上是可以正确访问num的值。
vm模块提供了方法创建一个安全的沙箱,在指定的上下文环境中运行代码,不受外界干扰。
constvm=require('vm');
varnum=100;
vm.runInThisContext('console.log(num)');
运行结果:
console.log(num)
^
ReferenceError:numisnotdefined
可以看到代码报错了,说明在vm创建了指定的上下文环境中,拿不到外界的参量。
CommonJS规范
在以前,由于javascript的历史原因导致它的模块机制很差,由于这些缺点使得javascript不太善于开发大型应用,于是提出了CommonJS规范以弥补javascript的不足。
CommonJS规范主要分为三块内容:模块导入导出、模块定义、模块标识。
模块导入导出
CommonJS中使用require()函数进行模块的引入。
constmymodule=require('mymodule');
使用exports导出模块
module.exports={
name:'Tom'
};
引用的名称可以不带路径,若不带路径表示引入的是node提供的模块或是npm安装的第三方模块(node_modules)
模块定义
module对象:在每一个模块中,module对象代表该模块自身。
export属性:module对象的一个属性,它向外提供接口。
模块标识
模块标识指的是传递给require方法的参数,必须是符合小驼峰命名的字符串,或者以.、..、开头的相对路径,或者绝对路径。
node中模块解析流程
- 首先接收参数,把传入的模块名称解析成绝对路径
- 若没有后缀名称,依次拼接.js.json.node尝试加载,仍到不到模块则报错
- 取得正确的路径后判断缓存中是否存在此模块,若有则取出
- 若缓存中不存在则加载此文件,在外包裹一层闭包并执行它
以上为大致流程,下面尝试着写一下模块。
代码的基本结构:
/**
*Module类,用于处理模块加载
*/
functionModule(){}
//模块的缓存
Module._cacheModule={};
//不同扩展名的加载策略
Module._extensions={};
//根据moduleId解析绝对路径,
Module._resolveFileName=function(moduleId){};
//入口函数
functionreq(moduleId){}
附上全部代码:
constpath=require('path');
constfs=require('fs');
constvm=require('vm');
/**
*Module类,用于处理模块加载
*/
functionModule(file){
this.id=file;//当前模块的id,它使用完整的绝对路径标识,因此是唯一的
this.exports={};//导出
this.loaded=false;//模块是否已加载完毕
}
//模块的缓存
Module._cacheModule={};
Module._wrapper=['(function(exports,require,module,__dirname,__filename){','});'];
//不同扩展名的加载策略
Module._extensions={
'.js':function(currentModule){
letjs=fs.readFileSync(currentModule.id,'utf8');//读取出js文件内容
letfn=Module._wrapper[0]+js+Module._wrapper[1];
vm.runInThisContext(fn).call(
currentModule.exports,
currentModule.exports,
req,
currentModule,
path.dirname(currentModule.id),
currentModule.id);
returncurrentModule.exports;
},
'.json':function(currentModule){
letjson=fs.readFileSync(currentModule.id,'utf8');
returnJSON.parse(json);//转换为JSON对象返回
},
'.node':''
};
//加载模块(实例方法)
Module.prototype.load=function(file){
letextname=path.extname(file);//获取后缀名
returnModule._extensions[extname](this);
};
//根据moduleId解析绝对路径,
Module._resolveFileName=function(moduleId){
letp=path.resolve(moduleId);
if(!path.extname(moduleId)){//传入的模块没有后缀
letarr=Object.keys(Module._extensions);
//循环读取不同扩展名的文件
for(vari=0;i
a.js的文件内容:
module.exports=function(){
console.log('Thismessagefroma.js');
console.log(__dirname);
console.log(__filename);
}
最终运行结果:
Thismessagefroma.js
/Users/cuiyue/workspace/test
/Users/cuiyue/workspace/test/a.js
重要代码说明
_resolveFileName
_resolveFileName方法的主要作用是把传入的模块解析成绝对路径,这样才可以进行下一步,根据完整的路径加载模块。
因此要进行判断,如果传入的模块不存在,则要报错;如果传入的模块已经有扩展名了,就不要拼接了;若没有扩展名,依次以.js.json.node的顺序拼接成完成的模块进行加载。
_extensions
此对象中封装了加载不同类型模块的处理方法,其中若是.json类型则使用fs读取文件直接转换成JSON对象并返回。
若是.js文件则读取后,拼接闭包,将exports,require,module,__dirname,__filename五大参数拼接好,使用vm模块的沙箱机制运行,得到的结果放入module.exports返回。
总结
以上就是node.js的模块加载的简单逻辑,实际上node.js的源码远远比上面的代码复杂,光是处理模块路径、判断合法等操作就写了N行。而且我这里没有写缓存以及其它的复杂逻辑,但核心差不多就是这些,核心的核心就是用fs.readFileSync读取js文件,把内容拼接到一个大大的闭包中,这也解释了为什么我们自己写的所有node模块中都会有require方法,exports导出,以及__dirname和__filename参数。
了解了node.js的模块加载逻辑,在以后写node.js就更可避免一些误解,写出精细的代码。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。