Node.js中的模块机制

本文为读书笔记。

一、CommonJS的模块规范

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则是将模块联系起来的一种机制。

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

3.兼容

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

相关文章:http://ifandelse.com/its-not-hard-making-your-library-support-amd-and-commonjs/

时间: 10-08

Node.js中的模块机制的相关文章

Node.js入门:模块机制

CommonJS规范  早在Netscape诞生不久后,JavaScript就一直在探索本地编程的路,Rhino是其代表产物.无奈那时服务端JavaScript走的路均是参考众多服务器端语言来实现的,在这样的背景之下,一没有特色,二没有实用价值.但是随着JavaScript在前端的应用越来越广泛,以及服务端JavaScript的推动,JavaScript现有的规范十分薄弱,不利于JavaScript大规模的应用.那些以JavaScript为宿主语言的环境中,只有本身的基础原生对象和类型,更多的对

Node.js中的模块接口module.exports浅析

在写node.js代码时,我们经常需要自己写模块(module).同时还需要在模块最后写好模块接口,声明这个模块对外暴露什么内容.实际上,node.js的模块接口有多种不同写法.这里作者对此做了个简单的总结. 返回一个JSON Object 如下代码是一个简单的示例. 1 var exp = { 2 "version": "1.0.0", 3 "function1": null, 4 "module1": null, 5 };

【译】深入理解python3.4中Asyncio库与Node.js的异步IO机制

转载自http://xidui.github.io/2015/10/29/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3python3-4-Asyncio%E5%BA%93%E4%B8%8ENode-js%E7%9A%84%E5%BC%82%E6%AD%A5IO%E6%9C%BA%E5%88%B6/ 译者:xidui原文: http://sahandsaba.com/understanding-asyncio-node-js-python-3-4.html 译者前言 如

node.js中使用net模块创建服务器和客户端

1.node.js中net模块创建服务器(net.createServer) // 将net模块 引入进来 var net = require("net"); // 创建一个net.Server用来监听,当连接进来的时候,就会调用我们的函数 // client_sock,就是我们的与客户端通讯建立连接配对的socket // client_sock 就是与客户端通讯的net.Socket var server = net.createServer(function(client_soc

node.js中通过dgram数据报模块创建UDP服务器和客户端

node.js中 dgram 模块提供了udp数据包的socket实现,可以方便的创建udp服务器和客户端. 一.创建UDP服务器和客户端 服务端: const dgram = require('dgram'); //创建upd套接字 //参数一表示套接字类型,'udp4' 或 'udp6' //参数二表示事件监听函数,'message' 事件监听器 let server = dgram.createSocket('udp4'); //绑定端口和主机地址 server.bind(8888, '1

使用Node.js 和ws 模块构建WebSocket服务器

Node.js 中的ws 模块是最新的一个易用的.速度超快的web socket 实现,可以用来快速构建web socket 应用.其中还包含了wscat 命令行工具,可以用来模拟客户端或者服务器端. 在本实例中,我们将研究能够找到的最快的WebSocket 服务器.Node.js 中ws 模块不仅执行超快,而且使用也很简单.它实施方便,是本实例介绍Websocket 的理想选择. ws 模块很新,符合当前HyBi 协议草案,可以发送和接收数组类型数据(ArrayBuffer.Float32Ar

node.js中net网络模块TCP服务端与客户端的使用

node.js中net模块为我们提供了TCP服务器和客户端通信的各种接口. 一.创建服务器并监听端口 const net = require('net'); //创建一个tcp服务 //参数一表示创建服务的一些配置 //参数二表示 事件 'connection' 监听回调函数 let server = net.createServer({ //表示是否允许一个半开的TCP连接,默认为false allowHalfOpen: false, //一旦来了连接,是否暂停套接字,默认为false pau

log4js-Node.js中的日志管理模块使用与封装

开发过程中,日志记录是必不可少的事情,尤其是生产系统中经常无法调试,因此日志就成了重要的调试信息来源. Node.js,已经有现成的开源日志模块,就是log4js,源码地址:点击打开链接 项目引用方法: npm install log4js 1.配置说明(仅以常用的dateFile日志类型举例,更多说明参考log4js-wiki): { "appenders": [ // 下面一行应该是用于跟express配合输出web请求url日志的 {"type": "

node.js中模块,require

在php,C++中都有命名空间的概念,命名空间主要是用来解决引入文件存在函数,类,变量重名的问题,在node.js中,没有命名空间这么复杂的概念,在node中,有模块的概念,也就是将功能性的代码都放在一起.然后引入,在本文件中使用.这种不会出现函数名,变量重名问题,在引入的模块中,都是以导入的方式注册的.看下面的例子: //circle.js var pi = 3.14; //计算圆的周长 exports.circle = function(r) { return 2 * pi * r; } /