NodeJs基础

206 阅读12分钟

一、NodeJS是什么

简单的说 Node.js 就是运行在服务端的 JavaScript。 nodejs.org/en/ nodejs.cn/api/

Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时环境。 Node.js是一个事件驱动I/O服务端JavaScript环境,基于Google的V8引擎,V8引擎执行Javascript的速度非常快,性能非常好。

运行时环境runtime: 就是程序运行时, js是解释性语言, 有些是编译性语言

二、node.js特性:

Node.js的强大功能体现在很多方面,如事件驱动、异步处理、非阻塞I/O等。在这里将介绍Node.js具备的不同于其它框架的特点。

1. 单线程

这里的单线程是指主线程为“单线程”,所有的阻塞部分交给部分的线程池处理,然后这个主线程通过一个队列跟线程池协作,Node.js以单线程为基础的,这个正是Node.js保持轻量级和高性能的关键。

单线程的弱点:

  • 无法利用多核CPU
  • 一个用户造成线程奔溃,整个服务都奔溃
  • 大量计算占用CPU导致无法继续调用异步I/O

单线程的优点:

  • 操作系统完全不再有线程创建,销毁的时间开销
  • 减少了内存的开销,操作系统的内存换页
  • 不用像多线程编程一样处处在意状态的同步问题
  • 一个8G的内存服务器,可以同时处理超过四万用户的连接

2. 异步、非阻塞I/O

阻塞非阻塞

  首先需要明白一个概念, Js是单线程, 但是浏览器并不是, 事实上你的请求是浏览器的另一个线程在跑。   如果是阻塞的话, 那么该线程就会一直等到这个请求完成之后才能被释放用于其他请求。   如果是非阻塞的话, 那么该线程就可以发起请求后而不用等请求完成继续做其他事情。

IO是什么?

I/O(英语:Input/Output),即输入/输出,通常指数据在内部存储器和外部存储器或其他周边设备之间的输入和输出。

异步IO操作

文件读取的方式是一次性全部读取,当文件过大的时候,一次性读取不仅缓慢,而且影响用户体验,那么怎么实现分步读取呢,

这就得使用到异步IO的操作,像水流一样流出一段取得一段。

具体实现:

我们创建一个文件读取流,先上代码

var fs = require("fs");

var  data = "";  //声明一个空字符串来存读取的数据
var rs = fs.createReadStream("a.txt"); //创建一个可读流

rs.setEncoding("utf-8");

//监听当有数据流入的时候
rs.on("data",function(chunc){
    data += chunc;       //将读取的数据拼接到data上。
    console.log("..."); //读的过程中,我们打印三个点。
});

rs.on("end",function(){
    console.log("没有数据了")
});

使用 reateReadStream创建读取流对象,在对象上使用on监听“data”读取数据的事件,每读取一段数据,就会触发这个事件,当读取完毕, 就会触发“end”事件。

执行后,我们就可以看到下面打印的结果,从打印的多行"..."中,我们就可以看出,读取了多次才读完。

将读取到的数据,慢慢的写入 b.txt中,如果写入的文件不存在,会自动创建一个文件

 var fs = require("fs"); 
 var rs = fs.createReadStream("a.txt");//创建一个可读流
 var ws = fs.createWriteStream("b.txt"); //创建一个写入流
 rs.setEncoding("utf-8");
  //监听当有数据流入的时候
 rs.on("data",function(chunc){
     console.log("..."); //读的过程中,我们打印三个点。
     ws.write(chunc,"utf-8"); //向文件写入东西
 });
 rs.on("end",function(){
     console.log("没有数据了");
     ws.end();                 //关闭写入流
 });

这样我们异步的读取和写入文件就实现了

异步I/O机制,因此在执行访问数据库的代码之后,将立即去执行其后面的代码,把数据库返回结果的处理代码放在回调函数中,每个调用之间无需等待之前的I/O调用结束,提高了程序的执行效率。

一个异步I/O的大致流程:

