Javascript第七篇,NodeJs第一篇,注重Node后端开发。

# 初始化Node文件夹

初始化文件夹

npm init -y
1

初始化文件夹后生成pacakge.json文件

nodemon用来监视node.js应用程序中的任何更改并自动重启服务,非常适合用在开发环境中。

安装nodemon

npm i -g nodemon
npm run serve  //运行服务器
1
2

package.json中dependency是全局使用的依赖

dev_dependcy只在开发时用的依赖,打包后不会上传,使用npm install -D 安装

# http创建服务器与客户端

创建服务器

var http = require('http');

http.createServer(function (request, response) {
    // 发送 HTTP 头部 
    // HTTP 状态值: 200 : OK
    // 内容类型: text/plain
    response.writeHead(200, {'Content-Type': 'text/plain'});

    // 发送响应数据 "Hello World"
    response.end('Hello World\n');
}).listen(8888);

// 终端打印如下信息
console.log('Server running at http://127.0.0.1:8888/');
1
2
3
4
5
6
7
8
9
10
11
12
13
14

创建客户端

var http = require('http');
 
// 用于请求的选项
var options = {
   host: 'localhost',
   port: '8080',
   path: '/index.html'  
};
 
// 处理响应的回调函数
var callback = function(response){
   // 不断更新数据
   var body = '';
   response.on('data', function(data) {
      body += data;
   });
   
   response.on('end', function() {
      // 数据接收完成
      console.log(body);
   });
}
// 向服务端发送请求
var req = http.request(options, callback);
req.end();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

服务端处理get、post请求


1

服务端安全退出:

api: Server.close()

# Fs读取文件夹、文件

Node由fs模块提供读写文件服务。基本上是POSIX文件操作命令的简单包装

基本上所有的与文件相关的操作都与fs核心模块有关

导入fs模块

var fs = require('fs')
1

fs模块常用的方法:读文件、写文件、追加写入文件、文件拷贝、创建目录

读写文件

//读、写文件
var readme = fs.readFileSync("readme.txt","utf8");//同步读文件

var readme = fs.readFile("readme.txt","utf8",function(err,data){
    if(!err){
      console.log(data);
    }
});

fs.writeFileSync("writeme.txt","readme")
fs.writeFile('writeme.txt',data,function(){

})
1
2
3
4
5
6
7
8
9
10
11
12
13

追加写入文件


1

拷贝文件、删除文件


fs.copyFileSync("3.txt","4.txt")  //同步拷贝
//异步拷贝,带回调函数
fs.copyFile("3.txt","4.txt",()=>{
  console.log(2)
})

//删除文件
fs.unlink('input.txt', function(err) {
   if (err) {
       return console.error(err);
   }
});
1
2
3
4
5
6
7
8
9
10
11
12
13

创建目录

//创建、读取、移除目录
fs.mkdir('',function(){

})
fs.readdir('',function(){

})
fs.rmdir('')
1
2
3
4
5
6
7
8

识别文件、目录类型

//识别文件、目录类型
fs.stat('',function(){
stats.isFile()
stats.isDirectory()
})
1
2
3
4
5

# Stream

stream(流)是一种抽象的数据结构。流可以把文件资源拆分成小块,一块一块的运输,资源就像水流一样进行传输,减轻服务器压力。

stream可以分为四类:

可读 Readable,有两个状态:paused、flowing。

可写 Writable,两个重要事件:drain、finish。

可读可写(双向)Duplex

可读可写(变化)Transform

可读流有两个状态 paused 和 flowing。

可读流默认处于 paused 态,一旦添加 data 事件监听,它就变为 flowing 态。删掉 data 事件监听,paused 态。

// 默认处于 paused 态
const stream = fs.createReadStream('./big_file.txt')
stream.pipe(response)
stream.pause(); // 暂停
setTimeout(() => {
  // 恢复
  stream.resume()
}, 3000)
1
2
3
4
5
6
7
8

用管道pipe连接两个不同的流,管道可以分为两个事件,监听data,stream1一有数据就传给stream2,监听 end 事件,当 stream1 停了,就停掉 stream2

