Node笔记一(简介 & 模块)

Node简介

Ryan Dahl选择Javascript作为Node的实现语言原因:

  1. JS开发门槛较低并且没有什么历史包袱
  2. V8引擎的高性能
  3. 基于事件驱动

Node给JS带来的意义:
Node与浏览器(Chrome)结构十分相似,而且均是基于事件驱动的异步架构,打破了Js只能在浏览器中运行的局面,可以随心所欲的访问本地文件,搭建webworker服务器端,连接数据库,如web workers玩转多进程。

Node的特点:

  1. 异步I/O—可并行I/O操作,提升效率
  2. 事件与回调函数
  3. 单线程

    好处:避免了多线程的死锁和线程上下文交换性能上的开销
    坏处:无法利用多核cpu; 错误会引起整个应用退出,健壮性不好;大量计算占用CPU导致无法继续I/O操作(浏览器中JS和UI公用一个线程,h5中用web workers创建工作线程解决JS大计算阻塞UI渲染的问题,node中用child_process解决前面几个问题)

  4. 可跨平台—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步:

  1. 路径分析
    加载速度: 核心模块缓存加载 > 其他文件的缓存加载 > 核心模块加载 > 路径形式的文件模块加载 > 自定义模块的加载(非核心模块也不是路径形式的标识符)

    Node定位文件模块的模块路径查找策略:(路径组成的数组)

    • 从当前目录下的node_modules目录开始查找,一直往父级目录下的node_modules查找,直至根目录下的node_modules目录,找到目标文件为止。
    • 文件路径越深,模块查找越耗时
  2. 文件定位

    • 扩展名分析:按照.js, .json, .node次序补足扩展名,依次尝试(尝试过程中会调用fs模块同步阻塞,所以如果是.node和.json最好带上扩展名)
    • 目录分析和包: 在分析文件扩展名后未找到对应文件得到的是目录,会查找目录下package.json文件,JSON.parse()解析出包的描述对象,从中取出main属性指定的文件名定位。
      若文件缺少扩展名,会继续上述扩展名分析的步骤。若main中没有文件或是没有package.json,则默认按照index.js, index.json, index.node依次查找。
  3. 编译执行
    定位到具体文件后,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目录下) 的两部分。

  1. JS核心模块编译
    编译程序直接将所有JS模块文件转换成C/C++里的数组,标识符分析后加载入内存再进行编译。编译成功的模块缓存到NativeModule._cache对象上(文件模块缓存到Module._cache对象上)。

  2. 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/

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值