(1) 发起I/O调用

  • 用户通过JavaScript代码调用Node核心模块,将参数和回调函数传入核心模块。
  • Node核心模块会将传入的参数和回调函数封装成一个请求对象。
  • 将这个请求对象推入I/O线程池等待执行。
  • JavaScript发起的异步调用结束,JavaScript线程继续执行后续操作。

(2)执行回调

  • I/O操作完成后会将结果存储到请求对象的result属性上,并发出操作完成的通知。
  • 每次事件循环时会检查是否有完成的I/O操作,如果有就将请求对象加入观察者队列中,之后当做事件处理。
  • 处理I/O观察者事件时会取出之前封装在请求对象中的回调函数,执行这个回调函数,并将result当做参数,以完成JavaScript回调的目的。
var fs=require('fs');
fs.readFile('/path',function(err,file){
    console.log('读取文件完成')
});

这里的“发起读取文件”是在“读取文件完成”之前输出的,同样,“读取文件完成”的执行也取决于读取文件的异步调用何时结束。

3. 事件与回调函数(事件驱动)

在node.js中,一个时刻只能执行一个事件回调函数,但在执行一个事件回调函数的过程中,可以转而处理其他事情(比如又有新用户连接),然后返回继续执行原事件的回调函数,这种处理机制称为“事件环”机制。

事件驱动的优势在于充分利用了系统资源,执行代码无须等待某种操作完成,有限的资源可以用于其他的任务。Node.js的目标是为后端的网络服务编程,在服务器的开发中,并发的请求处理是一个大问题,阻塞式的函数会导致资源的浪费和时间的延迟。通过事件的注册、异步函数,开发人员可以提高资源的利用率,性能也会改善。

准备工作

监听配置nodemon: 它的作用是监听代码文件的变动,当代码改变之后,自动重启 npm install -g nodemon 或者 yarn global add nodemon
安装在全局。 或者在vsCode开发工具安装code Runner 调试node程序:Debug - Start Debugging

util.promisify 的那些事儿

util.promisify是在node.js 8.x版本中新增的一个工具,用于将老式的 callback转换为Promise对象,让老项目改造变得更为轻松。

在官方推出这个工具之前,民间已经有很多类似的工具了,比如es6-promisify、thenify、bluebird.promisify。

手动实现一个promisify

es6原生支持了promise规范,可以使用new Promise()方法得到一个promise对象 参考:

function getName() {
    let p
    p = new Promise(function (resolve, reject) {
        setTimeout(function() {
            resolve('jianyong')
        }, 2000)
    })
    return p
}
getName().then(function (name) {
    console.log(name)
})

nodejs中,很多函数都是异步执行,我们需写一个回调函数作为异步函数的最后一个参数传入。比如fs包中的readFile这样的异步函数。

    //同步读取
    const data = fs.readFileSync('./package.json');
    console.log('data',data);
    console.log('data2',data.toString());


    //异步读取
    const data2 = fs.readFile('./package.json',function(err,data){
        console.log('异步data',data);
        console.log('异步data2',data.toString());
    });

使用promise改造fs.readFile

    // 使用 promise 改造 fs.readFile
    function readfile(file) { 
        return new Promise(function (resolve, reject) { 
           fs.readFile(file, function (err, data) {
                 if (err) { reject(err) } else { resolve(data) }
                 }) 
             }) 
           } 
   readfile('./package.json').then(function (data) { console.log(data.toString()) })

})();

如果每次遇到异步操作就需要像上面例子那样包裹改造一下岂不是很麻烦?!

