NodeJS学习笔记之Module的简介
Node.js模块系统
Node.js有一个简单的模块加载系统。在Node.js中,文件和模块是一一对应的(每个文件被视为单独的模块)。
例如,考虑下面这个名为foo.js的文件:
constcircle=require('./circle.js'); console.log(`Theareaofacircleofradius4is${circle.area(4)}`);
在第一行,foo.js加载与foo.js同一目录的模块circle.js。
circle.js的内容如下:
constPI=Math.PI; exports.area=(r)=>PI*r*r; exports.circumference=(r)=>2*PI*r;
模块circle.js导出了函数area()和circumference()。要将函数和对象添加到模块的根目录,可以将它们赋值到特殊exports对象上。
模块内部的变量一定是私有的,因为模块被Node.js包裹在一个函数中(参见下面的模块包装器)。在这个例子中,变量PI对于circle.js来说是私有变量。
如果你希望模块导出的是一个函数(如构造函数),或者是要导出完整的对象,而不是一次创建一个属性,则需要将其分配给module.exports而不是exports。
在下面的bar.js中,使用了square模块,它导出一个构造函数:
constsquare=require('./square.js'); varmySquare=square(2); console.log(`Theareaofmysquareis${mySquare.area()}`);
在square.js模块中定义一个square方法:
module.exports=(width)=>{ return{ area:()=>width*width; }; }
此外,模块系统在require(“module”)模块中实现。
『main』模块
当某个module直接从Node.js运行时,它会将require.main设置该module。你可以通过这个来测试这个module是被直接运行的还是被require的。
require.main===module
就拿文件foo.js来说,如果运行nodefoo.js这个属性就是true。运行require('./foo.js')就是false。
因为module提供了一个filename(通常相当于__filename),因此可以通过检查require.main.filename来获取当前应用程序的入口点。
包管理器的一些提示
Node.js的require()函数支持一些合理的目录结构。它让软件包管理器程序(如dpkg,rpm和npm)可以从Node.js模块中直接去构建本地的包而不需要修改。
下面我们给出一个可以正常工作的建议目录结构:
假设我们希望在/usr/lib/node/
此外,包还可以相互依赖。比如你想安装foo包,而这个包有可能需要安装指定版本的bar包。而bar包也很有可能依赖其他的包,并且在某些特殊情况下,这些依赖包甚至可能会产生循环依赖。
由于Node.js会查找加载的所有模块的realpath(即解析软链),然后再去node_modules文件夹中查找依赖的包,因此使用以下方案可以非常简单地解决此问题:
/usr/lib/node/foo/1.2.3/-包含foo包,版本是1.2.3
/usr/lib/node/bar/4.3.2/-包含foo所依赖的bar包
/usr/lib/node/foo/1.2.3/node_modules/bar-软链到/usr/lib/node/bar/4.3.2/
/usr/lib/node/bar/4.3.2/node_modules/*-软链到bar的依赖
因此,即使遇到循环依赖,或者是依赖冲突,每个模块都能加载到并使用自己所依赖指定版本的包。
当foo包中require('bar')时,它就可以软链到指定版本的/usr/lib/node/foo/1.2.3/node_modules/bar。然后,当bar包中的代码调用require('quux')时,它同样也可以软链到指定版本的/usr/lib/node/bar/4.3.2/node_modules/quux。
模块加载的全过程(重点,下面写的伪代码流程一定要记住)
要获取在调用require()将被加载的确切文件名,请使用require.resolve()函数。
以下是模块加载的全过程以及require.resolve的解析过程:
//加载X模块 require(X)frommoduleatpathY 1.IfXisacoremodule. a.returnthecoremodule b.STOP 2.IfXbeginswith'./'or'/'or'../' a.LOAD_AS_FILE(Y+X) b.LOAD_AS_DIRECTORY(Y+X) 3.LOAD_NODE_MODULES(X,dirname(Y)) 4.THROW"notfound" //加载X文件 //加载过程:X->X.js->X.json->X.node LOAD_AS_FILE(X) 1.If[X]isafile,load[X]asJavaScripttext.STOP 2.If[X.js]isafile,load[X.js]asJavaScripttext.STOP 3.If[X.json]isafile,load[X.json]asJavaScripttext.STOP 4.If[X.node]isafile,load[X.node]asJavaScripttext.STOP //加载入口文件 //加载过程:X->X/index.js->X/index.json->X/index.node LOAD_INDEX(X) 1.If[X/index.js]isafile,load[X/index.js]asJavaScripttext.STOP 2.If[X/index.json]isafile,load[X/index.json]asJavaScripttext.STOP 3.If[X/index.node]ifafile,load[X/index.node]asJavaScripttext.STOP //加载文件夹 LOAD_AS_DIRECTORY(X) 1.If[X/package.json]isafile. a.Parse[X/package.json],andlookfor"main"field b.letM=X+(jsonmainfield) c.LOAD_AS_FILE(M) d.LOAD_INDEX(M) 2.LOAD_INDEX(X) //加载node模块 LOAD_NODE_MODULES(X,START) 1.letDIRS=NODE_MODULES_PATHS(START) 2.foreachDIRinDIRS; a.LOAD_AS_FILE(DIR/X) b.LOAD_AS_DIRECTORY(DIR/X) //列出所有可能的node_modules路径 NODE_MODULES_PATHS(START) 1.letPARTS=pathsplit(START); 2.letI=countofPARTS-1 3.letDIRS=[] 4.whileI>0 a.IfPARTS[I]="node_modules"CONTINUE b.DIR=pathjoin(PARTS[0...I]+"node_modules") c.DIRS=DIRS+DIR d.letI=I-1 5.returnDIRS
模块缓存
所有的模块都会在第一次加载之后被缓存起来。这意味着你每次调用require('foo')将得到完全相同的对象。
对require('foo')的多次调用可能并不会多次执行该模块的代码。这是一个重要的功能。使用它,可以返回“partiallydone”对象,从而允许根据依赖关系一层一层地加载模块,即使这样做可能会导致循环依赖。
如果要让某个模块在每次被加载时都去执行代码,则需要exports一个函数,并调用该函数即可。
模块缓存注意事项
模块是基于其解析出来的文件名进行缓存。根据调用模块的路径,被调用的模块可能会解析出不同的文件名(从node_modules文件夹加载)。如果解析出来的是不同的文件,它不保证每次require('foo')总是返回相同的对象。
另外,在不区分大小写的文件系统或操作系统上,不同的解析文件名可以指向相同的文件,但缓存仍将它们视为不同的模块,并将重新加载该文件多次。例如,require('./foo')和require('./FOO')返回两个不同的对象,而不管./foo和./FOO是否是同一个文件。
核心模块
Node.js有些模块被编译成二进制文件。本文档中的其他部分将对这些模块进行更详细的描述。
核心模块在Node.js的源码lib/文件夹中。
如果核心模块的模块标识传递给require(),则它们总是优先加载。例如,即使有一个自定义模块叫http,我们去执行require('http')也将始终返回内置的HTTP模块,
循环引用
当循环引用require()时,返回模块可能并没有执行完成。
考虑这种情况:
a.js:
console.log('astarting'); exports.done=false; constb=require('./b.js'); console.log('ina,b.done=%j',b.done); exports.done=true; console.log('adone');
b.js:
console.log('bstarting'); exports.done=false; consta=require('./a.js'); console.log('inb,a.done=%j',a.done); exports.done=true; console.log('bdone');
app.js:
console.log('mainstarting'); consta=require('./a.js'); constb=require('./b.js'); console.log('inmain,a.done=%j,b.done=%j',a.done,b.done);
当app.js加载a.js时,a.js依次加载b.js.此时,b.js尝试加载a.js.为了防止无限循环,将a.js导出对象的未完成副本返回到b.js模块。b.js然后完成加载,并将其导出对象提供给a.js模块。
当app.js加载了这两个模块时,它们都已经完成。因此,该程序的输出将是:
$nodeapp.js mainstarting astarting bstarting inb,a.done=false bdone ina,b.done=true inmain,a.done=true,b.done=true
模块包装器
在执行模块的代码之前,Node.js将使用一个函数包装器来将模块内容包裹起来,如下所示:
(function(exports,require,module,__filename,__dirname){ //你的模块代码 });
通过这样做,Node.js实现了以下几点:
它将模块内部的顶级变量(定义为var,const或let)的作用域范围限定为模块内部而不是全局。
它有助于给模块内部提供一些实际上只属于该模块的全局变量,例如:
module和exports对象用来帮助从模块内部导出一些值
变量__filename和__dirname是当前模块最终解析出来的文件名和文件夹路径
module对象签名
Objectmodule{ id:String,//模块标识,为该模块文件在系统中的绝对路径 exports:Object,//该模块的导出对象 parent:Object|undefined,//引用该模块的父模块 filename:String|null,//最终解析的文件名称,与__filename相同。 loaded:Boolean,//该模块是否已经加载 children:Array,//改模块的引用列表 paths:Array//模块加载路径 }
require函数签名
Functionrequire{ [Function],//函数体 resolve:Function,//根据模块标识解析模块,返回绝对路径 main:undefined|Object,//应用的主(main)模块 extensions:{'.js':Function,'.json':Function,'.node':Function}, cache:Object//模块缓存,以模块的绝对路径为key }