NodeJS 模块

238 阅读5分钟

CommonJS 规范

CommonJS 提出的规范十分简单, 但是现实意义却十分强大。Node通过模块规范,组织了自身的原生模块,弥补JavaScript弱结构性的问题,形成了稳定的结构,并向外提供服务。

JavaScript 的发展历程,主要在浏览器前端发光发热。

  • Web1.0时代,只对DOM、BOM等基本的支持
  • Web2.0时代,HTML5将Web带进Web应用的时代,在浏览器中出现了强大的API供JavaScript调用

这些过程主要发生在前端,后端的JavaScript规范却远远落后。对于JavaScript而言,它的规范仍然是薄弱的,还有以下缺点:

没有模块系统

  • ES5 标准库较少
  • CMAScript 仅定义了部分核心库,对于文件系统,I/O流等常见的需求却没有标准的API。 没有标准接口
  • 几乎没有定义过如Web服务器或数据库之类的标准统一接口 缺乏包管理工具
  • 导致JavaScript应用中基本没有自动加载和安装以来的能力

CommonJS 规范涵盖了模块、二进制、Buffer、字符集编码、I/O流、进程环境、文件系统、套接字、单元测试、Web服务器网关接口、包管理等

CommonJS 的模块规范

CommonJS 对模块的定义:

  1. 模块引用
  2. 模块定义
  3. 模块标识

模块引用

var math = require('math')

require() 方法接受一个模块标识,来引入一个模块到当给钱上下文中。

模块定义

  • 在Node中,一个文件就是一个模块。
  • 在模块中,module 对象代表模块自身, exports 是module的属性。
  • 将方法挂载到 exports 对象上作为属性即可定义导出的方式:
exports.add = function() {
    var sun = 0, i=0;
    args = arguments, l = args.length;
    while(i<l) {
        sum += args[i++];
    }
    return sum;
}

模块标识

  • 就是传递给require()方法的参数
  • 必须符合小驼峰命名法的字符串
  • 或者以.或..开头的相对路径或者绝对路径
  • 可以没有文件后缀名

CommonJS 构建的模块导出和导入机制使得用户完全不必考虑变量污染

Node 的模块实现

在Node中引入模块, 需要经过3个步骤:

  1. 路径分析
  2. 文件定位
  3. 编译执行

Node模块分为两类:

  • 核心模块

    • 核心模块部分在Node源代码的编译过程中,编译进了二进制执行文件

    • 部分核心模块被就直接加载进内存中,所以在核心模块引入时,文件定位和编译执行步骤被省略

    • 在路径分析中优先判断,所以它的加载速度是最快的

  • 文件模块

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

路径分析

1.模块标识符分析

  • 模块标识符在Node中主要分为:
    • 核心模块
      • 核心模块的优先级仅次于缓存加载
      • 自定义模块名称不能与核心模块重名,否则是不会被加载的
    • .或..开始的相对经文件模块
      • require() 方法会将路径转为真实路径
      • 以真实路径为索引,将编译执行的结果存放到缓存
    • 以 / 开始的绝对路径文件模块
      • 以真实路径为索引,将编译执行的结果存放到缓存
    • 非路径形式的文件模块,如自定义的connect 模块
      • 一种特殊的文件模块
      • 可能是一种特殊的文件模块,可能是一个文件或者包的形式
      • 是所有方式中最慢的

模块路径是Node在定位文件模块的具体文件时指定的查找策略,具体表现为一个路径组成的数组

模块路径的生成规则:

  • 当前文件目录下的node_modules目录
  • 父目录下的mode_modules目录
  • 父目录的父目录下的node_modules目录
  • 沿着路径向上逐级递归,直到根目录下的node_modules目录

2.文件定位

  • 文件扩展名分析
    • CommonJS模块规范允许在标识符中不包含文件扩展名
    • Node会按.js、.json、.node的次序补足扩展名,依此尝试
    • 尝试的过程,会调用fs模块同步堵塞的判断文件是否存在

【建议】如果是.json或.node文件,在传递给require()的标识符中带上扩展名,会加快一些速度。

  • 目录分析和包
    • require() 通过分析文件扩展名后,可能没有找到对应的文件,但却得到一个目录,Node会将目录当做一个包来处理。
  • 首先:Node在当前目录下查找package.json(CommonJS包规范定义的包描述文件),通过 JSON.parse()解析出包描述对象,从中取出main属性指定的文件名进行定位。 如果文件名缺少扩展名,将会进入扩展名步骤
  • 如果main属性指定的文件名犯错误,或严格没有package.json文件,Node会将index当做默认文件名,然后依次查找index.js、index.json、index.node
  • 如果分析的过程中没有定位成功任何文件,则自定义模块进入到下一个模块模型进行查找,如果模块路径数组都被遍历完毕,仍然没有找到目标文件,则会抛出查找失败的异常

模块编译

定位到具体的文件后,Node会新建一个模块对象,然后根据路径载入并编译。对于不同的文件扩展名,载入方法不同:

  • JS 文件, 通过FS模块同步读取文件后编译执行
  • node 文件,是用C/C++编写的扩展文件,通过dlopen()方法加载最后编译生成的文件。
  • json 文件,通过fs模块同步读取文件后,用JSON.parse()解析返回结果
  • 其余扩展名文件,都被当做.js文件载入

自定义扩展名加载,require.extensions['.ext'], v0.10.6版本后不推荐使用,而是期望通过其他语言或文件编译成JavaScript文件后在加载

Node 对获取的JavaScript文件内容进行收尾包装。 头部添加: (function(exports,require, module,__filename, __dirname) { 尾部添加: })

头尾包装后,每个模块文件之间都进行了作用域隔离。

优先从缓存加载

Node 缓存的是编译和执行之后的对象
不论是核心模块还是文件模块,require() 方法对同一个模块的二次加载都一律采用缓存优先的方式。