CommonJS 模块规范
模块引用
require() 方法,引入一个模块的 API 到当前上下文中
const math = require('math')
模块定义
exports 对象用于导出当前模块的方法或者变量
module 对象代表模块自身,而 exports 是 module 的属性
// math.js
exports.add = function() {
let sum = 0,
i = 0,
args = arguments,
l = args.length
while(i < l) {
sum += args[i++]
}
return sum
}
模块标识
传递给 require() 方法的参数,它必须是符合小驼峰命名的字符串,或相对路径/绝对路径
Node 的模块实现
在 Node 中引入模块, 需要经历如下3个步骤
-
路径分析
-
文件定位
-
编译执行
在 Node 中, 模块分为两类:一类是 Node 提供的模块, 称为核心模块;另一类是用户编写的模块,称为文件模块
-
核心模块部分在 Node 源代码的编译过程中,编译进了二进制执行文件。在 Node 进程启动时,部分核心模块就被直接加载进内存中,所以这部分核心模块引入时,文件定位和编译执行这两个步骤可以省略掉,并且路径分析中优先判断,所以它的加载速度是最快的
-
文件模块则是在运行时动态加载,需要完整的路径分析、文件定位、编译执行过程,速度比核心模块慢
优先从缓存加载
Node 对引入过的模块都会进行缓存,以减少二次引入时的开销。与浏览器仅仅缓存文件不同,它缓存的是编译和执行之后的对象
路径分析和文件定位
- 模块标识符分析
-
核心模块:优先级仅次于缓存加载
-
路径形式的文件模块:将路径转为真实路径,并以真实路径作为索引,将编译执行后的结果存放到缓存中,以使二次加载时更快,其加载速度慢于核心模块
-
自定义模块:非核心模块,也不是路径形式的标识符。它是一种特殊的文件模块,可能是一个文件或者包的形式。这类模块的查找是最费时的,也是所有方式中最慢的一种
- 文件定位
-
文件扩展名分析:CommonJS 模块规范允许在标识符中不包括文件扩展名,这种情况下,Node 会按 .js、.json、.node 的次序补足扩展名,依次尝试
在尝试的过程中,需要调用 fs 模块同步阻塞式地判断文件是否存在。因为 Node 是单线程的,所以这里是一个会引起性能问题的地方。 小诀窍是:如果是 .node 和 .json 文件,在传递给 require() 的标识符中带上扩展名,会加快一点速度。另一个诀窍是:同步配合缓存,可以大幅度缓解 Node 单线程中阻塞式调用的缺陷
-
目录分析和包
Node 在当前目录下查找 package.json,通过 JSON.parse() 解析出包描述对象,从中取出 main 属性指定的文件名进行定位。如果文件名缺失扩展名,将会进入扩展名分析的步骤。
而如果 main 属性指定的文件名错误,或者压根没有 package.json 文件,Node 会将 index 当做默认文件名,然后依次查找 index.js、index.json、index.node
如果在目录分析的过程中没有定位成功任何文件,则自定义模块进入下一个模块路径进行查找。如果模块路径数组都被遍历完毕,依然没有查找到目录文件,则会抛出查找失败的异常。
模块编译
-
JavaScript 模块的编译
在编译的过程中,Node 对获取的 JavaScript 文件内容进行了头尾包装。在头部添加了
(function(exports, require, module, ____filename, ____dirname)) {\n 在尾部添加了 \n}
为何存在 exports 情况下,还存在 module.exports,其原因在于,exports 对象是通过形参的方式传入的,直接赋值形参会改变形参的引用,如果要达到 require 引入一个类的效果,请赋值给 module.exports 对象。这个迂回的方案不改变形参的引用。
-
C/C++ 模块的编译
Node 调用 process.dlopen() 方法进行加载和执行
-
JSON 文件的编译
Node 利用 fs 模块同步读取 JSON 文件的内容之后,调用 JSON.parse() 方法得到对象,然后将它赋给模块对象的 exports,以供外部调用。JSON 文件在用做项目的配置文件时比较有用。如果你定义了一个 JSON 文件作为配置,那就不必调用 fs 模块去异步读取和解析,直接调用 require() 引入即可。此外,你还可以享受到模块缓存的遍历,并且二次引入时页没有性能影响。
核心模块
核心模块其实分为 C/C++ 编写和 JavaScript 编写的两部分,其中 C/C++ 文件存放在 Node 项目的 src 目录下
JavaScript 核心模块的编译过程
-
转存为 C/C++ 代码
-
编译 JavaScript 核心模块:也经历头尾包装的过程,然后才执行和道出了 exports 对象。不同于文件模块是,获取源代码的方式(核心模块是从内存中加载的)以及缓存执行结果的位置
C/C++ 核心模块的编译过程
在核心模块中,有些模块全部由 C/C++ 编写,有些模块则由 C/C++ 完成核心部分,其他部分则由 JavaScript 实现包装或向外导出,以满足性能需求。
-
内建模块的组织形式
-
内建模块的导出
文件模块可能会依赖核心模块,核心模块可能会依赖内建模块
-