可以利用柯里化函数 继续改造:

    function promisify(fn) {
        return function () {
            // let args = Array.prototype.slice.call(arguments);
            let args = Array.from(arguments);
            console.log(args)

            //------ 第一个参数是 './package.json' , 希望第二个参数是 回调 
            //  function (err, data) {
            //     if (err) { 
            //         reject(err) 
            //     } else { 
            //         resolve(data)
            //      }
            //     })  
           
            return new Promise(function (resolve, reject) {
                args.push(function (err, result) {
                    if (err) { reject(err); }
                    else resolve(result);
                });
                // 希望这样: fn(args[0],args[1]) 
                fn.apply(null, args);
            });
        }
    }

    // 使用:
    var readFile = promisify(fs.readFile);
    readFile('./package.json').then((res)=>{
        console.log(res.toString())
    })

内置的 promisify 转换后函数

如果你的Node版本使用10.x以上的,还可以从很多内置的模块中找到类似.promises的子模块,这里边包含了该模块中常用的回调函数的Promise版本(都是async函数),无需再手动进行promisify转换了。 拿fs模块进行举例:

// 之前引入一些 fs 相关的 API 是这样做的
const { readFile, stat } = require('fs')

// 而现在可以很简单的改为
const { readFile, stat } = require('fs').promises
// 或者
const { promises: { readFile, stat } } = require('fs')

readFile('./package.json').then((res)=>{
    console.log('从回调函数得到的数据:',res.toString());
})

fs内置模块:实现I/O操作

  • fs.mkdir / fs.mkdirSync:创建文件夹,有Sync的是同步创建,反之没有是异步,想要实现无阻塞的I/O操作,我们一般都是用异步操作完成要处理的事情
  • fs.readdir / fs.readdirSync:读取文件目录中的内容
  • fs.rmdir :删除文件夹
  • fs.readFile:读取文件中的内容
  • fs.writeFile:向文件中写入内容(覆盖写入:写入的新内容会替换原有的内容)
  • fs.appendFile:追加写入新内容,原有的内容还在
  • fs.copyFile:拷贝文件到新的位置
  • fs.unlink:删除文件

使用模块(module)

node内建模块 require('os') 第三方模块 ,需要安装 cpu-stat require("cpu-stat"); 自定义模块: // 导出 module.exports = {} // 导入 require('./conf')

Buffer(缓冲区)

buffer参考

http

Node 的网络应用都需要先创建一个网络服务对象,这里我们通过 createServer 来实现。 传入 createServer 的 function 在每次 HTTP 请求时都将被调用执行,因此这个 function 也被称为请求的处理者。事实上通过 createServer 返回的 Server 对象是一个 EventEmitter,我们需要做的仅仅是在这里保存这个 server 对象,并在之后对其添加监听器。

var http = require('http');
var server = http.createServer(function(request, response) {   
// handle your request
});

也可以这样:

var http = require('http');
var server = http.createServer(); 
server.on('request'function(request, response) {
// handle your request
});

当 HTTP 请求这个服务时,node 调用请求处理者 function 并传入一些用于处理事务相关的对象:request 和 response。我们可以非常方便的获得这两个对象。

var http = require('http');
var server = http.createServer(); 
server.on('request'function(request, response) {
    // handle your request
}).listen(8080); 

为了对实际的请求提供服务,在 server 对象上需要调用 listen 方法。绝大多数情况你需要传入 listen 你想要服务监听的端口号,这里也存在很多其他的可选方案

EventEmitter 类

events 模块只提供了一个对象: events.EventEmitter。EventEmitter 的核心就是事件触发与事件监听器功能的封装。 你可以通过require("events");来访问该模块。 都绑定了一个监听器,eventEmitter.on()用于监听事件,eventEmitter.emit()用于触发事件。

继承 EventEmitter

大多数时候我们不会直接使用 EventEmitter,而是在对象中继承它。包括 fs、net、 http 在内的,只要是支持事件响应的核心模块都是 EventEmitter 的子类。 为什么要这样做呢?原因有两点:

  • 具有某个实体功能的对象实现事件符合语义, 事件的监听和发生应该是一个对象的方法。
  • JavaScript 的对象机制是基于原型的,支持 部分多重继承,继承 EventEmitter 不会打乱对象原有的继承关系。

1. 从EventEmitter类继承

