学习Node.js模块机制


Posted in Javascript onOctober 17, 2016

一、CommonJS的模块规范

学习Node.js模块机制

Node与浏览器以及 W3C组织、CommonJS组织、ECMAScript之间的关系

Node借鉴CommonJS的Modules规范实现了一套模块系统,所以先来看看CommonJS的模块规范。

CommonJS对模块的定义十分简单,主要分为模块引用、模块定义和模块标识3个部分。

1. 模块引用

模块引用的示例代码如下:

var math = require('math');

在CommonJS规范中,存在require()方法,这个方法接受模块标识,以此引入一个模块的API到当前上下文中。

2. 模块定义

在模块中,上下文提供require()方法来引入外部模块。对应引入的功能,上下文提供了exports对象用于导出当前模块的方法或者变量,并且它是唯一导出的出口。在模块中,还存在一个module对象,它代表模块自身,而exports是module的属性。在Node中,一个文件就是一个模块,将方法挂载在exports对象上作为属性即可定义导出的方式:

// math.js
exports.add = function () { 
var sum = 0,  i = 0,  args = arguments,  l = args.length; 
while (i < l) {  sum += args[i++]; }
 return sum;
};

在另一个文件中,我们通过require()方法引入模块后,就能调用定义的属性或方法了:

// program.js
var math = require('math');
exports.increment = function (val) { return math.add(val, 1);};

3.模块标识

模块标识其实就是传递给require()方法的参数,它必须是符合小驼峰命名的字符串,或者以.、..开头的相对路径,或者绝对路径。它可以没有文件名后缀.js。模块的定义十分简单,接口也十分简洁。它的意义在于将类聚的方法和变量等限定在私有的作用域中,同时支持引入和导出功能以顺畅地连接上下游依赖。每个模块具有独立的空间,它们互不干扰,在引用时也显得干净利落。

二、Node的模块实现

Node在实现中并非完全按照规范实现,而是对模块规范进行了一定的取舍,同时也增加了少许自身需要的特性。尽管规范中exports、require和module听起来十分简单,但是Node在实现它们的过程中究竟经历了什么,这个过程需要知晓。
在Node中引入模块,需要经历如下3个步骤。

1. 路径分析

2. 文件定位

3. 编译执行

在Node中,模块分为两类:一类是Node提供的模块,称为核心模块;另一类是用户编写的模块,称为文件模块

•  核心模块部分在Node源代码的编译过程中,编译进了二进制执行文件。在Node进程启动时,部分核心模块就被直接加载进内存中,所以这部分核心模块引入时,文件定位和编译执行这两个步骤可以省略掉,并且在路径分析中优先判断,所以它的加载速度是最快的。

•  文件模块则是在运行时动态加载,需要完整的路径分析、文件定位、编译执行过程,速度比核心模块慢。

1.优先从缓存加载

与前端浏览器会缓存静态脚本文件以提高性能一样,Node对引入过的模块都会进行缓存,以减少二次引入时的开销。不同的地方在于,浏览器仅仅缓存文件,而Node缓存的是编译和执行之后的对象。不论是核心模块还是文件模块,require()方法对相同模块的二次加载都一律采用缓存优先的方式,这是第一优先级的。不同之处在于核心模块的缓存检查先于文件模块的缓存检查。

2.路径分析和文件定位

因为标识符有几种形式,对于不同的标识符,模块的查找和定位有不同程度上的差异。

1). 模块标识符分析
Node基于一个模块标识符进行模块查找。模块标识符在Node中主要分为以下几类。

核心模块,如http、fs、path等。
.或..开始的相对路径文件模块。
以/开始的绝对路径文件模块。
非路径形式的文件模块,如自定义的connect模块。

•  核心模块

核心模块的优先级仅次于缓存加载,它在Node的源代码编译过程中已经编译为二进制代码,其加载过程最快。如果试图加载一个与核心模块标识符相同的自定义模块,那是不会成功的。如果自己编写了一个http用户模块,想要加载成功,必须选择一个不同的标识符或者换用路径的方式。

•  路径形式的文件模块

以.、..和/开始的标识符,这里都被当做文件模块来处理。在分析路径模块时,require()方法会将路径转为真实路径,并以真实路径作为索引,将编译执行后的结果存放到缓存中,以使二次加载时更快。由于文件模块给Node指明了确切的文件位置,所以在查找过程中可以节约大量时间,其加载速度慢于核心模块。

•  自定义模块

自定义模块指的是非核心模块,也不是路径形式的标识符。它是一种特殊的文件模块,可能是一个文件或者包的形式。这类模块的查找是最费时的,也是所有方式中最慢的一种。

2).文件定位

从缓存加载的优化策略使得二次引入时不需要路径分析、文件定位和编译执行的过程,大大提高了再次加载模块时的效率。但在文件的定位过程中,还有一些细节需要注意,这主要包括文件扩展名的分析、目录和包的处理。

