Node简介
Ryan Dahl选择Javascript作为Node的实现语言原因:
- JS开发门槛较低并且没有什么历史包袱
- V8引擎的高性能
- 基于事件驱动
Node给JS带来的意义:
Node与浏览器(Chrome)结构十分相似,而且均是基于事件驱动的异步架构,打破了Js只能在浏览器中运行的局面,可以随心所欲的访问本地文件,搭建webworker服务器端,连接数据库,如web workers玩转多进程。
Node的特点:
- 异步I/O—可并行I/O操作,提升效率
- 事件与回调函数
- 单线程
好处:避免了多线程的死锁和线程上下文交换性能上的开销
坏处:无法利用多核cpu; 错误会引起整个应用退出,健壮性不好;大量计算占用CPU导致无法继续I/O操作(浏览器中JS和UI公用一个线程,h5中用web workers创建工作线程解决JS大计算阻塞UI渲染的问题,node中用child_process解决前面几个问题) - 可跨平台—libuv
Node应用场景:
1. I/O密集型和并行I/O,原因在于Node利用的是事件循环处理能力;
2. CPU密集型,可以编写c/c++扩展的方式或子进程方式提高CPU的利用率。
Node结构:
- Node Standard Library标准库,如Http, Buffer模块;
- Node Bindings—沟通JS和C++的桥梁,封装V8和libuv的细节,向上提供基础API服务;
- 第三层是支撑Node.js运行的关键
- v8是google开发的JS引擎,提供JS运行环境,(Node.js通过V8 C++的API函数接口进行操控)
- libuv是专门为Node.js开发的一个封装库(C),用于非阻塞型的 I/O 操作,同时在所有支持的操作系统上保持一致的接口
- C-ares: 提供异步处理DNS相关的能力;
- http_parser、OpenSSL、zlib 等:提供包括 http 解析、SSL、数据压缩等其他的能力
模块机制
JS从表单校验跃迁到应用开发级别过程:
工具(浏览器兼容)-> 组件(功能模块) -> 框架(功能模块组织)-> 应用(业务模块组织)
但是JS缺乏模块功能,开始的时候是以命名空间等方式人为的约束代码,后来出现了CommonJs规范—希望JS能在任何地方运行。
CommonJS规范
web发展时,前端浏览器端出现了很多标准的API,但是后端JS规范远远落后,有以下缺陷:
没有模块系统
标准库较少
没有标准接口
缺乏包管理系统
commonJs对模块的定义:
模块引用:require()
模块定义: exports() , 是module对象的属性
模块标识: 传给require()方法的参数
模块标识分类:
- 核心模块,如http,fs,path等
- .或…开始的相对路径文件模块
- 以/开始的绝对lying文件模块
- 非路径形式的文件模块,如自定义的connect模块
Node中的模块实现
模块分为Node提供的核心模块和用户自己编写的文件模块。
- 核心模块在node源码的编译过程中,编译进了二进制执行文件。在node进程启动时,部分核心模块直接被加载进内存,这些模块无需文件定位和编译执行,在路径分析时优先判断,加载速度最快。
- Node对引入过的模块都会缓存,缓存编译和执行之后的对象。从缓存加载无需下面的引入模块的三个步骤,加载效率高。
- 缓存加载优先于核心模块,核心模块的缓存加载检查优先于文件模块
引入模块分为3步:
-
路径分析
加载速度: 核心模块缓存加载 > 其他文件的缓存加载 > 核心模块加载 > 路径形式的文件模块加载 > 自定义模块的加载(非核心模块也不是路径形式的标识符)Node定位文件模块的模块路径查找策略:(路径组成的数组)
- 从当前目录下的node_modules目录开始查找,一直往父级目录下的node_modules查找,直至根目录下的node_modules目录,找到目标文件为止。
- 文件路径越深,模块查找越耗时
-
文件定位
- 扩展名分析:按照.js, .json, .node次序补足扩展名,依次尝试(尝试过程中会调用fs模块同步阻塞,所以如果是.node和.json最好带上扩展名)
- 目录分析和包: 在分析文件扩展名后未找到对应文件得到的是目录,会查找目录下package.json文件,JSON.parse()解析出包的描述对象,从中取出main属性指定的文件名定位。
若文件缺少扩展名,会继续上述扩展名分析的步骤。若main中没有文件或是没有package.json,则默认按照index.js, index.json, index.node依次查找。
-
编译执行
定位到具体文件后,node会新建一个模块对象,根据路径载入并编译。编译成功的模块会将其文件路径缓存在Module._cache对象上。JS模块的编译
在编译过程中,Node对获取的JS文件内容进行头尾包装,在头部添加(function (exports, require, module, __filename, __dirname) {\n, 在尾部添加\n})
,每个模块都进行了作用域隔离,包装后的代码通过vm原生模块runInThisContext()
方法执行,返回具体的function对象。
C/C++模块的编译
Node调用process.dlopen()
方法加载和执行,在windows和*nix平台下该函数实现方式不一致,通过libuv进行封装;
.node模块是C/C++编译之后生成的,无需编译
JSON文件编译
fs模块同步读取JSON文件,调用JSON.parse()得到对象并赋值给模块对象exports,供外部使用。小知识:exports只是module.exports的一个引用。(https://segmentfault.com/a/1190000010426778)
Node中,每个文件模块都是一个对象,定义如下:
function Module(id, parent) {
this.id = id;
this.exports = {};
this.parent = parent;
if (parent && parent.children) {
parent.children.push(this);
}
this.filename = null;
this.loaded = false;
this.children = [];
}
Node核心模块
核心模块分为 C/C++(src目录下) 和 JS编写(lib目录下) 的两部分。
-
JS核心模块编译
编译程序直接将所有JS模块文件转换成C/C++里的数组,标识符分析后加载入内存再进行编译。编译成功的模块缓存到NativeModule._cache对象上(文件模块缓存到Module._cache对象上)。 -
C/C++核心模块编译
内建模块:纯的C/C++编写的模块,不被用户直接调用(如node的buffer, fs模块等)。不推荐直接调用内建模块,直接调用核心模块比较好。
每个内建模块在定义后,都通过NODE_MODULE
宏将模块定义到node命名空间中。
Node启动时,会生成一个全局变量process,并提供Binding()方法协助加载内建模块。加载内建模块时,先创建一个exports空对象,然后调用get_builtin_module()方法取出内建模块对象。
- C/C++扩展模块
总结:
C/C++内建模块提供API给JS核心模块和第三方JS文件模块调用;
JS核心模块分为纯粹的功能模块,无需跟底层打交道;以及作为C/C++模块的封装层和桥接层,供文件模块调用;
文件模块是第三方编写,包括普通的JS模块和C/C++扩展模块。
包与NPM
Node模块规范的出现,一定程度上解决了变量依赖,依赖关系的问题。包的出现是在模块的基础上进一步组织JS代码。
包由包结构和包描述文件两部分组成。
完全符合CommonJS规范的包包含下面这些文件:
package.json:包描述文件
bin:存放可执行二进制文件的目录
lib:用于存放JS代码的目录
doc:用于存放文档的目录
test:用于存放单元测试用例的代码
NPM, node的包管理工具。
命令:
npm -v
npm install xx
npm install xx -g
发布包:
npm adduser 注册
npm publish<folder>
上传包 (在package.json文件目录npm publish .)
npm install 安装
npm owner 多人发布使用该命令管理包的所有者(npm owner ls/add/rm )
npm ls 分析包
局域NPM:
企业内部需要考虑模块保密性的问题,所以会自己搭建自己的NPM库,与搭建镜像站基本一样,不同点在于企业局域NPM可选择不同步官方源仓库的包,而且可以把私有的可重用模块打包到局域NPM仓库中,可保持更新的中心化。
NPM存在的问题:
对上传的包没有要求,包的质量无法保证;
Node代码运行在服务器端,需要考虑安全的问题。
AMD & CMD
由于前端js加载的瓶颈在于带宽,后端瓶颈在于cpu和内存资源等,Node的模块引入几乎都是同步的,但是前端采用同步会影响用户体验,所以出现了异步模块定义。
AMD规范(代表requireJS):
与CommonJS不同之处在于AMD需要define明确定义一个模块,Node是隐式包装,还有AMD规范的内容需要通过返回的形式导出。define(id?, dependencies?, factory)
CMD规范(代表seaJS):
玉伯提出,与AMD主要不同在于定义模块和依赖引入的部分(动态引入),更接近CommonJS规范的定义。define (factory)
示例:
Node写法:
// Math.js
exports.add = function(a, b) {
return a + b;
}
// app.js
var math = require('math');
exports.app = function(val) {
return math.add(val, 1);
}
AMD:
//Math.js
define(function() {
return {
add: function() {
return a + b;
}
};
});
// app.js
define(['math'], function(math) {
math.add(2, 3);
});
CMD:
//app.js
define(function(require, exports, module) {
var math = reuire('math');
math.add(2, 3);
});
兼容多种模块规范写法(Node, AMD, CMD, 浏览器环境):
为了让同一模块可以运行在前后端,需要考虑兼容前后端的代码,将类库代码包装在一个闭包内。
// 将hello()定义到不同环境中
(function(name, definition) {
//检查上下文环境是否为AMD或CMD
var hasDefine = typeof define === 'function';
var hasExports = typeof module !== 'undefined' && module.exports;
if (hasDefine) {
//AMD环境或者CMD环境
define(definition);
} else if (hasExports) {
// 定义为普通Node模块
module.exports = definition();
} else {
//将模块执行结果挂在window变量上
this[name] = definition();
}
})('hello', function() {
var hello = function() {};
return hello;
});
参考:
https://yjhjstz.gitbooks.io/deep-into-node/content/chapter1/chapter1-0.html
github地址:https://github.com/nodejs/node
官网:https://nodejs.org/zh-cn/docs/meta/topics/dependencies/