问题 你希望通过事件驱动的手段来解决问题。假如你有一个音乐播放器类,你希望在异步事件或者接口成功发生的时候来操作它。

解决办法 你需要创建一个基于EventEmitter类的自定义类,基于EventEmitter类得到的示例,都绑定了一个监听器,eventEmitter.on()用于监听事件,eventEmitter.emit()用于触发事件。

下面是一个音乐播放器的实例: 首先实现对EventEmitter类的继承

const EventEmitter = require('events');
//所有的构造函数都必须继承自EventEmitter类;
class MusicPlayer extends EventEmitter{};
//再通过这个构造函数来创建触发事件的对象
let musicPlayer = new MusicPlayer();

// 通过继承创建的实例对象有绑定监听器,可以调用on,emit方法
// 订阅 发布
//音响设备AudioDevice 
let AudioDevice = {
    play:function(track){
      //
      console.log(track);
      console.log('audio play');
    },
    stop:function(){
      //
      console.log('audio stop');
    }
  }
  //监听事件
  musicPlayer.on('play',function(track){
    this.playing = true;
    AudioDevice.play(track);
  })
  //监听事件
  musicPlayer.on('stop',function(track){
    this.playing = false;
    AudioDevice.stop();
  });
  
  musicPlayer.emit('play','The Roots - The Fire');
  
  setTimeout(function(){
  //emit触发事件
    musicPlayer.emit('stop')
  },3000);

2.添加多个监听器

我们可以给事件添加多个监听器,比如上面的音乐播放器,我们在play触发时需要做些其他的事情比如用户界面需要更新等。对play事件添加一个新的监听器就能轻松实现。

musicPlayer.on('play',function(track){
  this.playing = true;
  AudioDevice.play(track);});//添加新的监听器
musicPlayer.on('play',function(track){
  console.log('添加新的监听器')});

3.移除监听器

eventEmitter.removeListener(eventname,fn):移除一个监听器
emitter.removeAllListeners([eventName]):移除所有的监听器

let playFn1 = function(track){
  this.playing = true;
  AudioDevice.play(track);}
musicPlayer.on('play',playFn1);//移除监听器
musicPlayer.removeEventListener('play',playFn1())

4. 错误处理

通过监听error事件,来进行错误处理。

//错误处理
let playFn1 = function(track){
  this.playing = true;
  AudioDevice.play(track);
  //这里如果出现错误,就触发error事件
  this.emit('error','unable to play')}
musicPlayer.on('play',playFn1);
musicPlayer.on('error',function(err){
console.log(err);})

5. 拓展: Vue父子组件通信之$emit

emit的作用
在Vue中,父组件监听子组件触发的事件,需要在子组件用使用emit 触发,在父组件中使用 v-on: / @ 自定义事件监听。

stream

stream

仿写一个简版Express

1. 体验express

const express = require('express');
const app = express();
app.get('/',(req,res) => {
    res.end('Hello world')
})
app.get('/users',(req,res) => {
    res.end(JSON.stringify({name:'abc'}))
})
app.listen(3100 , () => {
    console.log('Example listen at 3000')
})

2. 实现myexpress

const http = require('http');
const url = require('url');
let routers = [];
class Application {
    get(path,hander){
        routers.push({
            path,
            method:'get',
            hander
        });
    }
    listen2(){
        const server = http.createServer(function(req,res){
            const {pathname} = url.parse(req.url,true);
            const {method,headers} = req;
            console.log(method);
            //在路由表routers通过pathname,找到回调,然后执行   
            var tet = routers.find(v=>{
            return  v.path == pathname && req.method.toLowerCase()==v.method
        })
          tet && tet.hander(req,res); 
          
         })
         //在Application原型上添加listen方法匹配路径, 执行对应的hander
       
         server.listen(...arguments)
        //  server.listen(3200,() => {
        //     console.log('Example listen at 3200')
        // })
       
    }
    
}
module.exports = function(){
    return new Application();
}