媒介
Node应用是由模块构成的,Node遵照了CommonJS的模块范例,来断绝每一个模块的作用域,使每一个模块在它本身的定名空间中实行。
CommonJS范例的主要内容:
模块必需经由历程 module.exports 导出对外的变量或接口,经由历程 require() 来导入其他模块的输出到当前模块作用域中。
CommonJS模块的特性:
(1)一切代码运转在当前模块作用域中,不会污染全局作用域
(2)模块同步加载,依据代码中涌现的递次顺次加载
(3)模块能够屡次加载,然则只会在第一次加载时运转一次,然后运转结果就被缓存了,今后再加载,就直接读取缓存结果。要想让模块再次运转,必需消灭缓存。
一个简朴的例子:
demo.js
module.exports.name = 'Aphasia';
module.exports.getAge = function(age){
console.log(age)
};
//须要引入demo.js的其他文件
var person = require('./demo.js')
module对象
依据CommonJS范例,每一个文件就是一个模块,在每一个模块中,都邑有一个module对象,这个对象就指向当前的模块。module对象具有以下属性:
(1)id:当前模块的bi
(2)exports:示意当前模块暴露给外部的值
(3)parent: 是一个对象,示意挪用当前模块的模块
(4)children:是一个对象,示意当前模块挪用的模块
(5)filename:模块的绝对途径
(6)paths:从当前文件目次最先查找node_modules目次;然后顺次进入父目次,查找父目次下的node_modules目次;顺次迭代,直到根目次下的node_modules目次
(7)loaded:一个布尔值,示意当前模块是不是已被完整加载
示例:
module.js
module.exports = {
name: 'Aphasia',
getAge: function(age){
console.log(age)
}
}
console.log(module)
实行node module.js
1、module.exports
从上面的例子我们也能看到,module对象具有一个exports属性,该属性就是用来对外暴露变量、要领或全部模块的。当其他的文件require进来该模块的时刻,实际上就是读取了该模块module对象的exports属性。
简朴的运用示例
module.exports = 'Aphasia';
module.exports.name = 'Aphasia';
module.exports = function(){
//dosomething
}
module.exports = {
name: 'Aphasia',
getAge: function(){
//dosomething
}
}
2、exports对象
一最先我很忧郁,既然module.exports就可以满足一切的需求,为何另有个exports对象呢?实在,两者之间有下面的关联
(1)起首,exports和module.exports都是援用范例的变量,而且这两个对象指向统一块内存地址。在node中,两者一最先都是指向一个空对象的
exports = module.exports = {};
能够在REPL环境中直接运转下面代码module.exports
,结果会输出一个{}
(2)其次,exports对象是经由历程形参的体式格局传入的,直接赋值形参会转变形参的援用,然则并不能转变作用域外的值。这句话是什么意义呢?我们举个例子。
var module = {
exports: {}
}
var exports = module.exports
function change(exports) {
//为形参增加属性,是会同步到外部的module.exports对象的
exports.name = "Aphasia"
//在这里修改了exports的援用,并不会影响到module.exports
exports = {
age: 24
}
console.log(exports) //{ age: 24 }
}
change(exports)
console.log(module.exports) //{exports: {name: "Aphasia"}}
如今邃晓了吧?实在我们在模块中像下面的代码那样,直接给exports赋值,会转变当前模块内部的形参exports对象的援用,也就是说当前的exports已跟外部的module.exports对象没有任何关联了,所以这个转变是不会影响到module.exports的。因而,下面的这类体式格局是没有任何结果的,一切的属性和要领都不会被抛出。
//以下操纵都是不起作用的
exports = 'Aphasia';
exports = function(){
console.log('Aphasia')
}
实在module.exports就是为了处置惩罚上述exports直接赋值,会致使抛出不胜利的题目而发作的。有了它,我们就可以够如许来抛出一个模块了。
//这些操纵都是正当的
exports.name = 'Aphasia';
exports.getName = function(){
console.log('Aphasia')
}
//相当于下面的体式格局
module.exports = {
name: 'Aphasia',
getName: function(){
console.log('Aphasia')
}
}
如许就不必每次把要抛出的对象或要领赋值给exports的属性了 ,直接采纳对象字面量的体式格局越发轻易。
模块实例的require要领
我们都晓得,当运用exports或许module.exports抛出一个模块,经由历程给require()要领传入模块标识符参数,然后node依据肯定的划定规矩引入该模块以后,我们就可以运用模块中定义的要领和属性了。这里要讲的就是node的模块引入划定规矩。
1、node中引入模块的机制
在Node中引入模块,须要阅历3个步骤
(1)途径剖析
(2)文件定位
(3)编译实行
在Node中,模块平常分为两种
(1)Node供应的模块,比方http、fs等,称为中心模块。中心模块在node源代码编译的历程当中就编译进了二进制实行文件,在Node历程启动的时刻,部份中心模块就直接加载进内存中了,因而这部份模块是不必阅历上述的(2)(3)两个步骤的,而且在途径剖析中是优先推断的,因而加载速率最快。
(2)用户本身编写的模块,称为文件模块。文件模块是按需加载的,须要阅历上述的三个步骤,速率较慢。
优先从缓存中加载
与浏览器会缓存静态剧本文件以进步页面机能一样,Node对引入过的模块也会举行缓存。差别的处所是,node缓存的是编译实行以后的对象而不是静态文件。这一点我们能够用下面的体式格局来考证。
modA.js
console.log('模块modA最先加载...')
exports = function() {
console.log('Hi')
}
console.log('模块modA加载终了')
init.js
var mod1 = require('./modA')
var mod2 = require('./modA')
console.log(mod1 === mod2)
实行node init.js
,运转结果:
虽然我们两次引入modA这个模块,然则模块中的代码实在只实行了一遍。而且mod1和mod2指向了统一个模块对象。
下面是Module._load的源码:
Module._load = function(request, parent, isMain) {
// 盘算绝对途径
var filename = Module._resolveFilename(request, parent);
// 第一步:假如有缓存,掏出缓存
var cachedModule = Module._cache[filename];
if (cachedModule) {
return cachedModule.exports;
// 第二步:是不是为内置模块
if (NativeModule.exists(filename)) {
return NativeModule.require(filename);
}
// 第三步:天生模块实例,存入缓存
var module = new Module(filename, parent);
Module._cache[filename] = module;
// 第四步:加载模块
try {
module.load(filename);
hadException = false;
} finally {
if (hadException) {
delete Module._cache[filename];
}
}
// 第五步:输出模块的exports属性
return module.exports;
};
对应流程以下图所示:
2、途径剖析和文件定位
途径剖析
模块标识符剖析:
(1)中心模块,如http、fs、path
(2)以.
或..
最先的相对途径文件模块
(3)以/
最先的绝对途径文件模块
(4)非途径情势的文件模块
1)中心模块:优先级仅次于缓存,加载速率最快;假如自定义模块与中心模块称号雷同,加载是不会胜利的。若想加载胜利,必需挑选一个差别的称号或许换用途径。
2)途径情势的文件模块:以.
|| ..
|| /
最先的标识符,都邑被当作文件模块来处置惩罚。在加载的历程当中,require要领会将途径转换为实在的途径,加载速率仅次于中心模块
3) 非途径情势的自定义模块:这是一种特别的文件模块,多是一个文件或许包的情势。查找这类模块的战略类似于JS中作用域链,Node会逐一尝试模块途径中的途径,直到找到目的文件为止。
模块途径: 这是Node在定位文件模块的详细文件时指定的查找战略,详细表现为一个途径构成的数组。
能够在REPL环境中输出Module对象,检察其path属性的体式格局检察上述数组
文件定位:
文件扩展名剖析
require()剖析的标识符能够不包括扩展名,node会按.js、.node、.json的序次补足扩展名,顺次尝试
目的剖析和包
假如在扩展名剖析的步骤中,查找不到文件而是查找到响应目次,此时node会将目次当作包来处置惩罚,举行下一步剖析查找当前目次下package.json中的main属性指定的文件名,若查找不胜利则顺次查找index.js,index.node,index.json。
假如目次剖析的历程当中没有定位到任何文件,则自定义模块会进入下一个模块途径继承查找,直到一切的模块途径都遍历终了,依旧没找到则抛出查找失利的非常。
参考源码
在Module._load要领的内部挪用了Module._findPath这个要领,这个要领是用来返回模块的绝对途径的,源码以下:
Module._findPath = function(request, paths) {
// 列出一切能够的后缀名:.js,.json, .node
var exts = Object.keys(Module._extensions);
// 假如是绝对途径,就不再搜刮
if (request.charAt(0) === '/') {
paths = [''];
}
// 是不是有后缀的目次斜杠
var trailingSlash = (request.slice(-1) === '/');
// 第一步:假如当前途径已在缓存中,就直接返回缓存
var cacheKey = JSON.stringify({request: request, paths: paths});
if (Module._pathCache[cacheKey]) {
return Module._pathCache[cacheKey];
}
// 第二步:顺次遍历一切途径
for (var i = 0, PL = paths.length; i < PL; i++) {
var basePath = path.resolve(paths[i], request);
var filename;
if (!trailingSlash) {
// 第三步:是不是存在该模块文件
filename = tryFile(basePath);
if (!filename && !trailingSlash) {
// 第四步:该模块文件加上后缀名,是不是存在
filename = tryExtensions(basePath, exts);
}
}
// 第五步:目次中是不是存在 package.json
if (!filename) {
filename = tryPackage(basePath, exts);
}
if (!filename) {
// 第六步:是不是存在目次名 + index + 后缀名
filename = tryExtensions(path.resolve(basePath, 'index'), exts);
}
// 第七步:将找到的文件途径存入返回缓存,然后返回
if (filename) {
Module._pathCache[cacheKey] = filename;
return filename;
}
}
// 第八步:没有找到文件,返回false
return false;
};
3、消灭缓存
依据上述的模块引入机制我们晓得,当我们第一次引入一个模块的时刻,require的缓存机制会将我们引入的模块加入到内存中,以提拔二次加载的机能。然则,假如我们修改了被引入模块的代码以后,当再次引入该模块的时刻,就会发明那并非我们最新的代码,这是一个贫苦的事变。怎样处置惩罚呢?
检察require对象
require(): 加载外部模块
require.resolve():将模块名剖析到一个绝对途径
require.main:指向主模块
require.cache:指向一切缓存的模块
require.extensions:依据文件的后缀名,挪用差别的实行函数
处置惩罚要领
//删除指定模块的缓存
delete require.cache[require.resolve('/*被缓存的模块称号*/')]
// 删除一切模块的缓存
Object.keys(require.cache).forEach(function(key) {
delete require.cache[key];
})
然后我们再从新require进来须要的模块就可以够了。