stream1.on('data', (chunk) => {
	stream2.write(chunk)
})

stream1.on('end', () => {
	stream2.end()
})
1
2
3
4
5
6
7

https://juejin.im/post/5f1c508ff265da22ff546dca#heading-25

# Buffer

JavaScript 语言自身只有字符串数据类型,没有二进制数据类型。

但在处理像TCP流或文件流时,必须使用到二进制数据。因此在 Node.js中,定义了一个 Buffer 类,该类用来创建一个专门存放二进制数据的缓存区。

在 Node.js 中,Buffer 类是随 Node 内核一起发布的核心库。Buffer 库为 Node.js 带来了一种存储原始数据的方法,可以让 Node.js 处理二进制数据,每当需要在 Node.js 中处理I/O操作中移动的数据时,就有可能使用 Buffer 库。原始数据存储在 Buffer 类的实例中。一个 Buffer 类似于一个整数数组,但它对应于 V8 堆内存之外的一块原始内存。

stream转buffer

function streamToBuffer(stream) {
  return new Promise((resolve, reject) => {
    let buffers = []
    stream.on('error', reject);
    stream.on('data',(data)=> buffers.push(data));
    stream.on('end', () => resolve(Buffer.concat(buffers)))
  })
}
1
2
3
4
5
6
7
8

buffer转stream

let Duplex = require('stream').Duplex;

function bufferToStream(buffer) {
  let stream = new Duplex();
  stream.push(buffer);
  stream.push(null);
  return stream
}
1
2
3
4
5
6
7
8

https://juejin.im/post/6845166891401478158

# OS操作

os.tmpdir()//返回操作系统的默认临时文件夹
os.hostname()//返回操作系统的主机名
os.release()//返回操作系统的发行版本,字符串
os.type()//返回操作系统名,
os.uptime()//返回上次重新启动之后操作系统的运行时间,单位为秒
os.totalmem()//返回系统总内存量,单位字节
os.freemem()//返回系统空闲内存量,单位字节
os.arch()//返回操作系统的CPU架构
os.cpus()//返回数组对象,包含每个CPU的信息
os.networkInterfaces()//返回系统上可用的网络接口的详细信息
os.userInfo() //返回包含当前username、uid、gid、shell和homedir的对象
os.platform() //返回Nodejs的编译平台,如darwin、freebsd、linux、openbsd、win32等
1
2
3
4
5
6
7
8
9
10
11
12

# Path

var path = require("path");

//获取文件夹路径
path.dirname(p)
//获取文件名
path.basename(p[, ext])
//获取文件后缀名
path.extname(p)
//将路径转化为数组
path.parse(filepath)
//绝对路径相对路径转化
path.resolve([from ...], to)//转化为绝对路径
path.relative(from, to)//转化为相对路径
1
2
3
4
5
6
7
8
9
10
11
12
13

node中的几个文件变量的含义

__dirname // 获取当前执行文件所在目录的完整目录名
__filename // 获取当前执行文件的带有完整绝对路径的文件名
process.cwd()  //获取当前执行node命令时的文件夹目录名
./    //文件所在目录
1
2
3
4

# 核心工具函数util

util 是一个Node.js 核心模块,提供常用函数的集合,用于弥补核心 JavaScript 的功能 过于精简的不足。

const util = require('util');
1

util.inherits(constructor, superConstructor) 是一个实现对象间原型继承的函数。JavaScript 的面向对象特性是基于原型的,与常见的基于类的不同。JavaScript 没有提供对象继承的语言级别特性,而是通过原型复制来实现的。

util.inspect(object,[showHidden],[depth],[colors]) 是一个将任意对象转换 为字符串的方法,通常用于调试和错误输出。它至少接受一个参数 object,即要转换的对象。

util.isArray(object)判断给定的参数 "object" 是一个数组则返回 true,否则返回 false。

util.isRegExp(object)判断给定的参数 "object" 是一个正则表达式返回true,否则返回false。

util.isDate(object)判断给定的参数 "object" 是一个data对象则返回true,否则返回false。

# Node模块化加载方法