•  文件扩展名分析

CommonJS模块规范也允许在标识符中不包含文件扩展名,这种情况下,Node会按.js、.json、.node的次序补足扩展名,依次尝试。在尝试的过程中,需要调用fs模块同步阻塞式地判断文件是否存在。因为Node是单线程的,所以这里是一个会引起性能问题的地方。小诀窍是:如果是.node和.json文件,在传递给require()的标识符中带上扩展名,会加快一点速度。

•  目录分析和包

在分析标识符的过程中,require()通过分析文件扩展名之后,可能没有查找到对应文件,但却得到一个目录,此时Node会将目录当做一个包来处理。

在这个过程中,Node对CommonJS包规范进行了一定程度的支持。首先,Node在当前目录下查找package.json(CommonJS包规范定义的包描述文件),通过JSON.parse()解析出包描述对象,从中取出main属性指定的文件名进行定位。如果文件名缺少扩展名,将会进入扩展名分析的步骤。而如果main属性指定的文件名错误,或者压根没有package.json文件,Node会将index当做默认文件名,然后依次查找index.js、index.node、index.json。

如果在目录分析的过程中没有定位成功任何文件,则自定义模块进入下一个模块路径进行查找。如果模块路径数组都被遍历完毕,依然没有查找到目标文件,则会抛出查找失败的异常。

3).模块编译
在Node中,每个文件模块都是一个对象,它的定义如下:

function Module(id, parent) {  
  this.id = id;  
  this.exports = {};  
  this.parent = parent;  
   if (parent && parent.children) {   
   parent.children.push(this);  
  }  
  this.filename = null;  
   this.loaded = false;  
  this.children = []; 
}

编译和执行是引入文件模块的最后一个阶段。定位到具体的文件后,Node会新建一个模块对象,然后根据路径载入并编译。对于不同的文件扩展名,其载入方法也有所不同,具体如下所示。

•  .js文件。

通过fs模块同步读取文件后编译执行。

•  .node文件。

这是用C/C++编写的扩展文件,通过dlopen()方法加载最后编译生成的文件。

•  .json文件。

通过fs模块同步读取文件后,用JSON.parse()解析返回结果。

•  其余扩展名文件。

它们都被当做.js文件载入。

每一个编译成功的模块都会将其文件路径作为索引缓存在Module._cache对象上,以提高二次引入的性能。

JavaScript模块的编译

回到CommonJS模块规范,我们知道每个模块文件中存在着require、exports、module这3个变量,但是它们在模块文件中并没有定义,那么从何而来呢?甚至在Node的API文档中,我们知道每个模块中还有__filename、__dirname这两个变量的存在,它们又是从何而来的呢?如果我们把直接定义模块的过程放诸在浏览器端,会存在污染全局变量的情况。

事实上,在编译的过程中,Node对获取的JavaScript文件内容进行了头尾包装。在头部添加了(function (exports, require, module, __filename, __dirname) {\n,在尾部添加了\n});。一个正常的JavaScript文件会被包装成如下的样子:

(function (exports, require, module, __filename, __dirname) {
 var math = require('math');
 exports.area = function (radius) {
  return Math.PI * radius * radius;
 };
});

这样每个模块文件之间都进行了作用域隔离。包装之后的代码会通过vm原生模块的runInThisContext()方法执行(类似eval,只是具有明确上下文,不污染全局),返回一个具体的function对象。最后,将当前模块对象的exports属性、require()方法、module(模块对象自身),以及在文件定位中得到的完整文件路径和文件目录作为参数传递给这个function()执行。

3.包和NPM

在模块之外,包和NPM则是将模块联系起来的一种机制。

学习Node.js模块机制

CommonJS的包规范的定义其实也十分简单,它由包结构和包描述文件两个部分组成,前者用于组织包中的各种文件,后者则用于描述包的相关信息,以供外部读取分析。

1.包结构

包实际上是一个存档文件,即一个目录直接打包为.zip或tar.gz格式的文件,安装后解压还原为目录。完全符合CommonJS规范的包目录应该包含如下这些文件。

package.json:包描述文件。
bin:用于存放可执行二进制文件的目录。
lib:用于存放JavaScript代码的目录。
doc:用于存放文档的目录。
test:用于存放单元测试用例的代码。

2.包描述文件

包描述文件用于表达非代码相关的信息,它是一个JSON格式的文件——package.json,位于包的根目录下,是包的重要组成部分。而NPM的所有行为都与包描述文件的字段息息相关。

这个可以看看NPM官网对package.json的定义规范。

可以通过npm adduser,  npm publish把自己的package上传到npm仓库。

三、题外话: AMD、CMD、兼容多种模块规范的类库

1. AMD

