首页 > 编程语言 >Node.js 模块化机制原理探究

Node.js 模块化机制原理探究

时间:2023-07-01 14:04:04浏览次数:42  
标签:Node exports 缓存 模块化 require module js 模块 filename

前言

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 模块化机制原理探究_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.sayHi = 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 模块化机制原理探究_前端开发_02

虽然我们两次引入 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 模块化机制原理探究_开源项目_03

2、路径分析和文件定位

路径分析

模块标识符分析:

  1. 核心模块,如 http、fs、path
  2. 以 . 或 .. 开始的相对路径文件模块
  3. 以 / 开始的绝对路径文件模块
  4. 非路径形式的文件模块
  • 核心模块:优先级仅次于缓存,加载速度最快;如果自定义模块与核心模块名称相同,加载是不会成功的。若想加载成功,必须选择一个不同的名称或者换用路径。
  • 路径形式的文件模块:以 . || .. || / 开始的标识符,都会被当做文件模块来处理。在加载的过程中,require 方法会将路径转换为真实的路径,加载速度仅次于核心模块
  • 非路径形式的自定义模块:这是一种特殊的文件模块,可能是一个文件或者包的形式。查找这类模块的策略类似于JS中作用域链,Node会逐个尝试模块路径中的路径,直到找到目标文件为止。

模块路径: 这是 Node 在定位文件模块的具体文件时指定的查找策略,具体表现为一个路径组成的数组。

可以在REPL环境中输出 Module 对象,查看其 path 属性的方式查看上述数组


Node.js 模块化机制原理探究_前端开发_04

文件定位:

文件扩展名分析

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 模块化机制原理探究_uniapp源码_05

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

学习更多node.js知识请关注CRMEB。

标签:Node,exports,缓存,模块化,require,module,js,模块,filename
From: https://blog.51cto.com/u_15723831/6598994

相关文章

  • python -- json与dict
    一、python中的dict 与json1、dict 的表现形式    dict中的key和value,不论写的是 双引号 还是 单引号,最后 python的dict都会转成 单引号。2、jsonjson是一个字符串,是一种数据交换格式,不同于dict属于python的基础数据类型。js......
  • js 数组和链表分别实现队列
    链表实现/***1.单项链表实现队列,但要同时记录head和tail*2.要从tail入队,head出对,否则出队时,tail不好定位*2.单独记录length,不可遍历链表获取length*/classMyQueue{head=null;//头tail=null;//尾len=0;add(n){letnewNode={......
  • js的一些小技巧
    作用域全局作用域局部作用域(函数里)也称函数作用域块级作用域{}包裹的例如iffor 括号()也算变量全局变量谁都能用,在函数内也可以局部变量,只能在该函数内用,如果这个函数嵌套了子函数,那么父函数定义的变量,子函数也能用,不能颠倒,父亲的钱就是儿子的钱,儿子的钱不是父亲的钱块级变量(){}......
  • JS高级用法:像大神一样玩转JavaScript
    前言众所周知,JavaScript是一种非常流行的编程语言,它已经成为了网页开发的必备技能。但是,在我们从事JavaScript编程的时候,我们却没有完全发掘和利用它的全部潜力。在本文中,我们将分享一些高级的JavaScript技巧,希望帮助掘友们更好地理解和掌握JavaScript编程。关于JS高级用法在学习Ja......
  • js 链路表和反向链路
     /***数组转链路表数据-{value:A,next:{value:B,next:{value:C}}*@param{*}listarr*@returns*/functioncreateLink(list){letlength=list.length;//最后一级,没有nextletcurrNode={value:list[length-1],};//[1,2,3......
  • Linux索引节点(Inode)用满导致空间不足
    1、问题:在创建新目录和文件是提示“nospaceleftondevice”!按照以前的情况,很有可能是服务器空间又被塞满了,通过命令查看,发现还有剩余。再用df-i查看了一下/分区的索引节点(inode),发现已经用满(已用=100%),导致系统无法创建新目录和文件。2、问题原因分析:Inode译成中文就是索引节......
  • js基础速成
    js记录js中对象类型object创建对象vartest=newobject();直接添加属性test.name='熊大'没有属性,系统不会报错,会返回undefined删除属性deletetest.name创建对象varobj={}或者newobjectjs中函数也是一个对象varfun=newfunction()常规写法functionfun(......
  • [转]前台传递给后台的JSON字符串中的引号 “” 在JAVA后台被转义为 &quot
    1、问题:前台数据,JSON字符串带有引号“”,数据被传递到后台,引号被转义为&quot,后台无法解析。前台数据如下:正常后台数据如下:大部分正常,只有JSON字符串中的“”被转义为&quot2、解决:方法一:使用apache的lang包里的方法StringappJson=StringEscapeUtils.un......
  • JSR107
    JSR107JSR是JavaSpecificationRequests的缩写,Java规范请求,故名思议提交Java规范,JSR-107呢,就是关于如何使用缓存的规范,是java提供的一个接口规范,类似于JDBC规范,没有具体的实现,具体的实现就是ehcache等这些缓存解决方案。  JSR107核心接口JavaCaching(JSR-1......
  • 原生JS滚动条触底
    /console.log("UL的高度",main.clientHeight);//console.log("所有的LI的高度",main.scrollHeight);//console.log("可以滚动的距离",main.scrollHeight-main.clientHeight);//console.log("main.scrollTop",main.scrollTop);if(main.scr......