通常,不推荐文件模块直接调用内建模块。如需调用,直接调用核心模块即可,因为核心模块中基本都封装了内建模块。
核心模块的引入流程

编写核心模块
编写头文件和编写 C/C++ 文件
C/C++ 扩展模块
JavaScript 的一个典型弱点就是位运算。JavaScript 的位运算参照 Java 的位运算实现,但是 Java 位运算是在 int 型数字的基础上进行的, 而 JavaScript 中只有 double 型的数据类型,在进行位运算的过程中,需要将 double 型转换为 int 型,然后再进行。所以,在 JavaScript 层面上作位运算的效率不高。
在应用中,会频繁出现效率低的操作(如上面提到的位运算),如果通过 JavaScript 来实现,CPU 资源将会耗费好多,这时编写 C/C++ 扩展模块来提升性能的机会来了。

前提条件
-
GYP 项目生成工具
-
V8引擎 C++ 库
-
libuv 库
-
Node 内部库
-
其他库
C/C++ 扩展模块的编写
普通的扩展模块与内建模块的区别在于无须将源代码编译进 Node,而是通过 dlopen() 方法动态加载。所以在编写普通模块时,无须将源代码写进 node 命名空间,也不需要提供头文件。
C/C++ 扩展模块的编译
写好 .gyp 项目文件,node-gyp 约定 .gyp 文件为 binding.gyp
C/C++扩展模块的加载
require() 方法通过间隙标识符、路径分析、文件定位,然后加载执行即可

C/C++ 扩展模块与 JavaScript 模块的区别在于加载之后不需要编译,直接执行之后就可以被外部调用了,其加载速度比 JavaScript 模块略快
使用 C/C++ 扩展模块的一个好处在于可以更灵活和动态地加载他们,保持 Node 模块自身简单性的同时,给予 Node 无线的可扩展性
模块调用栈

包与 NPM
CommonJS 的包规范的定义其实十分简单,它由包结构和包描述文件两个部分组成,前者用于组织包中的各种文件,后者则用于描述包的相关信息,以供外部读取分析
包结构
完全符合 CommonJS 规范的包目录应该包含如下这些文件
-
package.json:包描述文件
-
bin:用于存放可执行二进制文件的目录
-
lib:用于存放 JavaScript 代码的目录
-
doc:用于存放文档的目录
-
test:用于存放单元测试用例的代码
包描述文件与 NPM
包描述文件用于表达非代码相关的信息,它是一个 JSON 格式的文件——package.json, 位于包的根目录下,是包的重要组成部分
NPM 常用功能
- 查看帮助
$ npm help <command>
- 安装依赖包
# 全局模式安装
$ npm install <package> -g
# 从本地安装
$ npm install <tarball file>
$ npm install <tarball url>
$ npm install <folder>
# 从非官方源安装
$ npm install underscore --registry=http://registry.url
# 指定默认源
$ npm config set registry http://registry.url
- npm 钩子命令
"scripts": {
"preinstall": "preinstall.js",
"install": "install.js",
"uninstall": "uninstall.js",
"test": "test.js"
}
在以上字段中执行 npm install <package>
时,preinstall 指向的脚本将会被加载执行,然后 install 指向的脚本会被执行。在执行 npm uninstall <package>
时,uninstall 指向的脚本也许会做一些清理工作等。当在一个具体的包目录下执行 npm test
时,将会运行 test 指向的脚本
-
发布包
-
编写模块
-
初始化包描述文件
-
注册包仓库账号
-
上传包
-
安装包
-
管理包权限
-
分析包
-
局域 NPM
搭建自己的 NPM 仓库
NPM 潜在问题
潜在问题在于,鉴于开发者水平不一,上面的包质量也良莠不齐。另一个问题则是,Node 代码可以运行在服务器端,需要考虑安全问题
前后端公用模块
模块的侧重点
客户端的瓶颈在于带宽,服务端的瓶颈在于 CPU 和内存等资源
鉴于网络的原因,CommonJS 为后端 JavaScript 指定的规范并不完全是个前端的应用场景
AMD 规范
CMD 规范
兼容多种模块规范
;(function (name, definition) {
// 检测上下文环境是否为AMD或CMD
var hasDefine = typeof define === 'function',
// 检查上下文环境是否为Node
hasExports = typeof module !== 'undefined' && module.exports;
if (hasDefine) {
// AMD环境或CMD环境
define(definition);
} else if (hasExports) {
// 定义为普通Node模块
module.exports = definition();
} else {
// 将模块的执行结果挂在window变量中,在浏览器中this指向window对象
this[name] = definition();
}
})('hello', function () {
var hello = function () {};
return hello;
});
网友评论