是CommonJS模块规范的一个延伸,它的模块定义如下:
define(id?, dependencies?, factory);

2.CMD

学习Node.js模块机制

3.兼容

为了让同一个模块可以运行在前后端,在写作过程中需要考虑兼容前端也实现了模块规范的环境。为了保持前后端的一致性,类库开发者需要将类库代码包装在一个闭包内。以下代码演示如何将hello()方法定义到不同的运行环境中,它能够兼容Node、AMD、CMD以及常见的浏览器环境中:

学习Node.js模块机制

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

Javascript 相关文章推荐
分享XmlHttpRequest调用Webservice的一点心得
Jul 20 Javascript
手写的一个兼容各种浏览器的javascript getStyle函数(获取元素的样式)
Jun 06 Javascript
常用的JavaScript模板引擎介绍
Feb 28 Javascript
JS模式之简单的订阅者和发布者模式完整实例
Jun 30 Javascript
jQuery实现默认是闭合的FAQ展开效果菜单
Sep 14 Javascript
jquery实现的V字形显示效果代码
Oct 27 Javascript
js实现的二分查找算法实例
Jan 21 Javascript
jQuery实现鼠标经过显示动画边框特效
Mar 24 jQuery
解决vue2.0动态绑定图片src属性值初始化时报错的问题
Mar 14 Javascript
vue登录页面cookie的使用及页面跳转代码
Jul 10 Javascript
JS array数组检测方式解析
May 19 Javascript
vue-preview动态获取图片宽高并增加旋转功能的实现
Jul 29 Javascript
微信小程序 火车票查询实例讲解
Oct 17 #Javascript
让编辑器支持word复制黏贴、截屏的js代码
Oct 17 #Javascript
Node.js下自定义错误类型详解
Oct 17 #Javascript
js HTML5多媒体影音播放
Oct 17 #Javascript
基于JavaScript实现前端文件的断点续传
Oct 17 #Javascript
js html5 css俄罗斯方块游戏再现
Oct 17 #Javascript
Node.js包管理器Yarn的入门介绍与安装
Oct 17 #Javascript
You might like
实测在class的function中include的文件中非php的global全局环境
2013/07/15 PHP
php中用memcached实现页面防刷新功能
2014/08/19 PHP
PHP与Java对比学习日期时间函数
2016/07/03 PHP
PHP实现电商订单自动确认收货redis队列
2017/05/17 PHP
详解阿里云视频直播PHP-SDK接入教程
2020/07/09 PHP
为你的网站增加亮点的9款jQuery插件推荐
2011/05/03 Javascript
JavaScript基本编码模式小结
2012/05/23 Javascript
jQuery 隐藏和显示 input 默认值示例
2014/06/03 Javascript
js实现特定位取反原理及示例
2014/06/30 Javascript
Node.js中的事件驱动编程详解
2014/08/16 Javascript
node.js 使用ejs模板引擎时后缀换成.html
2015/04/22 Javascript
js过滤HTML标签完整实例
2015/11/26 Javascript
基于JavaScript实现类似于百度学术高级检索功能
2016/03/02 Javascript
解决jquery无法找到其他父级子集问题的方法
2016/05/10 Javascript
jQuery中Nicescroll滚动条插件的用法
2016/11/10 Javascript
ionic开发中点击input时键盘自动弹出
2016/12/23 Javascript
详解Jquery EasyUI tree 的异步加载(遍历指定文件夹,根据文件夹内的文件生成tree)
2017/02/11 Javascript
nodejs中向HTTP响应传送进程的输出
2017/03/19 NodeJs
Js利用Canvas实现图片压缩功能
2017/09/13 Javascript
基于Bootstrap下拉框插件bootstrap-select使用方法详解
2018/08/07 Javascript
Vue项目路由刷新的实现代码
2019/04/17 Javascript
JS桶排序的简单理解与实现方法示例
2019/11/25 Javascript
Python/Django后端使用PIL Image生成头像缩略图
2019/04/30 Python
机器学习实战之knn算法pandas
2019/06/22 Python
python网络编程:socketserver的基本使用方法实例分析
2020/04/09 Python
如何用python实现一个HTTP连接池
2021/01/14 Python
HTML5 3D衣服摇摆动画特效
2016/03/17 HTML / CSS
bareMinerals官网:矿物质化妆品和护肤品
2018/02/04 全球购物
师范学院美术系毕业生自我鉴定
2014/01/29 职场文书
护士个人自我鉴定
2014/03/24 职场文书
有关九一八事变的演讲稿
2014/09/14 职场文书
学校安全管理制度
2015/08/06 职场文书
2019经典广告词集锦!
2019/07/02 职场文书
教你如何使用Python开发一个钉钉群应答机器人
2021/06/21 Python
python基础之模块的导入
2021/10/24 Python
Apache Hudi集成Spark SQL操作hide表
2022/03/31 Servers