Node.js模块化机制道理探讨

媒介

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

《Node.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,运转结果:

《Node.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;
};

对应流程以下图所示:
《Node.js模块化机制道理探讨》

2、途径剖析和文件定位

途径剖析

模块标识符剖析:
(1)中心模块,如http、fs、path
(2)以...最先的相对途径文件模块
(3)以/最先的绝对途径文件模块
(4)非途径情势的文件模块

1)中心模块:优先级仅次于缓存,加载速率最快;假如自定义模块与中心模块称号雷同,加载是不会胜利的。若想加载胜利,必需挑选一个差别的称号或许换用途径。

2)途径情势的文件模块:以. || .. || /最先的标识符,都邑被当作文件模块来处置惩罚。在加载的历程当中,require要领会将途径转换为实在的途径,加载速率仅次于中心模块

3) 非途径情势的自定义模块:这是一种特别的文件模块,多是一个文件或许包的情势。查找这类模块的战略类似于JS中作用域链,Node会逐一尝试模块途径中的途径,直到找到目的文件为止。

模块途径: 这是Node在定位文件模块的详细文件时指定的查找战略,详细表现为一个途径构成的数组。

能够在REPL环境中输出Module对象,检察其path属性的体式格局检察上述数组

《Node.js模块化机制道理探讨》

文件定位

  • 文件扩展名剖析

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对象

《Node.js模块化机制道理探讨》

  • 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进来须要的模块就可以够了。

参考链接

  1. 经由历程源码剖析 Node.js 中一个文件被 require 后所发作的故事

  2. 阮一峰–CommonJS范例

  3. Nodejs源码–GitHub

    原文作者:Aphasia
    原文地址: https://segmentfault.com/a/1190000008371376
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