Node对CommonJS的模块规范


Posted in Javascript onNovember 06, 2019

Node能够以一种相对程度的的姿态出现,离不开CommonJS规范的影响。Node借鉴CommonJS的Modules规范实现了一套非常易用的模块系统,NPM对packages规范的完好支持使得Node应用在开发过程中事半功倍。

在Node中引用模块,需要经历如下三个步骤。

1. 路径分析

Node中的模块分为核心模块和文件模块 。

核心模块是由Node提供的模块,它们在Node源代码的编译过程中就编译进了二进制执行文件,在Node进程启动时,核心模块就被直接加载进内存中,所以在引用核心模块时,文件定位和编译执行这两个步骤可以省略,并且在路径分析中优先判断,所以它的加载速度时最快的。通过require引用核心模块时,直接引用即可。如 require('http')

文件模块是用户编写的模块,它是在运行时动态加载的,需要完整的路径分析,文件定位,编译执行的过程,所以它的速度比核心模块慢。引用文件模块的方式分为三种:

1.以.或..开始的相对路径文件模块。

2.以/开始的绝对路径文件模块。

3.非路径形式的文件模块(自定义模块)。

1,2两种方法用于引用用户自己编写的模块,require会将路径转为真实路径,并以真实路径作为索引,将编译执行的结果(对象)存放在缓存中,由于指定了明确的文件位置,其加载速度慢于核心模块,快于自定义模块。第3中方式用于引用下载的第三方模块,这类模块的查找是最费时的。这里有一个 模块路径 的概念。自定义模块的查找速度慢的原因就在于此。

/**
   通过以下代码,可以看出模块路径的生成规则如下:当前目录下的node_modules目录,父目录下的node_modules目录,沿路径向上逐级递归,直到根目录下的node_modules目录。
 */
 //a.js
 console.log(module.paths) 
 //将打印出如下结果
 [ 'H:\\Files\\qiuzhao\\please-offer\\node_modules',
  'H:\\Files\\qiuzhao\\node_modules',
  'H:\\Files\\node_modules',
 'H:\\node_modules' 
 ]

1. require('../a.js')
2. require('/a.js')
3. require('koa')

2. 文件定位

1) 文件扩展名:CommonJS规范允许在标识符中不包含文件扩展名,这时候Node会按照.js,.json,.node的次序补足扩展名,依次尝试。

2)目录分析和包(自定义模块):在分析提供给require的标识符的过程中,在文件扩展名的依次尝试后,依然没有得到对应的文件,却得到一个目录,这在引用自定义模块并沿着模块路径逐个进行查找时经常会出现,此时Node会将目录当做一个包来处理。这种情况下,Node首先会在当前目录下查找package.json(包描述文件),通过JSON.parse()解析出对象后,从中取出main属性指定的文件名进行定位,视情况而定会j扩展名的分析。如果main属性指定的文件名错误或者根本就没有package.json文件,Node会将index当做默认文件名,然后进行扩展名的依次尝试。如果在目录分析的过程中没有成功定位到任何文件,则进入模块路径的下一个路径进行查找,如果模块路径数组遍历完毕仍未找到文件,则抛出错误。

3. 编译执行

在Node中,每个文件都是一个模块,每个模块都是一个对象,这个对象的定义如下:

function Module(id,parent){
    this.id = id
    this.exports = {}
    this.parent = parent
    if(parent&&parent.children){
      parent.push(this)
    }
    this.filename = null
    this.loaded = false
    this.children = []  //当前模块引用的其他模块会存储在这里
  }

在成功定位到文件后,首先Node会 新建一个对象 ,然后会将文件内容载入并编译执行,并 将模块的exports属性返回给调用方 。针对不同扩展名的文件,有不同的载入方法,通过 require.extensions 可以查看系统以及支持的文件加载方式。

1).js文件:通过fs模块 同步 读取文件后编译执行。

在编译该类型的文件时,Node会对获取得文件内容进行头尾的包装,在头部添加 (function(exports,require,module,__filename,__dirname){\n ,在尾部添加 \n}) 。一个正常的js文件会被包装成如下的样子:

(function(exports,require,module,__dirname,__filename){
   ... 
  })  //从这里可以看出,node对模块的实现,也借鉴了前端js经常使用的利用函数作用域还形成一个独立的空间,以防污染全局作用域,这里node包装了这一过程。

包装之后的代码会通过vm原生模块的runInThisContext()方法执行(类似eval),返回一个具体的function对象(runInThisContext()的作用在这里就是声明一个函数),最后,将当前模块对象(别忘了Node在成功定位到文件后,会首先创建一个module对象)的exports属性,require方法,module本身,以及在之前两步中得到的完整文件路径和文件目录作为参数传递给这个函数。这里有一点经典的例子:

//当我们想为模块的输出定义一个全新的对象时
  
  //error
  exports = {}
  
  
  //right
  module.exports = {}
  
  //这样做的原因时,exports和modlue.exports指向的是同一个对象,而exports={}这种方式,不会影响module.exports指向的对象。Node真正返回给调用者的是module.exports
var val = 10
  var chageVal = function(val){
    val = 100
    console.log(val)
  }
  changeVal(val) //100
  console.log(val) //
  
  /----------------------/
  
  var obj = {
    age:12
  }
  
  var changeName = function(obj){
    obj = {
      age:21
    }
    console.log(obj.age)
  }
  
  changeName(obj) //21
  console.log(obj.age) //12
  
  //出现这种现象的原因是,当调用函数时,传入的是变量的副本。

