node进阶

单线程的弱点

  1. 无法利用多核CPU。
  2. 错误会引起整个应用退出,应用的健壮性值得考虑。
  3. 大量计算占用CPU导致无法继续调用异步I/O。

像浏览器中JavaScript与UI共用一个线程一样,JavaScript长时间执行会导致UI渲染和响应被中断。在Node中,长时间的CPU占用也会导致后续异步I/O发不出调用,已完成的异步I/O的回调函数也会得不到及时执行。

Node采用了与Web Workers相同的思路来解决单线程中大计算量的问题:child_process。

子进程的出现,意味着Node可以从容地应对单线程在健壮性和无法利用多核CPU方面的问题。通过计算分发到各个子进程,可以将大量计算分解掉,然后再通过进程之间的事件消息来传递结果,这可以很好的保持应用模型的简单和地低依赖。通过Master-Worker的管理方式,也可以很好地管理各个工作进程,以达到更高的的健壮性。

node简介

  • Node.js是一个js运行环境,实际上它是对Google v8引擎的封装。V8引擎执行js的速度快,性能好。node.js对一些特殊用例进行了优化,提供了替代的API,使得V8在非浏览器环境上运行的更好。
  • node.js是一个基于chrome javascript运行建立的平台,用于方便的搭建响应速度快,易于拓展的网络应用。node.js使用事件驱动,非阻塞I/O模型得以轻量和高效,非常适合在分布式设备上运行数据密集型的实时应用。
  • node采用一系列“非阻塞”库来支持事件循环的方式。本质上就是为文件系统,数据库之类的资源提供接口。向文件系统发送一个请求时,无需等待硬盘(寻址并检索文件),硬盘准备好的时候非阻塞接口会通知node。该模型以可拓展的方式简化了对慢资源的访问,直观易懂。
  • node.js可以在不新增额外线程的情况下,依然可以对任务进行并行处理——node.js是单线程的。它通过事件轮询(event loop)来实现并行操作,因此,我们应该要充分的利用这一点——尽可能地避免阻塞操作

node.js组成部分

  • 引入required模块:我们可使用required指令载入node.js模块 var http = require(“http”);
  • 创建服务器:服务器可监听客户端请求,类似于Apache,Nginx等http服务器 。使用http.createServer()方法创建服务器,并使用listen方法绑定端口。
  • 接收请求和响应请求:服务器很容易创建,客户端可以使用浏览器或终端发送http请求,服务器接收请求并返回响应数据。使用request和response参数来接收和响应数据。

npm包管理器

  • 允许用户从NPM服务器下载别人编写的第三方包到本地使用。
  • 允许用户从NPM服务器下载并安装别人编写的命令行程序到本地使用
  • 允许用户将自己编写的包或命令行程序上传到NPM服务器供别人使用

NPM常用命令

NPM提供了很多命令,例如install和publish,使用npm help可查看所有命令。

  • NPM提供了很多命令,例如installpublish,使用npm help可查看所有命令。

  • 使用npm help <command>可查看某条命令的详细帮助,例如npm help install

  • package.json所在目录下使用npm install . -g可先在本地安装当前命令行程序,可用于发布前的本地测试。

  • 使用npm update <package>可以把当前目录下node_modules子目录里边的对应模块更新至最新版本。

  • 使用npm update <package> -g可以把全局安装的对应命令行程序更新至最新版。

  • 使用npm cache clear可以清空NPM本地缓存,用于对付使用相同版本号发布新版本代码的人。

  • 使用npm unpublish <package>@<version>可以撤销发布自己发布过的某个版本代码。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    npm -v  						查看版本
    npm version 查看所有模块的版本
    npm search 包名/部分包名 搜索包
    npm init 初始化package.json文件
    npm install/i 安装包
    npm remove/r包名 删除包
    npm install/i 包名 --save 安装包并添加到依赖中
    npm install 根据package.json下载当前项目所依赖的包
    npm install 包名 -g 全局安装包,用于一些编译根据,比如:gulp,webpack
  • 注意:如果想在页面中引入node_module中某个模块,优先从当前目录引入,如果没有,则从上一级目录中找,直到根目录。

