深入浅出Node.js——模块机制与CommonJS规范


在长时间的尝试中,JavaScript被不断的类聚和抽象,以更好的组织业务逻辑,但是在一个角度而言,它也道出了JavaScript先天就缺乏的一项功能——模块。
Java有类文件,Python有import机制,Ruby有require。而JavaScript只有通过 <script>标签引入而显得杂乱无章,语言本身毫无组织和约束能力。

经过了十多年的发展,CommonJs规范的提出算是一个重要的里程碑。

CommonJS规范

CommonJS规范为Javascript制定了一个美好的愿景,希望JavaScript能够在任何地方运行。

CommonJS将模块分为:

  • 模块引用
  • 模块定义
  • 模块标识

  • 模块引用

var math=require('math')
require()方法接受模块标识,以此引入一个模块的API到当前上下文中。


  • 模块定义

直接看代码:

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

在Node中,一个文件(比如math.js)就是一个module对象,它代表模块自身,而exports是module的属性。

上下文提供了exports对象用于导出当前模块的方法或者变量,并且它是唯一导出的出口。

在另一个文件中,通过require()方法引入模块后,就可以调用定义的属性或者方法了。比如:

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

  • 模块标识

模块标识其实就是传递给require()方法的参数,它必须是符合小驼峰命名的字符串,或者以..或者.开头的相对/绝对路径。它可以没有后缀名js。

Node的模块实现

尽管规范中的exports/require/module听起来十分简单,但是Node中引入模块,需要经历下面三个步骤:
1. 路径分析
2. 文件定位
3. 编译执行

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

  • 核心模块部分在Node源代码的编译过程中,编译进了二进制执行文件。这部分在Node进程启动时,直接被加载进内存中。所以文件定位和编译执行这两个步骤被跳过,并且在路径分析中优先判断,所以加载速度最快。
  • 文件部分要经历完成的上述三个过程。

优先从缓存加载

Node对引入过的模块都会进行缓存,以减少二次引入时的开销。

不论是核心模块还是文件模块,require()方法对相同模块的二次加载都一律采用缓存优先的方式。

路径分析和文件定位


  1. 模块标识分析符

前面提到过require()接受一个标识符作为参数。模块标识符主要分为下面几类:

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

下面的方式实验:
(1)创建module_path.js文件,其内容为console.log(module.paths)
(2)将其放在任意一个目录然后执行node module_path.js

在这里插入图片描述
根据其输出,可以看到,模块路径的生成规则:

  1. 当前目录下的node_modules目录
  2. 父目录下的node_modules目录
  3. 沿路径向上逐级扫描,直到根目录下的node_modules目录

当前文件的路径越深,模块查找会越耗时,这是自定义模块的加载速度最慢的原因。


  • 文件定位

从缓存加载使得第二次引入不需要分析路径、文件定位和编译执行的过程。
但是在文件的定位过程中,还有一些细节需要注意,包括文件扩展名的分析、目录和包的处理。

CommonJS模块规范允许标识符中不包含文件扩展名,这种情况下Node会按照.js、.json、.node的次序补足扩展名,依次尝试。所以加上扩展名会加快一点速度


  • 目录分析和包

如果在分析扩展名后,没有找到对应的文件但是找到了对应的目录,这时Node会将目录当作一个包来处理。

模块编译

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 = []; 
} 

编译和执行是引入文件模块的最后一个阶段。针对不同的文件名,载入方法也不同:

  • .js文件:通过fs模块同步读取文件后编译执行。
  • .node文件:通过dlopen()方法处理。
  • .json文件:通过fs模块同步读取文件后,用JSON.parse()解析返回结果。
  • 其余扩展名:当作.js文件载入。

NPM

Node组织了自身的核心模块,也使得第三方文件模块可以有序的编写和使用。但是在第三方模块中,模块与模块之间仍然是散列在各地的,相互之间不能直接引用。而在模块之外,包和NPM则是将模块联系起来的一种机制。

包的出现,则是在模块的基础上进一步组织JavaScript代码。
在这里插入图片描述

包结构

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

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

NPM帮助Node完成了第三方模块的发布、安装和依赖。借助NPM,Node与第三方模块之间形成了很好的生态系统。

包描述文件与NPM

package.json文件定义了如下一些必须的字段:

  • name:包名。
  • description:包简介。
  • version:版本号。
  • keywords:关键词数组。NPM中主要用来做分类搜索,一个好的关键词数组有利于用户快速找到你编写的包。
  • maincontainers:包维护者列表。每个维护者由name、email、web组成。比如:“maincontainers”:[{“name”:“LeesangHyuk”,“email”:“xxxx@163.com”,“web”:“http://xxxx.xxxx”}]
  • contributors:贡献者列表。
  • bugs:反馈bug的网页地址。
  • …等等
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值