CommonJs模块规范
CommonJS对模块的定以,主要分为模块引用、模块定义和模块标识3个部分
1、模块引用
1 | var math = require('math') |
2、模块定义
在Node中,一个文件就是一个模块,将方法挂载在exports对象上作为属性即可定义导出的方式。
模块中,module对象代表模块本身,exports是module属性,同时上下文提供exports对象用于到处当前模块带方法或变量。上下文提供的exports对象是module.exports对象的简写。
1 | //math.js |
模块标识
模块标识就是传递给require()
的参数,它必须符合小驼峰命名规则,或者以.、..开头的相对路径,或者绝对路径。它可以没有文件名后缀.js
Node的模块实现
在Node中引入模块,需要经历如下3个步骤:
- 路径分析,
- 文件定位,
- 编译执行
在Node中,模块分为两类:
- 核心模块: Node自带,Node启动时直接加载进内存,路径分析中优先判断,不需要文件定位和编译执行,加载速度最快,
- 文件模块:运行时动态加载,速度较慢
优先从缓存加载
不论是核心模块还是文件模块,require()
对相同模块对第二次加载都一律采用缓存优先,这是第一级优先。
核心模块缓存先于文件模块。
路径分析和文件定位
1. 模块标识符分析
模块标识符分类:
- 核心模块
- .或者..开始的相对路径文件模块
- 以/开始的绝对路径文件模块
- 非路径形式的文件模块,如自定义的connect模块
核心模块
优先级仅次于缓存加载
路径形式的文件模块
以.、..和/开始的标识符,都被当作文件模块处理。require()
会将路径转换为真实路径,并当作索引。
自定义模块
自定义模块指非核心模块,也不是路径形式的标识符。它是一种特殊的文件模块,可能是一个文件或者包的形式。
模块路径
模块路径是Node在定位文件模块的具体文件时制定的查找策略,具体表现为一个路径组成的数组。
模块路径的生成规则如下:
- 当前文件目录下的node_modules目录,
- 父目录下的node_modules目录,
- 父目录的父目录下的node_modules目录,
- 沿路径向上逐级递归,直到根目录下的node_modules目录
2. 文件定位
文件扩展名分析
标识符不包含文件扩展名时,Node按照.js
,.json
,.node
顺序补足扩展名。
目录分析和包
当分析扩展名之后,没有找到对应文件,却得到一个目录,Node将目录当作一个包来处理。
- 首先,Node在当前目录下查找
package.json
,通过JSON.parse()
解析出包描述对象,从中取出main
属性指定的文件名进行定位, - 如果
main
属性指定的文件名错误,或者压根没有package.json
文件,Node会将index当作默认文件名,然后依次查找index.js、index.json、index.node, - 如果目录分析没有定位成功任何文件,则自定义模块进入下一个模块路径进行查找,
文件模块编译
Node中,每个文件模块都是一个对象,它的定义如下:
1 | function Module(id, parent) { |
定位到具体文件后,Node会新建一个模块对象,然后根据路径载入并编译。对于不同的文件扩展名,其载入方式也有所不同,具体如下:
- .js文件。通过fs模块同步读取文件后编译执行。
- .node文件。这是用C/C++编写的扩展文件,通过
dlopen()
方法加载后编译。 - .json文件。通过fs魔都同步读取文件后,用
JSON.parse()
解析返回结果 - 其余扩展名文件。都当作.js文件载入。
每一个编译成功的模块都会将其文件路径作为索引缓存在Module._cache
对象上。
1. JavaScript模块的编译
编译过程中,Node对获取的JavaScript文件内容进行了头尾包装。在头部添加(function (exports, require, module, __filename, __dirname) {\n
,在尾部添加了\n}
。
1 | (function (exports, require, module, __filename, __dirname) { |
这样每个模块文件之间都进行了作用域隔离。包装之后的代码会通过vim原生模块runInThisContext()
方法执行,返回一个具体的function对象。
最后,将当前模块对象的exports
属性、require()
方法、module
(模块对象自身),以及在文件定位中得到的完整文件路径和文件目录作为参数传递给这个function()
执行。在require()执行后,模块的exports属性被返回给了调用方。
至此,require
、exports
、module
的流程已经完整,这Node对CommonJS的实现。
2. C/C++模块的编译
.node的模块文件是编写C/C++模块之后编译生成的,并不需要编译,只有加载和执行过程。在执行过程中,模块的exports对象与.node模块产生联系,然后返回给调用者。
3. JSON文件的编译
利用fs模块同步读取JSON文件内容,再调用JSON.parse()方法得到对象,然后将它赋给模块对象的exports。
核心模块
以后再补充
C/C++扩展模块
以后再补充
模块调用栈
包与NPM
包结构
包实际上是一个存档文件,即一个目录直接打包为.zip或tar.gz格式的文件,安装解压还原为目录。完全符合CommonJS规范的包目录应该包含如下文件:
- package.json:包描述文件。
- bin:用于存放可执行二进制文件的目录。
- lib:用于存放Javascript代码的目录。
- doc:用于存放文档的目录。
- test:用于存放单元测试用例的代码。
包描述文件与NPM
包描述文件package.json,位于包的根目录下,与NPM的所有行为息息相关。
CommonJS为package.json文件定义以下必需字段:
- name。规范定义它需要由小写的字母和数字组成,可以包含.、_和-,但不允许出现空格。
- description。 包简介。
- version。 版本号。semver上有详细定义,通常为major.minor.revision格式。
- keywords。关键词数组。
- maintainers。 包维护者列表。每个维护者由name、email和web3个属性组成。NPM通过该属性进行权限认证。
- contributors。贡献者列表。第一位是包作者。
- bugs。一个可以反馈bug的网页地址或邮件地址。
- licenses。当前所使用的许可证列表。
- repositories。托管源代码的位置列表。
- dependencies。当前包所需要依赖的包列表。
规范还定义一部分可选字段,如下: - homepage。 当前包的网站地址。
- os。
- cpu。
- engine。
- builtin。标志当前包是否是内建在底层系统的标准组件。
- directories。 包目录说明。
- implements。实现规范的列表。
- scripts。 脚本说明对象。主要用来被包管理器用来安装、编译、测试和卸载
NPM在包规范基础上增加author、bin、main和devDependencies4个字段
- author。包作者。
- bin。一些包作者希望包可以作为命令行工具使用。配置好bin字段后,通过
npm install package name -g
命令可以将脚本添加到执行路径中,之后可以命令行中直接执行。 - main。 模块引入方法require()在引入包时,会优先检查这个字段,并将其作为包中其余模块的入口。如果不存在这个字段,require()会查找包目录下的index.js、index.node、index.json文件作为默认入口。
- devDependencies。 一些模块只在开发时需要依赖。
NPM常用功能
全局模式安装
全局模式并不是将一个模块安装为一个全局包的意思,它并不意味着可以从任何地方通过require()来引用它。
实际上,-g
是将一个包安装为全局可用的可执行命令。通过全局模式安装的所有模块包都被安装进一个统一的目录下(path.resolve(process.execPath,'..','..','lib','node_modules')
),然后根据包描述文件中的bin字段来配置,通过软链接的方式将实际脚本链接到与node可执行文件相同的路径下。
NPM钩子命令
package.json中scripts字段的提出就是让包在安装或者卸载等过程中提供钩子机制。