|
第二章 模块机制
JavaScript的变迁,经历十多年的发展后,社区也为JavaScript制定了相应的规范,其中CommonJS规范的提出算是最为重要的里程碑。
2.1 CommonJS规范
CommonJS规范为JavaScript制定了一个美好的愿景——希望JavaScript能够在任何地方运行。
2.1.1 CommonJS的出发点
前端JavaScript规范已经十分强大,而后端却远远落后。对于JavaScript自身而言,他的规范依然是薄弱的,还有以下缺陷:
1. 没有模块系统
2. 标准库较少
3. 没有标准接口
4. 缺乏包管理系统
CommonJS规范的提出,主要是为了弥补当前JavaScript没有标准的缺陷。如今,CommonJS中的大部分规范虽然依旧是草案,但是已经初见成效。
Node能以一种比较成熟的姿态出现,离不开CommonJS规范的影响。Node借鉴CommonJS的Modules规范实现了一套非常易用的模块系统,NPM对Package规范的完好支持使得Node应用在开发过程中事半功倍。
2.1.2 CommonJS的模块规范
1. 模块引用
var math = require('math');
在CommonJS规范中,存在require()方法,用它引入一个模块的API到当前上下文中。
2. 模块定义
对应引入功能,上下文提供了exports对象用于导入当前模块的方法或者变量,并且它是唯一导出的出口。在模块中,还存在一个module对象,它代表模块自身,而exports是module的属性。
在Node中一个文件就是一个模块,将方法挂在exports对象上作为属性即可定义导出的方式:
//math.js
export.add = function(){
var sum = 0,
i = 0,
args = arguments,
l = args.length;
while(i < 1){
sum += args[i++];
}
return sum;
}
//program.js
var math = require('math');
export.increment = function (val){
return math.add(val, 1);
}
3. 模块标示
模块标示其实就是传递给require()方法的参数。
它必须是符合小驼峰命名的字符串,或者以 .、..开头的相对路径,或者绝对路径。
它可以没有文件名后缀.js。
2.2 Node 的模块实现
在Node中引入模块,需要经历如下3个步骤。
1. 路径分析
2. 文件定位
3. 编译执行
在Node中,模块分为两类:
1. Node提供的模块,称为核心模块。
文件定位、编译执行这两个步骤可以省略掉,并且在路径分析中优先判断,所以它的加载速度是最快的。
2. 用户编写的模块,称为文件模块。
在运行时动态加载,三个步骤都要经历,速度比核心模块慢。
2.2.1 优先从缓存加载
Node对引入过的模块都会进行缓存(缓存编译和执行之后的对象),以减少二次引入的开销。
不论是核心模块还是文件模块,require()方法对相同模块的二次加载都一律采用缓存优先的方式,这是第一优先级。不同之处在于核心模块的缓存检查先于文件模块的缓存检查。
2.2.2 路径分析和文件定位
1. 模块标示符分析
模块标示符在Node中主要分为以下几类:
1. 核心模块,如http、fs、path等。
核心模块的优先级仅次于缓存加载,它在Node的源代码编译过程中已经编译为二进制代码,起加载过程最快。
2. .或..开始的相对路径文件模块。
3. 以/开始的绝对路径文件模块。
以.、..和/开始的标示符,这里被当做文件模块来处理。在分析路径模块时,require()方法会路径转为真实路径,并以真实路径作为索引,将编译执行后的将结果存放到缓存中,以使二次加载时更快。其加载速度慢于核心模块。
4. 非路径形式的文件模块,如自定义的connect模块。
模块路径:Node在定义文件模块的具体文件时制定的查找策略,具体表现为一个路径组成的数组。
模块路径的生成规则如下所示:
1. 当前文件目录下的node_modules目录。
2. 父目录下的node_modules目录。
3. 父目录的父目录下的node_modules目录。
4. 沿路径向上逐级递归,直到根目录下的node_modules目录。
在加载过程中,Node会逐个尝试模块路径中的路径,直到找到目标文件为止。可以看出,当前文件的路径越深,模块查找耗时会越多,这是自定义模块加载速度是最慢的原因。
2. 文件定位
文件扩展名分析
require()在分析标识符的过程中,会出现标示符中不包含文件扩展名的情况。这种情况下,Node会按.js、.node、.json的次序补足扩展名,并依次尝试。
建议:如果是.node和.json文件,在传递给require()的标示符中带上扩展名,会加快一点速度。
目录分析和包
在分析标示符的过程中,require()通过分析文件扩展名之后,可能没有查找到对应文件,但却得到一个目录,此时Node会将目录当做一个包来处理。
这个过程中,Node首先在当前目下查找package.json,通过JSON.parse()解析出包描述对象,从中取出main属性制定的文件名称进行定位。若main属性是定的文件名错误或者没有package.json文件,Node会将index当做默认文件名,依次拼接后缀进行查找。若都找不到,则会抛出查找失败异常。
2.2.3 模块编译
定位到具体的文件后,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});。
(function(exports, require, module, __filename, __dirname){
var math = require('math');
exprots.area = function(radius){
return Math.PI * radius * radius;
}
});
这样每个模块之间都进行了作用域隔离。
如果要达到require引入一个类的效果,请赋值给module.exprots对象。这个迂回的方案不改变形参的引用。
2. C/C++模块的编译
3. JSON文件的编译
2.3 核心模块
核心模块其实分为C/C++编写的和JavaScript编写的两部分。
C/C++文件存放在Node项目的src目录下。
JavaScript文件存放在lib目录下。
2.3.1 JavaScript核心模块的编译过程
1. 转存为C/C++代码
Node采用了V8附带的js2c.py工具,将所有内置的JavaScript代码转换成C++里的数组,生成node_natives.h头文件。在这个过程中,JavaScript代码以字符串的形式存储在node命名空间中。在启动Node进程是,JavaScript代码直接加在进内存中。
2. 编译JavaScript核心模块
在引入JavaScript核心模块的过程中,也经理了头尾包装的过程,然后才执行和导出exports对象。
核心模块源文件通过process.binding('native')取出,编译成功的模块缓存到NativeModule._cach对象上。
2.3.2 C/C++核心模块的编译过程
C++模块主内完成核心,JavaScript主外实现封装的模式是Node能够提高性能的常见方式。
我们将那些由纯C/C++编写的部门统一称为内建模块,他们通常不被用户直接调用。
1. 内建模块的组织形式
2. 内建模块的导出
Node在启动时,会生成一个全局变量process,并提供Binding()方法来协助加载内建模块。
在加载内建模块时,我们先创建一个exports空对象,然后调用get_builtin_module()方法去处内建模块对象,通过执行register_func()填充exports对象,最后将exports对象按模块名缓存,并返回给调用方法完成导出。
2.3.3 核心模块的引入流程
2.3.4 编写核心模块
2.4 C/C++扩展模块
在应用中,会频繁出现位运算的需求,包括转码、编码等过程,如果通过JavaScript来实现,CPU资源将会耗费很多,这是编写C/C++扩展模块来提升性能的机会来了。
值得注意的是,一个平台下的.node文件在另一个平台下是无法加载执行的,必须重新用各自平台下的编译器编译为正确的.node文件。
2.4.1 前提条件
2.4.2 C/C++扩展模块的编写
2.4.3 C/C++扩展模块的编译
2.4.4 C/C++扩展模块的加载
2.5 模块调用栈
我们明确一下各种模块之间的调用关系:
C/C++内建模块属于最低层的模块,属于核心模块,主要提供API给JavaScript核心模块和第三方JavaScript文件模块调用。
JavaScript核心模块主要扮演的职责有两类:
1. 作为C/C++内建模块的封装层和桥接层,供文件模块调用。
2. 纯粹的功能模块,不需要跟低层打交道,但十分重要。
文件模块通常有第三方编写,包括普通的JavaScript模块和C/C++扩展模块,主要调用方向为普通JavaScript模块调用扩展模块。
2.6 包与NPM
Node对模块规范的实现,一定程度上解决了变量依赖、依赖关系等代码组织问题。包的出现,则是在模块的基础上近一步组织JavaScript代码。
2.6.1 包结构
包实际上是一个文档文件,即一个目录直接打包为.zip或tar.gz格式文件,安装后解压还原为目录。
完全符合CommonJS规范的包目录应该包含如下这些文件。
1. package.json:包描述文件。
2. bin:用于存放可执行二进制文件的目录。
3. lib:用于存放JavaScript代码的目录。
4. doc:用于存放文档的目录。
5. test:用于存放单元测试用例的代码。
2.6.2 包描述文件与NPM
它是一个json格式的文件——package.json,位于包的根目录下,是包的重要组成部分。而NPM的所有行为都与包描述文件的字段息息相关。
CommonJS为package.json文件定义了如下一下必须的字段:
name:报名。
description:包简介。
version:版本号。
keywords:关键字数组,NPM中组要用来做分类搜索。
maintainers:包维护者列表。每个维护这由name、email、web三个属性组成。NPM通过该属性进行权限认证。
contributors:贡献者列表。
bugs:一个可以反馈bug的网页地址或邮件地址。
licenses:当前包所使用的许可证列表,表示这个包可以在哪些许可证下使用。
repositories:托管资源代码列表,表名可以通过哪些方式和地址访问包的源代码。
dependencies:使用当前包所需要依赖的包列表。这个属性十分重要,NPM会通过这个属性帮助自动加载依赖的包。
除了必须字段外,规范还定义了一部分可选字段:
homepage:当前包的网站地址。
os:操作系统支持列表。
cpu:CPU架构的支持列表。
engine:支持的JavaScript引擎列表。
builtin:标志当前包是否是内建在底层系统的标准组建。
directories: 包目录说明。
implements:实现规范的列表。
script:脚本说明对象。它主要被管理器用来安装、编译、测试和卸载包。
NPM实际需要的字段主要有:name、version、description、keywords、repositories、author、bin、main、scripts、engines、dependencies、devDependencies。
与包规范的区别多了author、bin、main和devDependencies这4个字段。
author:包作者。
bin:一些包作者希望包可以作为命令行工具使用。配置好bin后,通过npm install package_name -g命令添加到执行路径中,之后可以在命令行直接执行。
main:模块引入方法require()在引入包时,会优先检查该字段,并将其作为包中其余模块的入口。如果不存在这个字段,require()方法会检查包目了下的index.js、index.node、index.json文件作为默认入口。
devDependencies:一些模块只在开发是需要依赖。配置这个属性,可以提示包的后续开发者安装依赖包。
2.6.3 NPM常用功能
1. 查看帮助
2. 安装依赖包
这是NPM最常见的用法,它的执行语句:npm install express。执行该命令后,NPM会在当前目录下创建node_module目录,然后在node_module目录下面创建express目录,接着讲包解压到这个目录。
安装好依赖后,直接在代码中调用require(‘express’);即可一如该包。
全局模块安装
npm install express -g
实际上,-g是将一个包安装位全局可用的可执行命名。他根据包描述文件中的bin字段配置,将实际脚本链接到Node可执行文件相同的路径下。
从本地安装
对于一些没有发表到NPM上的包,可以通过将包加载到本地,然后以本地安装。
从非官方源安装
3. NPM钩子命令
配合scripts属性使用
4. 发布包
npm init
5. 分析包
npm ls
2.6.4局域NPM
2.6.5 NPM潜在问题
2.7 前后端共用模块
2.7.1 模块的侧重点
总管Node的模块引入过程,几乎全部都是同步的。尽管与Node强调异步的行为有些相反,但它是合理的。但是如果前端模块也采用同步的方式来引入,那将会在用户体验上造成很大的问题。UI在初始化过程中需要花费很多时间来等待脚本加载完成。
鉴于网络的原因,CommonJS为后端JavaScript制定的规范并不完全适合前端的应用场景。
2.7.2 AMD规范
2.7.3 CMD规范
2.7.4 兼容多种模块规范
为了让同一个模块可以运行在前后端,在写作过程中需要考虑兼容前端也实现了模块规范的环境。为了保持前后端的一致性,类库开发者需要将类库代码包装在一个闭包内。
第三章 异步I/O
http://xjyylc.iteye.com/blog/2087533 |
|