JavaScript 现在有两种模块。一种是 ES6 模块,简称 ESM;另一种是 CommonJS 模块,简称 CJS。

CommonJS 模块是 Node.js 专用的,与 ES6 模块不兼容。它们采用不同的加载方案。从 Node.js v13.2 版本开始,Node.js 已经默认打开了 ES6 模块支持。

Node.js 要求 ES6 模块采用.mjs后缀文件名。也就是说,只要脚本文件里面使用import或者export命令,那么就必须采用.mjs后缀名。Node.js 遇到.mjs文件,就认为它是 ES6 模块,默认启用严格模式,不必在每个模块文件顶部指定"use strict"

如果不希望将后缀名改成.mjs,可以在项目的package.json文件中,指定type字段为module

ES6 模块与 CommonJS 模块尽量不要混用。require命令不能加载.mjs文件,会报错,只有import命令才可以加载.mjs文件。反过来,.mjs文件里面也不能使用require命令,必须使用import

package.json文件有两个字段可以指定模块的入口文件:mainexports。比较简单的模块,可以只使用main字段,指定模块加载的入口文件。

# Crypto

# Node全局变量

node程序内部自带一些变量和函数,可以在node程序全局使用

# 当前目录与当前文件

_filename//输出当前脚本文件的绝对路径
_dirname//输出当前脚本文件的目录
1
2

# 定时器函数

var t= setTimeout(cb, ms);//设定ms后执行函数cb
clearTimeout(t)
setInterval(cb, ms)//每个ms后执行函数cb
clearTimeout(t)//停止一个之前创建的定时器
1
2
3
4

Node独有的定时器函数:

setImmediate/clearImmediate

# 控制台输出函数

console.log()
1

# Process

process对象是 Node 的一个全局对象,提供当前 Node 进程的信息。它可以在脚本的任意位置使用,不必通过require命令加载。该对象部署了EventEmitter接口。

基本属性:

  • process.argv:返回一个数组,成员是当前进程的所有命令行参数。一般第一个参数是node路径,第二个参数是文件路径,第三个参数
  • process.env:返回一个对象,成员为当前Shell的环境变量,比如process.env.HOME
  • process.installPrefix:返回一个字符串,表示 Node 安装路径的前缀,比如/usr/local。相应地,Node 的执行文件目录为/usr/local/bin/node
  • process.pid:返回一个数字,表示当前进程的进程号。
  • process.platform:返回一个字符串,表示当前的操作系统,比如Linux
  • process.title:返回一个字符串,默认值为node,可以自定义该值。
  • process.version:返回一个字符串,表示当前使用的 Node 版本,比如v7.10.0

针对shell的属性:

process.env属性返回一个对象,包含了当前Shell的所有环境变量。比如,process.env.HOME返回用户的主目录。

通常的做法是,新建一个环境变量NODE_ENV,用它确定当前所处的开发阶段,生产阶段设为production,开发阶段设为developstaging,然后在脚本中读取process.env.NODE_ENV即可。

方法:

  • process.chdir():切换工作目录到指定目录。
  • process.cwd():返回运行当前脚本的工作目录的路径。process.cwd()__dirname的区别。前者进程发起时的位置,后者是脚本的位置,两者可能是不一致的。
  • process.exit():退出当前进程。
  • process.getgid():返回当前进程的组ID(数值)。
  • process.getuid():返回当前进程的用户ID(数值)。
  • process.nextTick():指定回调函数在当前执行栈的尾部、下一次Event Loop之前执行。
  • process.on():监听事件。
  • process.setgid():指定当前进程的组,可以使用数字ID,也可以使用字符串ID。
  • process.setuid():指定当前进程的用户,可以使用数字ID,也可以使用字符串ID。

# eventEmitter

Node.js 所有的异步 I/O 操作在完成时都会发送一个事件到事件队列。

Node.js 里面的许多对象都会分发事件:一个 net.Server 对象会在每次有新连接时触发一个事件, 一个 fs.readStream 对象会在文件被打开的时候触发一个事件。 所有这些产生事件的对象都是 events.EventEmitter 的实例。

events 模块只提供了一个对象: events.EventEmitter。EventEmitter 的核心就是事件触发与事件监听器功能的封装。

