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 相关文章推荐
URL编码转换,escape() encodeURI() encodeURIComponent()
Dec 27 Javascript
jQuery对象与DOM对象之间的转换方法
Apr 15 Javascript
使用JavaScript修改浏览器URL地址栏的实现代码
Oct 21 Javascript
JS如何判断移动端访问设备并解析对应CSS
Nov 27 Javascript
JavaScript基础知识及常用方法总结
Jan 10 Javascript
Node.js Addons翻译(C/C++扩展)
Jun 12 Javascript
值得收藏的八个常用的js正则表达式
Oct 19 Javascript
微信小程序tabbar底部导航
Nov 05 Javascript
如何为vue的项目添加单元测试
Dec 19 Javascript
vue-drag-chart 拖动/缩放图表组件的实例代码
Apr 10 Javascript
详解Vue3中对VDOM的改进
Apr 23 Javascript
ES6新增的数组知识实例小结
May 23 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
php获取ip的三个属性区别介绍(HTTP_X_FORWARDED_FOR,HTTP_VIA,REMOTE_ADDR)
2012/09/23 PHP
Php无限级栏目分类读取的实现代码
2014/02/19 PHP
php实现简单的语法高亮函数实例分析
2015/04/27 PHP
php使用Session和文件统计在线人数
2015/07/04 PHP
PHP检测数据类型的几种方法(总结)
2017/03/04 PHP
用js判断用户浏览器是否是XP SP2的IE6
2007/03/08 Javascript
JQuery Ajax通过Handler访问外部XML数据的代码
2010/06/01 Javascript
仿淘宝TAB切换搜索框搜索切换的相关内容
2014/09/21 Javascript
jQuery左右滚动支持图片放大缩略图图片轮播代码分享
2015/08/26 Javascript
JS实现横向与竖向两个选项卡Tab联动的方法
2015/09/27 Javascript
jquery验证手机号是否正确实例讲解
2015/11/17 Javascript
Vue.js第三天学习笔记(计算属性computed)
2016/12/01 Javascript
jQuery实现模拟flash头像裁切上传功能示例
2016/12/11 Javascript
jQuery实现鼠标悬停3d菜单展开动画效果
2017/01/19 Javascript
详解windows下vue-cli及webpack 构建网站(二)导入bootstrap样式
2017/06/17 Javascript
Vue cli+mui 区域滚动的实例代码
2018/01/25 Javascript
使用node.js实现微信小程序实时聊天功能
2018/08/13 Javascript
在vue中使用console.log无效的解决
2020/08/09 Javascript
在vue中封装方法以及多处引用该方法详解
2020/08/14 Javascript
微信小程序基于ColorUI构建皮皮虾短视频去水印组件
2020/11/04 Javascript
Python最长公共子串算法实例
2015/03/07 Python
Python中的super用法详解
2015/05/28 Python
python验证码识别教程之利用滴水算法分割图片
2018/06/05 Python
Python使用pymongo模块操作MongoDB的方法示例
2018/07/20 Python
pyinstaller打包单个exe后无法执行错误的解决方法
2019/06/21 Python
Python Gluon参数和模块命名操作教程
2019/12/18 Python
导入tensorflow:ImportError: libcublas.so.9.0 报错
2020/01/06 Python
深入浅析python变量加逗号,的含义
2020/02/22 Python
python实现单机五子棋
2020/08/28 Python
Python 图片处理库exifread详解
2021/02/25 Python
公司同意接收函
2014/01/13 职场文书
学校四风对照检查材料
2014/08/28 职场文书
爱牙日宣传活动总结
2015/02/05 职场文书
圆明园观后感
2015/06/03 职场文书
小学一年级班主任工作经验交流材料
2015/11/02 职场文书
Pytorch 使用tensor特定条件判断索引
2021/04/08 Python