学习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以及常见的浏览器环境中:

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

(0)

相关推荐

  • 一行命令搞定node.js 版本升级

    node有一个模块叫n(这名字可够短的...),是专门用来管理node.js的版本的. 首先安装n模块: npm install -g n 第二步: 升级node.js到最新稳定版 n stable 是不是很简单?! n后面也可以跟随版本号比如: n v0.10.26 或 n 0.10.26 就这么简单,这可怎么办??!! 另外分享几个npm的常用命令 npm -v #显示版本,检查npm 是否正确安装. npm install express #安装express模块 npm install

  • 简单模拟node.js中require的加载机制

    一.先了解一下,nodejs中require的加载机制 1.require的加载文件顺序 require 加载文件时可以省略扩展名: require('./module'); // 此时文件按 JS 文件执行 require('./module.js'); // 此时文件按 JSON 文件解析 require('./module.json'); // 此时文件预编译好的 C++ 模块执行 require('./module.node'); // 载入目录module目录中的 package.js

  • Node.js(安装,启动,测试)

    概念 Node.js 是构建在Chrome javascript runtime之上的平台,能够很容易的构建快速的,可伸缩性的网络应用程序.Node.js使用事件驱动,非阻塞I/O 模式,这使它能够更轻量,高效且完美的适用于运行在分布式设备之间的数据密集型实时应用程序. 安装 这里主要介绍基于windows平台上最简单方便的安装方式,我们首先直接访问node.js官方网站http://www.nodejs.org/,直接点击Install按钮开始下载安装. 点击Run按钮开始运行 继续点击Nex

  • Node.js与Sails ~项目结构与Mvc实现及日志机制

    本文首先从sails的安装讲起接下来介绍node.js与Sails的日志机制,小伙伴们已经迫不及待要看下文了吧,好吧. Sails是一个Node.js的中间件架构,帮助我们很方便的构建WEB应用程序,网址:http://www.sailsjs.org/,它主要是在Express框架的基础上发展起来的,扩展了新的功能组件,下面我们来看一下安装方法 一 安装Sails npm -g install sails 二 建立一个Sails的项目 sails new testProject 三 启动项目 c

  • node.js中的事件处理机制详解

    EventEmitter类 在Node.js的用于实现各种事件处理的event模块中,定义了一个EventEmitter类.所有可能触发事件的对象都是一个集成了EventEmitter类的子类的实例对象,在Node.js中,为EventEmitter类定义了许多方法,所有与对象的事件处理函数的绑定及解除相关的处理均依靠这些方法的调用来执行. EventEmitter类的各种方法 event:代表事件名 listener:代表事件处理函数 中括号内的参数代表该参数为可选参数 方法名与参数 描述 a

  • node.js中watch机制详解

    几乎所有构建系统都选择使用watch机制来解决开发过程中需要反复生成构建后文件的问题,但在watch机制下,长期以来我们必须忍受修改完代码,保存完代码必须喝口茶才能刷新看看效果的问题.在这里我们尝试探讨为什么watch不是银弹,并尝试寻找一种更好的方案来解决这个问题. watch基于的事实 当一个文件修改,我们能知道其修改可能导致的文件修改,那么重新构建这些文件即可. 通常对于文件A,构建成文件B这种场景,这种对应关系是极好确定的.但现实场景下,构建过程往往不是那么简单.例如: 文件A + 文件

  • 跟我学Node.js(四)---Node.js的模块载入方式与机制

    其它的如通过NPM安装的第三方模块(third-party modules)或本地模块(local modules),每个模块都会暴露一个公开的API.以便开发者可以导入.如 复制代码 代码如下: var mod = require('module_name') 此句执行后,Node内部会载入内置模块或通过NPM安装的模块.require函数会返回一个对象,该对象公开的API可能是函数,对象,或者属性如函数,数组,甚至任意类型的JS对象. 这里列下node模块的载入及缓存机制 1)载入内置模块(

  • Node.js中的模块机制学习笔记

    Javascript自诞生以来,曾经没有人拿它当做一门编程语言.在Web 1.0时代,这种脚本语言主要被用来做表单验证和网页特效.直到Web 2.0时代,前端工程师利用它大大提升了网页上的用户体验,JS才被广泛重视起来.在JS逐渐流行的过程中,它大致经历了工具类库.组件库.前端框架.前端应用的变迁.Javascript先天就缺乏一项功能:模块,而CommonJS规范的出现则弥补了这一缺陷.本文将介绍CommonJS规范及Node的模块机制. 在其他高级语言中,Java有类文件,Python有im

  • 详解Node.js中的事件机制

    前言 在前端编程中,事件的应用十分广泛,DOM上的各种事件.在Ajax大规模应用之后,异步请求更得到广泛的认同,而Ajax亦是基于事件机制的. 通常js给我们的第一印象就是运行在客户端浏览器上面的脚本,通过node.js我们可以在服务端运行javascript. node.js是基于单线程无阻塞异步式的I/O,异步式的I/O指的是当遇到I/O操作的时候,线程不阻塞而是进行下面的操作,那么I/O操作完成之后,线程时如何知道该操作完成的呢? 当操作完成耗时的I/O操作之后,会以事件的形式通知I/O操

  • 跟我学Nodejs(一)--- Node.js简介及安装开发环境

    学习资料 1.深入浅出Node.js 2.Node.js开发指南 简介(只捡了我觉得重要的) Node.js是让Javascript脱离浏览器运行在服务器的一个平台,不是语言: Node.js采用的Javascript引擎是来自Google Chrome的V8:运行在浏览器外不用考虑头疼的Javascript兼容性问题 采用单线程.异步IO与事件驱动的设计来实现高并发(异步事件也在一定程度上增加了开发和调试的难度): Node.js内建一个HTTP服务器,所以对于网站开发来说是一个好消息:  

随机推荐