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;
});