实例

//event.js 文件
var EventEmitter = require('events').EventEmitter; 
var event = new EventEmitter(); 
event.on('some_event', function() { 
    console.log('some_event 事件触发'); 
}); 
setTimeout(function() { 
    event.emit('some_event'); 
}, 1000); 
1
2
3
4
5
6
7
8
9

常用场景:

  1. 模块间传递消息

  2. 回调函数内外传递消息

  3. 处理流数据,因为流是在EventEmitter基础上实现的. 4) 观察者模式发射触发机制相关应用

# 同步问题

event-emitter是同步触发

# 多进程

child_process模块开启多个子进程来执行node文件,执行开启的进程是主进程,被开启的进程是子进程。

child_process模块用于新建子进程。子进程的运行结果储存在系统缓存之中(最大200KB),等到子进程运行结束以后,主进程再用回调函数读取子进程的运行结果。

node有三种创建子进程的接口,每种方法有特定的使用场景。

exec/execFile: 用于执行bash命令,它的参数是一个命令字符串。用操作系统原生的方式执行各种命令,适用于输出轻量数据,执行的结果会存储在Buffer中,不同的是前者创建shell进行来执行命令,后者直接创建进程执行可执行文件,

spawn:是流式和操作系统进行交互,它属于异步执行,适用于子进程长时间运行的情况,适用于进程输入、输出数据量比较大的情况,支持stream的形式输入输出,可以用于任何命令,可以创建常驻后台进程。

fork: fork是spawn的特例,fork是两个node程序(javascript)之间时行交互,fork会在父子进程之间创建IPC通道,通过监听message事件和调用send方法,就可以在父子进程间通信了。

进程通信

使用 child_process.fork() 生成新进程之后,就可以用 child.send(message, [sendHandle]) 向新进程发送消息。新进程中通过监听message事件,来获取消息。

Node.js默认单进程运行,对于32位系统最高可以使用512MB内存,对于64位最高可以使用1GB内存。对于多核CPU的计算机来说,这样做效率很低,因为只有一个核在运行,其他核都在闲置。Node中提供了cluster模块,cluster实现了对child_process的封装,通过fork方法创建子进程的方式实现多进程模型。通过该模块简化多进程服务器程序的开发,统一通过主进程监听接口和分发请求。

cluster模块允许设立一个主进程和若干个worker进程,由主进程监控和协调worker进程的运行。worker之间采用进程间通信交换消息,cluster模块内置一个负载均衡器,采用Round-robin算法协调各个worker进程之间的负载。运行时,所有新建立的链接都由主进程完成,然后主进程再把TCP连接分配给指定的worker进程。

