设为首页 收藏本站
查看: 1182|回复: 0

[经验分享] 《深入浅出nodejs》读书笔记 第2章 模块机制

[复制链接]

尚未签到

发表于 2017-2-22 10:11:11 | 显示全部楼层 |阅读模式
第二章 模块机制
 
JavaScript的变迁,经历十多年的发展后,社区也为JavaScript制定了相应的规范,其中CommonJS规范的提出算是最为重要的里程碑。
DSC0000.png
 

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应用在开发过程中事半功倍。
DSC0001.png
 

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。
DSC0002.png
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. 内建模块的导出
DSC0003.png
Node在启动时,会生成一个全局变量process,并提供Binding()方法来协助加载内建模块。

在加载内建模块时,我们先创建一个exports空对象,然后调用get_builtin_module()方法去处内建模块对象,通过执行register_func()填充exports对象,最后将exports对象按模块名缓存,并返回给调用方法完成导出。
 
2.3.3 核心模块的引入流程
DSC0004.png
 

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++扩展模块的加载
DSC0005.png
 

2.5 模块调用栈
我们明确一下各种模块之间的调用关系:
C/C++内建模块属于最低层的模块,属于核心模块,主要提供API给JavaScript核心模块和第三方JavaScript文件模块调用。
JavaScript核心模块主要扮演的职责有两类:
  1. 作为C/C++内建模块的封装层和桥接层,供文件模块调用。
  2. 纯粹的功能模块,不需要跟低层打交道,但十分重要。
文件模块通常有第三方编写,包括普通的JavaScript模块和C/C++扩展模块,主要调用方向为普通JavaScript模块调用扩展模块。
DSC0006.png
 

2.6 包与NPM
Node对模块规范的实现,一定程度上解决了变量依赖、依赖关系等代码组织问题。包的出现,则是在模块的基础上近一步组织JavaScript代码。
DSC0007.png
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
DSC0008.png
 

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

运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-345565-1-1.html 上篇帖子: nodejs 解析php的session_decode 的module(session_decode.js) 下篇帖子: 《深入浅出nodejs》读书笔记 第3章 异步I/O
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表