2).node文件:这是使用C/C++编写的扩展文件,通过dlopen()方法加载最后编译执行的结果。dlopen()方法在不同平台下有不同的实现,通过libuv兼容层封装。实际上,.node的模块文件不需要编译,因为它是编写C/C++模块之后编译生成的,这里只有加载和执行的过程,没有编译的过程。在执行的过程中,模块的exports对象与.node模块产生联系,然后返回给调用者。

3).json文件:通过fs模块同步读取文件后,使用JSON.parse()解析后返回结果。 这种类型的文件是三者中编译最简单的,Node利用fs模块同步读取文件内容后,调用JSON.parse()将其解析成对象,然后将其赋值给模块对象的exports,以供外部调用。

4).其他扩展名文文件:被当做.js文件进行处理。

模块的缓存

与前端浏览器会缓存静态脚本文件已提高性能一样,Node对引用的模块都会进行缓存,以减少二次引入时的开销,不同之处在于,浏览器缓存的是文件,Node缓存的是编译和执行后的对象。require()方法对相同模块的二次加载一律采用缓存优先的方式,这是第一优先级的。每一个编译成功的模块都会将其文件路径做为索引缓存在Module._cache对象上,Module._cache会被赋值给require()方法的cache属性,所以可以通过require.cache还查看已经缓存的模块。如果不想使用缓存的模块,可以在被引用的模块内添加 delete require.cache[module.filename] 。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
[HTML/CSS/Javascript]WWTJS
Sep 25 Javascript
文本框中,回车键触发事件的js代码[多浏览器兼容]
Jun 07 Javascript
Js如何判断客户端是PC还是手持设备简单分析
Nov 22 Javascript
js模仿jquery的写法示例代码
Jun 16 Javascript
文本框(input)获取焦点(onfocus)时样式改变的示例代码
Jan 10 Javascript
Javascript中this的用法详解
Sep 22 Javascript
一个JavaScript函数把URL参数解析成Json对象
Sep 24 Javascript
JavaScript获取当前日期是星期几的方法
Apr 06 Javascript
JavaScript仿微信打飞机游戏
Jul 05 Javascript
vue项目tween方法实现返回顶部的示例代码
Mar 02 Javascript
JavaScript中的函数式编程详解
Aug 22 Javascript
JS Canvas接口和动画效果大全
Apr 29 Javascript
VUE 组件转换为微信小程序组件的方法
Nov 06 #Javascript
vuex存储复杂参数(如对象数组等)刷新数据丢失的解决方法
Nov 05 #Javascript
解决vue.js提交数组时出现数组下标的问题
Nov 05 #Javascript
js+html实现点名系统功能
Nov 05 #Javascript
vuex 实现getter值赋值给vue组件里的data示例
Nov 05 #Javascript
在Vue mounted方法中使用data变量详解
Nov 05 #Javascript
解决vue项目F5刷新mounted里的函数不执行问题
Nov 05 #Javascript
You might like
建立文件交换功能的脚本(三)
2006/10/09 PHP
php md5下16位和32位的实现代码
2008/04/09 PHP
获取URL文件名后缀
2013/10/24 PHP
将FCKeditor导入PHP+SMARTY的实现方法
2015/01/15 PHP
PHP获取路径和目录的方法总结【必看篇】
2017/03/04 PHP
PHP实现QQ登录的开原理和实现过程
2018/02/04 PHP
JQuery从头学起第三讲
2010/07/06 Javascript
学习javascript,实现插入排序实现代码
2011/07/31 Javascript
Javascript类定义语法,私有成员、受保护成员、静态成员等介绍
2011/12/08 Javascript
终于解决了IE8不支持数组的indexOf方法
2013/04/03 Javascript
JS 两日期相减,获得天数的小例子(兼容IE,FF)
2013/07/01 Javascript
JQuery获取表格数据示例代码
2014/05/26 Javascript
js中回调函数的学习笔记
2014/07/31 Javascript
JS打开新窗口防止被浏览器阻止的方法
2015/01/03 Javascript
jquery遍历json对象集合详解
2016/05/18 Javascript
JSONP跨域请求实例详解
2016/07/04 Javascript
vue2.0使用swiper组件实现轮播的示例代码
2018/03/03 Javascript
在vue中,v-for的索引index在html中的使用方法
2018/03/06 Javascript
vue+VeeValidate 校验范围实例详解(部分校验,全部校验)
2018/10/19 Javascript
jQuery选择器之基本选择器用法实例分析
2019/02/19 jQuery
[05:17]DOTA2睡衣妹卖萌求签名 CJ第二天全明星影像
2013/07/28 DOTA
[42:50]NB vs VP 2018国际邀请赛小组赛BO2 第二场 8.18
2018/08/19 DOTA
将Django使用的数据库从MySQL迁移到PostgreSQL的教程
2015/04/11 Python
python: line=f.readlines()消除line中\n的方法
2018/03/19 Python
详解flask入门模板引擎
2018/07/18 Python
对TensorFlow的assign赋值用法详解
2018/07/30 Python
Python 正则表达式匹配字符串中的http链接方法
2018/12/25 Python
解决Tensorflow 内存泄露问题
2020/02/05 Python
浅谈keras中Dropout在预测过程中是否仍要起作用
2020/07/09 Python
DBA数据库管理员JAVA程序员架构师必看
2016/02/07 面试题
图书室管理制度
2014/01/19 职场文书
八年级美术教学反思
2014/02/02 职场文书
感恩主题班会教案
2015/08/12 职场文书
互联网的下一个风口:新的独角兽将诞生
2019/08/02 职场文书
如何把新闻人物写得立体、鲜活?
2019/08/14 职场文书
Vue过滤器(filter)实现及应用场景详解
2021/06/15 Vue.js