REPL交互式解释器

  • REPL(Read Eval Print Loop:交互式解释器)表示一个电脑的环境,类似于终端,可接受系统的响应。node自带了交互式解释器,可执行以下任务
    • 读取,读取用户输入,解析输入js数据结构并存储于内存中。
    • 执行,执行输入的数据结构。
    • 打印,输出结果。
    • 循环,循环操作以上步骤直到用户两次按下ctrl+c按钮退出。

node知识

node.js与其它语言的区别
  • node.js不是一门独立的语言。php,jsp即使语言,又是平台。node.js用js进行编程,运行平台是封装后的js引擎V8

  • 轻量级架构

    • java,php,net,需要运行在服务器上,apache,tomcat
    • node.js不用架设在任何服务器软件之上。
    • 用最低的硬件成本,达到更高的开发,更优的处理函数。
  • node.js没有web容器,就是安装配置完成之后,没有根目录(php的根目录是www)

node.js的特点(追求极致性能)
  • 单线程
    • 优势:减少内存消耗(操作系统不再有创建线程,销毁线程的开销。
      • 在php,jsp等服务器语言中,会为每个用户创建一个线程,而每个线程大约需要2M内存,每创建一个线程就要占用内存空间。
      • 当有客户链接时,就会触发一个内部事件,通过非阻塞I/O,事件驱动机制,让node.js宏观上是并发的。可同时处理4万用户的请求。即当张三连接着时,李四请求连接,引擎就会停止张三语法的执行,转而将李四加入到时间栈中。
      • node.js不为每个用户创建一个线程,仅仅使用同一个线程。
    • 劣势:
      • 如果线程遭遇I/O阻塞,整个线程便阻塞了。
      • 如果有人将node.js搞崩溃了,则会全部奔溃。
  • 非阻塞
    • node.js采用非阻塞I/O机制,因此在执行完访问数据库操作后,会立即执行后面的代码(其他非阻塞事件不会,他们会等数据库操作完毕并返回结果才执行后面的代码),把数据库的处理代码放入回调函数中,从而提高效率。
    • 当某个I/O执行完毕后,将以事件的形式通过执行I/O操作的线程,线程执行完这个事件的回调函数。为了处理异步I/O,线程必须有事件循环,不断检查有没有未处理的事件,并依次予以处理。
    • 阻塞模式下,一个线程只能处理一项任务,要想提高吞吐量必须通过多线程。而非阻塞模式下,一个线程永远执行计算操作,这个线程的CPU核心利用率永远是100%;
  • 事件驱动
    • 不管用户的请求,还是老用户的I/O完成,都将以事件的形式加入事件环,等待调度。
  • node.js的iI/O都是异步的,都是回调函数调回调函数
node事件触发
  • 继承EventEmitter,大多数情况下不会直接使用EventEmitter,而是在对象中继承它。包括 fs、net、 http 在内的,只要是支持事件响应的核心模块都是 EventEmitter 的子类。
  • 原因:
    • 具有某个实体功能的对象实现时间符合语义,事件的监听和发射应该是一个对象的方法。
    • js对象是基于原型的,支持多重继承,继承EventEmitter不会打乱对象原有的继承关系。
node.js应用方向
  1. 特点
    • 善于I/O,不善于计算。
      • 因为node.js最擅长任务的调度,如果你的任务有很多CPU计算,实际上相当于这个计算阻塞了这个单线程,就不适合node开发。
      • 当应用程序需要处理大量并发的I/O,而在向客户端发出响应后,应用程序内部并不需要进行复杂的处理时,node也非常适合与websocket配合,开发长连接的实时交互应用程序。
    • 天生异步
      • callback,trunk(参数的求值策略),promise,generator(es6的生成器,用于计算),asynac函数

node模块化

  • CommonJS规范为JS能够在任何地方执行,这是一个愿景。
  • 从文件角度看,每个JS文件就是一个个模块,从结构上看,多个JS文件之间可以相互require共同实现一个功能,这整体功能就是一个模块。
  • 在node.js中,一个模块定义的变量只能在该文件中使用,当需要另一文件中的变量时,需使用exports进行暴漏,然后使用require引入。
  • 引入模块时,如果是非核心模块,且在同级目录时,require需要加上./,核心模块则不需要,直接写名字即可
node核心模块
  • 全局变量是global。

  • 每个node都在外面给我们套了一个函数。

    function(exports,require,module,_filename,_dirname){//里面是你写的内容}

    • exports:该对象用来将函数内部的局部函数暴漏到外部函数中。
    • require:用来引入外部模块。
    • module:代表当前模块本身,exports就是module的属性。我们可以使用exports或modile.exports导出。
    • _filename:当前模块的完整路径
    • _dirname:当前模块所在的文件的完整路径。
  • exports与module.exports的区别

  • json文件不能加注释。

Buffer缓存区

  • 可以理解为是一个存放二进制的容器,专门用于数据的存放。
  • node自带的,不需要引入就可使用。一个字节占8bit
  • 8bit = 1B 1024B = 1KB 1024KB = 1MKB 1GB=1024KB 1TB=1024GB
  • Buffer.from(str,编码格式);
  • Buffer.alloc(size[,fill[,encoding]])
    • size:新建的Buffer期望的长度,不能动态改变,溢出的数据不做处理。int值
    • fill:用来预填充新建的Buffer的值,默认为0. String Buffer int
    • encoding:如果fill为字符串,则该值就是它的字符编码,默认为“utf-8”

fs文件处理

  • fs.open(path,flag[,mode],callback)打开文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    //1.引入模块
    let fs = require('fs')
    //2.创建写入流,相当于在it666.txt与服务器之间建立一个通道,使数据可以源源不断从it666.txt中上传至服务器中
    let ws = fs.createWriteStream("it666.txt");
    //3.打开通道,监听打开事件,有on,once等方法,因只需监听一次,故用once
    stream.once('open',() => {
    console.log('通道已经打开');
    })
    stream.once('close', ()=> {
    console.log('通道已经关闭');
    });
    //写入东西
    stream.write('dfsadc');
    stream.write('dfsadc');
    //关闭通道,当发现还有东西未写入时,不会关闭
    stream.end()
    1
    2
    3
    4
    5
    let fs = require('fs')
    let ws = fs.createWriteStream("it666.txt");
    let rs = fs.createReadStream("sp.mp4");
    //创建管道,该语句就会自动将数据存入sp.mp4中,同时监听打开关闭等事件
    rs.pipe(ws);

数据库

  • 数据库就是按照一定的数据结构来组织,存储和管理数据的仓库。
  • 我们写的程序都是在计算机上运行的,一旦计算机断电或程序运行完毕,程序数据就会全部丢失,所以我们需要将一些程序运行的数据持久性保存到硬盘中,以确保数据的安全性。
  • 选择数据库的原因:
    • 数据库是有结构的,数据与数据之间可以建立各种关系,类似于网状拓扑图。
    • 数据库提供各种接口,让各种操作(增删改查)变得快捷简单。
    • 给个种语言(php,jsp,java)提供了完善的接口。
  • 数据库分类
    • RDBMS(关系型数据库):MySQL,SQL Server,ORACLE,DB2….
    • NoSQL(非关系型数据库 Not only SQL):MongoDB,CouchDB,HBase,Redis…
      • 没有行列的概念,用JSON来存储数据,集合就相当于“表”,文档就相当于“行”。
      • 非关系型数据库为非标准化的数据库。
      • 特征:键值存储数据库,列存储数据库,文档存储数据库,图形数据库。
    • 两者区别:关系型数据库比较结构化,操作不是很灵活;非关系型数据库操作灵活,但不适合大型数据存储,比较适合微架构….两者相辅相成。
MongoDB
  • MongoDB是为快速开发互联网Web应用而设计的数据库系统。他的数据类型是面向文档的,类似于JSON的结构。
  • 基本组成
    • 数据库(database):数据库是一个仓库,在仓库中可以存放集合。
    • 集合(collection):集合类似于数组,在集合中可以存放文档。
    • 文档(document):文档数据库中的最小单位,我们存储和操作的内容都是文档。
  • mongoDB的基本指令:
    • show dbs :显示当前所有的数据库
    • use 数据库名 :进入到指定的数据库中
    • db :显示当前数据库
    • show collections :显示数据库中的所有集合
  • 命令进行CRUD
    • 插入:db..insert(doc); 如:db.student.insert({id:001,name:’nikita’});
    • 查询:db..find();

同步与异步,阻塞与非阻塞

  • 同步,当发起一个调用时,在没有获取结果前,调用不会返回,直到获取结果。事一件一件做,做完一件在做一件。

  • 异步:当发起一个调用时,在没有获取结果之前,调用就返回了,调用者并不会立即得到结果,而是被调用者通知调用调用者,通过回调函数处理结果。

  • 阻塞,在等待结果时,不能干其他事,线程被挂起,直到结果返回。

  • 非阻塞:在等待结果中,还能干其他事,线程不会被阻塞。

url相关操作

  • 将url解析为一个url对象:url.parse(urlString[,parseQueryString[,slashDenotHost]])
  • 将一个url对象反解析为一个url地址:url.format(urlObject)
  • 将部分url拼接为一个完整url地址url.resolve(from,to)
  • response对象有一个方法:write可以用来给客户端发送响应数据。write可以使用多次,但最后需要用end来结束响应,否则客户端会一直等待
  • req.url是地址栏中localhost后面的所有内容,除了hash的内容。
  • querystring.parse将字符串转为一个对象。

上传文件

使用第三方插件

需要先安装formidable包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let http = require('http');
let url = require('url');
let formidable = require('formidable');
let util = require('util');//用于将object转为字符串
http.createServer((req,res) => {
if(req.url === '/postmsg' && req.methods.tolowerCase() === "post"){
//实例化对象
let form = new formidable.IncomingForm();
//设置上传文件路径
form.uploadDir = './uploads';
//获取表单内容
form.parse(req,(err,fileds,files) => {
res.writeHead(200,{"content-Type":"text/plain;charset=UTF-8"});
res.end(util.inspect({fields:fields,files:files}));
})
}
}).listen(80,'127.0.0.1');

模块编译

在Node中,每个文件模块都是一个对象,它的定义如下:

1
2
3
4
5
6
7
8
9
10
11
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会新建一个模块对象,然后通过路径载入并编译。对于不同的文件扩展名,其载入的方式也有所不同,具体如下。

1
2
3
4
5
6
7
.js文件。通过fs模块同步读取文件后编译执行。

.node文件。这是用C/C++编写的扩展文件,通过dlopen()方法加载最后的编译生成的文件。

.json文件。通过fs模块同步读取文件后,用JSON.parse()解析返回结果。

其余扩展名文件。它们都被当作.js文件解析。(因为node只能解析js文件,其他文件最后都会被转化成js文件,故当其余扩展名文件出现时,node无法识别,故将其认为是默认扩展名进行解析,即.js)

每一个编译成功的模块都会将文件路径作为索引缓存在Module._cache对象上,以提高二次引入的性能。

根据不同文件扩展名,node会调用不同的读取方式,如:.json文件调用如下:

1
2
3
4
5
6
7
8
9
Module._extensions['.json'] = function(module,filename){
var content = NativeModule.require('fs').readFileSync(filename,'utf8');
try{
module.exports = JSON.parse(strinpBOM(cntent));
}catch(err){
err.message = filename + ':' + err.message;
throw err;
}
}

其中,Module._extensions会被赋值给 require() 的extensions属性,所以通过在代码中访问require.extensions可知道系统中已有的加载方式。编写代码测试一下:

1
2
3
4
//新建index.js文件
console.log(require.extensions);
//运行node index.js,输出
//[Object: null prototype] { '.js': [Function], '.json': [Function], '.node': [Function] }

如果想对自定义的扩展名进行特殊的加载,可以通过类似require.extensions[‘.ext’]的方式实现。早期的CoffeeScript文件就是通过添加require.extensions[‘.coffee’]扩展方式来加载的。但是从v0.10.6版本开始,官方不鼓励通过这种方式进行自定义扩展名的加载,而是期望先将其它语言或文件先编译成js文件后再进行加载,这样做的好处是不将繁琐的编译加载等过程引入node的执行过程。

在确定文件的扩展名后,node将调用具体的编译方式来将文件执行后返回给调用者。

注:我们都知道CommonJS模块规范中,每个模块文件都存在require、exports、module、_filename、_dirname这5个变量却不知其从何而来。若是把直接定义模块的过程放在浏览器端,势必会存在污染全局变量的情况,故其不可能。

事实上,在编译过程中,node会对获取的js文件内容进行包装,具体如下:

1
2
3
4
5
6
(function(require、exports、module、_filename、_dirname){
var math = require('math');
exports.area = function(radius){
return Math.PI * radius * radius;
}
})

这样每个模块文件之间都进行了作用于隔离。包装之后的代码会通过vm原生模块的runInThisContext() 方法执行,类似于eval,只是具有明确上下文,不污染全局,返回一个体的function对象。最后,将当前模块对象的exports属性,require()方法,module(模块对象本身),以及在文件定位中得到的完整文件路径和文件目录作为参数传递给这个function()执行。

这就是为什么这些变量并没有在每个模块声明却可以使用的原因。在执行后,模块的exports属性被返回给了调用方。exports属性上的任何方法都可以被外部调用到,但是模块中的其余变量和属性则不可直接被调用。

至此,require、exports、module的流程已经完整,这就是Node对CommonJS模块规范的实现。

JSON文件的编译

json文件的编译是三种编译方式中最简单的。Node利用fs模块同步读取JSON文件的内容之后,调用JSON.parse()方法得到对象,然后将它赋给模块对象的exports,以供外部调用。

JSON文件在用作项目的配置文件时比较有用。如果你定义了JSON文件作为配置,那就不用调用fs模块去异步读取和解析,直接调用require()引入即可。此外,你还可以享受到模块缓存的便利,并且二次引入时也没有性能影响。

这里我们提到的模块编译都是指文件编译,即用户自己编写的模块。

内建模块

内建模块的优势在于:首先,他们本身由C/C++编写,性能上优于脚本语言;其次,其次,在进行文件编译时,他们会被编译进二进制文件。一旦node开始执行,他们会被直接加载进内存中,无需再次坐标识符定位、文件定位、编译等过程,直接就可执行。

Node在启动时,会生成一个全局变量process,并提供Binding()方法来协助加载内建模块。

在加载内建模块时,我们会先创建一个exports空对象,然后调用get_builtin_module()方法取出内建模块对象,通过执行register_func()填充exports对象,最后将exports对象按模块名缓存,并返回给调用方完成导出。

文章目录
  1. 1. 单线程的弱点
  2. 2. node简介
  3. 3. node.js组成部分
  4. 4. npm包管理器
  5. 5. NPM常用命令
  6. 6. REPL交互式解释器
  7. 7. node知识
    1. 7.0.1. node.js与其它语言的区别
    2. 7.0.2. node.js的特点(追求极致性能)
    3. 7.0.3. node事件触发
    4. 7.0.4. node.js应用方向
  • 8. node模块化
    1. 8.0.1. node核心模块
  • 9. Buffer缓存区
  • 10. fs文件处理
  • 11. 数据库
    1. 11.0.1. MongoDB
  • 12. 同步与异步,阻塞与非阻塞
  • 13. url相关操作
  • 14. 上传文件
    1. 14.0.1. 使用第三方插件
  • 15. 模块编译
    1. 15.0.1. 内建模块
  • |