//导入cluster
var cluster = require('cluster');
//判断是否是主进程,是主进程就按cpu数新建若干worker进程,是worker进程就在该进程启动一个服务器程序
if(cluster.isMaster) {
  var numWorkers = require('os').cpus().length;
  console.log('Master cluster setting up ' + numWorkers + ' workers...');

  for(var i = 0; i < numWorkers; i++) {
    cluster.fork();
  }

  //监听子进程状态,主进程一旦监听到worker进程的exit事件,就会重启一个worker进程。worker进程一旦启动成功,可以正常运行了,就会发出online事件。
  cluster.on('online', function(worker) {
    console.log('Worker ' + worker.process.pid + ' is online');
  });

  cluster.on('exit', function(worker, code, signal) {
    console.log('Worker ' + worker.process.pid + ' died with code: ' + code + ', and signal: ' + signal);
    console.log('Starting a new worker');
    cluster.fork();
  });
}else {
  http.createServer(function(req, res) {
    res.writeHead(200);
    res.end("hello world\n");
  }).listen(8000);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

cluster代表整个集群,也就是工作进程和主进程,随着当前执行进程的变化,cluster的属性也在变化。在cluster上绑定的事件对每个进程都起作用,cluster的某些api只对主进程起作用,如fork、cluster.workers,有一些只对工作进程有用,如cluter.worker

worker对象:

# 孤儿进程与僵尸进程

孤儿进程是指父进程先退出,子进程由 pid 为 1 的 init 进程托管。

僵尸进程是指子进程先退出,但是父进程没有获取子进程的状态信息,导致子进程的进程描述符仍然保存在系统中。僵尸进程是有危害的,处理方法是退出主进程,init 进程会以父进程的身份对僵尸进程状态进行处理。

守护进程是在「后台运行」不受「终端控制」的进程(如输入、输出等)。在 nodejs 中,开启守护进程需要满足三个条件:

  • 使子进程成为进程组的头
  • 中断父子进程的 i/o
  • 去除父进程的事件循环中对子进程的引用

# 多线程

Nodejs中有三种线程

  • Event loop的主线程

  • libuv的异步I/O线程池

  • Node10.5之后,Node提供了worker_threads给node提供了真正的多线程

worker_thread模块中有四个对象两个类

  • isMainThread:是否是主线程

  • MessagePort:用于线程间的通信,继承自EventEmitter

  • MessageChannel:用于创建异步、双向通信的通道实例

  • threadId:线程ID

Worker:用于在主线程中创建子线程,第一个参数为filename,表示子线程的执行入口

parentPort:在worker线程中表示父进程的MessagePort类型的对象,在主线程中为null

workData:用于在主进程中向子进程传递数据

const {
    Worker,
    MessageChannel,
    MessagePort,
    isMainThread,
    parentPort
} = require('worker_threads');
if(isMainThread) {
    const worker = new Workd(_filename);
    const subChannel = new MessageChannel();
    worker.postMessage({hereIsYourPort:subChannel.port1},[subChannel.port1]);
    subChannel.port2.on('message',(value)=>{
        console.log('received:',value)
    })
} else {
    subChannel.once('message',(value)=>{
        assert(value.hereIsYourPort instanceof MessagePort);
        value.hereIsYourPort.postMessage('the worker is sending this');
        value.hereIsYourPort.close();
    })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 线程间通信

worker_thread线程之间可以共享内存。使用ArrayBuffer或SharedArrayBuffer

parentPort

主要用于主子线程通信,通过经典的 on('message'), postMessage形式

MessageChannel

可以通过线程间的消息传递来实现双向通信。 在内部,一个 Worker 具有一对内置的 MessagePort,在创建该 Worker 时它们已经相互关联。 虽然父端的 MessagePort 对象没有直接公开,但其功能是通过父线程的 Worker 对象上的 worker.postMessage() 和 worker.on('message') 事件公开的。 要创建自定义的消息传递通道(建议使用默认的全局通道,因为这样可以促进关联点的分离),用户可以在任一线程上创建一个 MessageChannel 对象,并将该 MessageChannel 上的 MessagePort 中的一个通过预先存在的通道传给另一个线程,

https://www.cnblogs.com/mengff/p/12815198.html

通常node的单线程是由于JavaScript的执行默认是单线程的,但是JavaScript的宿主环境,无论是node还是浏览器都是多线程的

node的单线程带来了一些问题,比如对cpu 的利用不足,某个未捕获的异常可能会导致整个程序的退出等。node的事件驱动和无阻塞特性使得在I/O密集型的业务场景(如限时抢购)等体现出巨大的优势。但是遇到加密、解密等CPU密集型复杂运算。当一个CPU占用率高的任务执行迟迟未完成时,后续队列中的延时、监听回调、nextTick等函数都会因被阻塞而无法执行,造成严重的延迟。更严重的情况。如果某个请求抛出错误,将有可能导致整个服务瘫痪。

# Node调用C++包

有一些场景下,用 C++扩展来实现尤为合适:

  • 计算密集型模块,C++的执行性能一般要高于 JS
  • 将现有的 C++类库低成本地封装成 Node.js 扩展,供 Node 生态使用
  • Node.js 提供的原生能力无法满足需要,比如fsevents (opens new window)
  • JS 语言在一些方面存在先天不足(例如数值精度、位运算等),可以通过 C++来补足

# node-gyp

node-gyp 是基于 GYP( 全称 Generate Your Projects,是谷歌开发的一套构建系统) 的。它会识别包或者项目中的 binding.gyp文件,这个里面是JSON的文件对工程依赖的各种文件进行了描述(可以理解为一个node版的CMakeList),然后根据该配置文件生成各系统下能进行编译的项目,如 Windows 下生成 Visual Studio 项目文件(*.sln 等),Unix 下生成 Makefile。在生成这些项目文件之后,node-gyp 还能调用各系统的编译工具(如 GCC)来将项目进行编译,得到最后的动态链接库 *.node 文件

在项目的顶层创建名为 binding.gyp 的文件,使用类似 JSON 的格式描述模块的构建配置。 该文件由 node-gyp (opens new window) 使用,这是一个专门为编译 Node.js 插件而编写的工具。

创建 binding.gyp 文件后,使用 node-gyp configure 为当前平台生成适当的项目构建文件。 这将在 build/ 目录中生成 Makefile(在 Unix 平台上)或 vcxproj 文件(在 Windows 上)。

{
    'targets': [
        {
            'target_name': 'addon', // 编译后为addon.node文件
            'sources': ['./addon.cc'] // 需要编译的源码
        }
    ]
}
1
2
3
4
5
6
7
8

接下来,调用 node-gyp build 命令生成编译后的 addon.node 文件。 这将被放入 build/Release/ 目录。

当使用 npm install 安装 Node.js 插件时,npm 使用它自己的 node-gyp 捆绑版本来执行相同的一组操作,按需为用户平台生成插件的编译版本。

在进行编译得到.node二进制文件

node-gyp build
1

编译得到文件Release/hoho.node,在index.js中引入该文件

// index.js
// 省略后缀名,自动找到hoho.node并加载、初始化
const hoho = require('./build/Release/hoho.node');

console.log(hoho.hoho());
1
2
3
4
5

运行该js文件,就可以运行hoho.node文件

$ node index.js
hoho, there.
1
2

命令功能

install安装开发文件,针对特定版本的node

list当前安装的工具列表

remove移除node特定版本的开发者文件

clean移除所有用configure和build命令生成的文件

configure为当前模块生成编译配置信息等

build编译当前模块

rebuild重新配置编译当前模块 相当于 clean ,configure, build的组合

# node-ffi

node-ffi提供了一组强大的工具,用于在Node.js环境中使用纯JavaScript调用动态链接库接口。它可以用来为库构建接口绑定,而不需要使用任何C++代码。

node-ffi并不能直接调用C++代码,你需要将C++代码编译为动态链接库:在 Windows下是 Dll ,在 Mac OS下是 dylib ,Linuxso

node-ffi 加载 Library是有限制的,只能处理 C风格的 Library

node-ffi: 这个模块可以直接引入C++的库,实现不用操作任何C++代码的C++库文件引入

编辑tsconfig.json


1

ts由于有较严的格式规范,往往会报一些不必要的格式警告,干扰编译

解决方法:

在vscode中下载插件prettier,然后在代码中全选,右键菜单选择格式化文档即可

# Webassembly

wasi WebAssembly系统接口

使用

import { readFile } from 'fs/promises';
import { WASI } from 'wasi';
import { argv, env } from 'process';

const wasi = new WASI({
  args: argv,
  env,
  preopens: {
    '/sandbox': '/some/real/path/that/wasm/can/access'
  }
});

// Some WASI binaries require:
//   const importObject = { wasi_unstable: wasi.wasiImport };
const importObject = { wasi_snapshot_preview1: wasi.wasiImport };

const wasm = await WebAssembly.compile(
  await readFile(new URL('./demo.wasm', import.meta.url))
);
const instance = await WebAssembly.instantiate(wasm, importObject);

wasi.start(instance);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 设置npm镜像源

npm config get registry  // 查看npm当前镜像源

https://registry.npm.taobao.org/  // 设置npm镜像源为淘宝镜像

yarn config get registry  // 查看yarn当前镜像源

https://registry.npm.taobao.org/  // 设置yarn镜像源为淘宝镜像
1
2
3
4
5
6
7

新建npmrc文件

在npmrc文件中粘贴npm地址

Last Updated: 2/19/2022, 12:09:50 PM