一、Nodejs介绍
在前端技术发展之初,前端工程师只能使用html、css和javascript进行页面的交互开发,但是随着Nodejs的出现,打破了javascript只能在浏览器中运行的局面,实现了前后端编程环境的统一。下面来简单介绍一下Nodejs的特点:
1、基于 Chrome V8 引擎的 Javascript 运行环境
笔者在前面介绍typescript模块化的时候有提及到commonjs和AMD这两种模块,其中commonjs就是服务器端js(Nodejs)模块化的规范。
也就是说Nodejs不是库,是一个运行环境,或者说是一个 JS 语言解释器。
它是2009年由大神 Ryan Dahl 开发的,它的底层源码是用C++开发的。Node环境下使用javascript语言,使得前端工程师有了更加广阔的发展空间,类似 React/Vuejs 这样的前端框架的开发框架变得非常强大和负责。
2、事件驱动
所谓事件驱动,是指在持续事务管理过程中,进行决策的一种策略,即跟随当前时间点上出现的事件,调动可用资源,执行相关任务,使不断出现的问题得以解决,防止事务堆积。
下面以一个生活例子模拟Nodejs的事件驱动: 假设现在我们在麦当劳点餐,常规操作是这样的:点餐付钱拿到相应的号码,然后在位置上等待叫号,等到餐品做好了,店员会喊号码,我们拿到了自己的饭菜,进行后续的处理——吃饭。
整个过程没有阻塞新用户的连接(点餐),也不需要维护已经点餐的用户与厨师的连接,这个取号的动作就是将回调函数存放在事件队列(event queue)中,喊号码的动作则是执行回调函数(Callback),能在事件(烧菜,I/O)处理完成后继续执行后面的逻辑(吃饭)。
(1)Node是一个单线程的语言,采用事件驱动和异步回调的机制。在执行代码的时候,主线程从上往下依次执行,遇到有需要回调的地方,就将此处加入到事件队列中,然后主线程继续往下走,直到运行结束以后,才去执行事件队列中的回调
(2)Node去执行事件队列中的事件时,如果遇到回调,依然是按照顺序添加进入事件队列,主线程一次往下执行,遇到回调就添加,直至执行完毕。
(3)Node进程创建一个循环,每个循环就是一个周期,在循环中会从事件队列里查看是否有事件需要处理,如果有就去除事件并执行相关的函数。对于阻塞事件的处理在幕后使用线程池来确保工作的运行,而不占用主循环流程。
3、非阻塞I/O,单线程
I/0 在服务器上可以理解为读写操作,非阻塞 I/O,也叫异步 I/O,显然对应的就是阻塞式 I/O。
传统的服务器语言大多是多线程、阻塞式 I/O。这也是 Node 与众不同的地方,对于传统的服务器语言,在与用户建立连接时,每一个连接都是一个线程。
当有十万个用户连接时,服务器上就会有十万个线程。而阻塞式 I/O 是指,当一个线程在执行 I/O 操作时,这个线程会阻塞,等待 I/O 操作完成后继续执行。
看看这样的业务场景:需要从多个数据源拉取数据,然后进行处理。假如获取profile和timeline操作各需要1S,那么串行获取就需要2S。
而Nodejs的处理方式是:遇到I/O事件会创建一个线程去执行,然后主线程会继续往下执行的,因此,拿profile的动作触发一个I/O事件,紧接着继续执行拿timeline的动作,再触发一个I/O事件,两个动作并行执行,假如各需要1S,那么总的时间也就是1S。
上面介绍到,在Node.js中,只有一个单线程不断地轮回查询队列中是否有事件。对于数据库 文件系统等I/O操作,包括HTTP请求等等这些容易堵塞等待的操作。
如果也是在这个单线程中实现,肯定会堵塞影响其他工作任务的执行,所以Javascript/Node.js会委托给底层的线程池(Thread Pool)执行,并会告诉线程池一个回调函数,这样单线程继续执行其他事情,当这些堵塞操作完成后,其结果与提供的回调函数一起再放入队列中,当单线程从队列中不断读取事件,读取到这些堵塞的操作结果后,会将这些操作结果作为回调函数的输入参数,然后激活运行回调函数。
请注意,Node.js的这个单线程不只是负责读取队列事件,还会执行运行回调函数,这是它区别于多线程模式的一个主要特点,多线程模式下,单线程只负责读取队列事件,不再做其他事情,会委托其他线程做其他事情,特别是多核的情况下,一个CPU核负责读取队列事件,一个CPU核负责执行激活的任务,这种方式最适合很耗费CPU计算的任务。
反过来,Node.js的执行激活任务也就是回调函数中的任务还是在负责轮询的单线程中执行,这就注定了它不能执行CPU繁重的任务,比如JSON转换为其他数据格式等等,这些任务会影响事件轮询的效率。
二、Nodejs调试
工欲善其事必先利其器,在前端开发中调试是非常重要的一部分,可以帮助我们更好的发现错误进行改正。下面就介绍一下Node的调试方法(在windows环境下,其他环境暂不展开):
1、Node文件名 + console.log
命令行中输入Node + js文件名,可以将js文件从上至下执行,Node语法中同样有console相关的api,所以我们可以将两者结合在控制台输入想要的数据:
这个方法的弊端显而易见,就是无法打断点进行调试,这对于我们习惯在chrome面板上调试web页面的前端开发来说是非常痛苦的,为此Node提供了两个可用于调试的协议,这也是我们主要使用的调试方式。
2、Node --inspect协议
Node6.3+ 的版本提供了两个用于调试的协议:v8 Debugger Protocol 和 v8 Inspector Protocol 可以使用第三方的 Client/IDE 等监测和介入 Node(v8) 运行过程,进行调试 。
v8 Inspector Protocol 是新加入的调试协议,通过 websocket (通常使用 9229 端口)与 Client/IDE 交互,同时基于 Chrome/Chromium 浏览器的 devtools 提供了图形化的调试界面。 下面是官方提供的调试指令:
同样是上面的代码:
// index.js
var fs = require("fs");
fs.readFile('./txt/remove.txt', function(err, data) {
if(err) throw err;
console.log('txt 文件数据:\n', data.toString());
})
fs.readFile('./html/first-child.html', {encoding: 'utf-8'}, (err, data) => {
if(err) throw err;
console.log('html 数据:\n', data);
})
(1)在cmd命令行执行--inspect相关指令
上面提及到Node --inspect基于 Chrome/Chromium 浏览器的 devtools 提供了图形化的调试界,这时候我们输入localhost:9229去发现页面无法访问,那是因为--inspect 对于一般的程序都是一闪而过,断点信号还没发送出去,就执行完毕了。 断点根本不起作用,这个时候就可以用--inspect-brk ,它的意思断点停在代码执行之前。
这个时候我们访问localhost:9229就可以看到页面显示‘webSockets request was expected’,打开调试面板,可以看到elements旁边有一个Node的绿色图标,点击即可进行调试,调试方式与我们平常在chrome上的调试基本一样。
(2)vscode中执行--inspect相关指令
vscode 内置了 Node debugger ,支持 v8 Debugger Protocol 和 v8 Inspector Protocol 两种协议。对于 v8 Inspector Protocol ,只需要在配置里添加一条 Attach 类型配置即可。在 Debug 控制面板, 点击 settings 图标,打开 .vscode/launch.json. 点击 “Node.js” 进行初始配置即可.
然后直接设置需要打断点的命令行,这次我们我们输入Node --inspect index.js,在vscode的面板我们可以看到代码断点在了我们想要的位置,左侧也有变量、监视、调用堆栈、断点这些我们比较熟悉的调试面板,如果输入Node --inspect index.js,代码执行会在首行之前停止,所以vscode调试也是一种比较可靠的调试方法
三、Nodejs之fs模块
根据官方文档的介绍,fs模块可用于与文件系统进行交互(以类似于标准 POSIX 函数的方式)。所有的文件系统操作都具有同步的、回调的、以及基于 promise 的形式。要使用该模块必须进行引入:
const fs = require('fs');
下面介绍一些我们常用到的fs的api方法,剩余的api可以到Nodejs官网去搜索查询:
1、文件系统标志
在开始fs模块的介绍之前,先为大家介绍一下fs的文件系统标志,这在后面会经常用到。当 flag 选项采用字符串时,则以下标志均可用:
'a': 打开文件用于追加。 如果文件不存在,则创建该文件。
'ax': 类似于 'a',但如果路径存在,则失败。
'as': 打开文件用于追加(在同步模式中)。 如果文件不存在,则创建该文件。
'as+': 打开文件用于读取和追加(在同步模式中)。 如果文件不存在,则创建该文件。
'r': 打开文件用于读取。 如果文件不存在,则会发生异常。
'r+': 打开文件用于读取和写入。 如果文件不存在,则会发生异常。
'rs+': 打开文件用于读取和写入(在同步模式中)。 指示操作系统绕过本地的文件系统缓存。
'w': 打开文件用于写入。 如果文件不存在则创建文件,如果文件存在则截断文件。
'wx': 类似于 'w',但如果路径存在,则失败。
'w+': 打开文件用于读取和写入。 如果文件不存在则创建文件,如果文件存在则截断文件。
'wx+': 类似于 'w+',但如果路径存在,则失败。
2、fs.readFile
readFile是fs模块的基础功能之一,用来读取文件的全部内容,以下是官方提供的方法介绍:
我们逐个参数来进行分析:
(1)path
path的类型可以是string、Buffer、URL以及integer;
我们一般填写文件的相对路径或者绝对路径对应的就是string类型
fs.readFile('./txt/remove.txt', function(err, data) {
if(err) throw err;
console.log('txt 文件数据:\n', data.toString());
})
integer指的是文件描述符,而文件描述符指的是每个打开的文件都分配了一个称为文件描述符的简单的数字标识符,在fs中可以根据文件描述符fd来操作文件,我们可以通过fs.open来试验一下,代码执行结果与上面一致:
// r 表示打开文件用于读取。 如果文件不存在,则会发生异常
fs.open('./txt/remove.txt', 'r', (err, fd) => {
if(err) throw err;
console.log(fd);
fs.readFile(fd, (err, data) => {
console.log('通过文件描述符读取', data.toString());
})
})
// 最终输出 fd 的值为一个整数
path也可以是Buffer类型,Buffer 对象用于表示固定长度的字节序列 ,Buffer 类是 JavaScript 的 Uint8Array 类的子类,且继承时带上了涵盖额外用例的方法 。
Buffer是Node的一个重要类型,在后面会有单独的详细介绍,这里先不展开。
// 执行结果与上面代码一致,只是把路径转为了 Buffer
const fileBuffer = Buffer.from('./txt/something.txt', 'utf-8');
fs.readFile(fileBuffer, { encoding: 'utf-8' }, (err, data) => {
if(err) throw err;
console.log(data);
})
在Node版本v7.6.0以后,path新增了对URL对象的支持。对于大多数 fs 模块的函数, path 或 filename 参数可以传入 WHATWG URL 对象。 仅支持使用 file: 协议的 URL 对象。
File协议主要用于访问本地计算机中的文件,就如同在Windows资源管理器中打开文件一样。要使用File协议,基本的格式如下:file:///文件路径,比如要打开D盘images文件夹中的pic.gif文件,可以根据路径file:///D:/images/pic.gif查询。
// 根据想要访问的文件地址转换成 URL 对象
const fileURL = new URL('file:///C:/Users/james/Desktop/training/Node/txt/something.txt');
fs.readFile(fileURL, { encoding: 'utf-8' }, (err, data) => {
if(err) throw err;
console.log(data);
})
(2)options
options主要有两个参数:encoding和flag。
encoding代表读取文件的编码格式,如果没有指定encoding,则返回原始的Buffer,它的值主要有:
type BufferEncoding = "ascii" | "utf8" | "utf-8" | "utf16le" | "ucs2" | "ucs-2" | "base64" | "latin1" | "binary" | "hex";
flag指的就是文件系统标志,详细可见本章节的part-1
(3)callback
callback指的是读取文件完成以后的回调函数,参数主要有两个:error以及data,error这里就不多说了,data根据encoding而定,默认为Buffer
(4)readFileSync
所有的文件系统操作都具有同步的、回调的、以及基于 promise 的形式,readFileSync是readFile的同步写法,除了没有回调函数以外,它的参数与readFile相同,它还返回读取的数据。
同步的形式会阻塞 Node.js 事件循环和进一步的 JavaScript 执行,直到操作完成。
let data = fs.readFileSync('./txt/something.txt', { encoding: 'utf-8' });
console.log('同步获得的数据为:', data);
// 由于同步阻塞的缘故,console 输出的 data 数据为 something.txt 文件内的数据
3、fs.writeFile
writeFile是fs模块的基础功能之一,用来写入数据到文件,以下是官方提供的方法介绍:
我们逐个参数来进行分析:
(1)path
这里的path与readFille的path相同,但是有一点需要注意的是:
当 file 是一个文件描述符时,行为几乎与直接调用 fs.write() 类似,与直接调用 fs.write() 的区别在于,在某些异常情况下, fs.write() 可能只写入部分 buffer,需要重试以写入剩余的数据,而 fs.writeFile() 将会重试直到数据完全写入(或发生错误)。
在文件描述符的情况下,文件不会被替换! 数据不一定写入到文件的开头,文件的原始数据可以保留在新写入的数据之前和/或之后。
例如,如果连续两次调用 fs.writeFile(),首先写入字符串 'Hello',然后写入字符串 ' World',则该文件将会包含 'Hello, World',并且可能包含文件的一些原始数据(取决于原始文件的大小和文件描述符的位置)。 如果使用了文件名而不是描述符,则该文件将会保证仅包含 ', World'。
// writeFile 的 path 为字符串,在 flag 取默认值的情况,hello 完全替换之前的文本
fs.writeFile('./txt/copy.txt', 'hello', (err) => {
if(err) throw err;
})
// writeFile 的 path 为文件描述符,在 flag 取默认值的情况,hello 以及,I am james 并没有完全替换之前的文本,而是包含了这两部分
fs.open('./txt/copy.txt', 'r+', (err, fd) => {
fs.writeFile(fd, 'hello', (err) => { if(err) throw err });
fs.writeFile(fd, ',I am james', (err) => { if(err) throw err });
})
(2)data
data的类型分别可以为:string、Buffer、TypedArrray和DataView。这里主要介绍TypedArrray和DataView。
从Node的ts声明可以知道,TypedArrray的类型主要有以下几种,它们都是Javascript标准内置对象中的可索引的集合对象,这里不展开描述:
type TypedArray =
| Uint8Array
| Uint8ClampedArray
| Uint16Array
| Uint32Array
| Int8Array
| Int16Array
| Int32Array
| BigUint64Array
| BigInt64Array
| Float32Array
| Float64Array;
DataView同样是Javascript标准内置对象,它是一个可以从 二进制ArrayBuffer 对象中读写多种数值类型的底层接口视图,以下是typescript的声明内容:
interface DataViewConstructor {
readonly prototype: DataView;
new(buffer: ArrayBufferLike, byteOffset?: number, byteLength?: number): DataView;
}
declare var DataView: DataViewConstructor;
(3)options
writeFile的options共有三个参数: encoding、mode和flag,encoding和flag上面已经介绍过了,而mode指的是读写权限,值为数字,一般去默认值。
(4)callback
callback需要注意的是,只有一个error参数,返回错误提示。
(5)writeFileSync
writeFileSync是writeFile的同步写法,除了没有回调函数,调用方法写入数据的方式与writeFile相同,但是会阻塞Node.js 事件循环和进一步的 JavaScript 执行,直到操作完成。
4、fs.unlink
unlink是fs模块的基础功能之一,unlink方法是fs模块删除文件的方法,以下是官方提供的方法介绍:
(1)path
path的参数只有三种:string、Buffer和URL,没有文件描述符
(2)callback
除了可能的异常,完成回调没有其他参数。
(3)unlinkSync
unlinkSync是unlink的同步写法,除了没有回调函数,调用方法写入数据的方式与writeFile相同,但是会阻塞Node.js 事件循环和进一步的 JavaScript 执行,直到操作完成。
5、fs.open
open方法主要用来打开文件,但返回的数据为文件描述符,在上面readFile的path介绍中有提及到。open方法的参数解析与readFile的也相同。
6、fs几种读写方式的区别
fs模块针对读操作为我们提供了readFile,read, createReadStream三个方法,针对写操作为我们提供了writeFile,write, createWriteStream三个方法。下面让我们来看一下它们之间的差异性:
(1)readFile和writeFile
readFile方法是将要读取的文件内容完整读入缓存区,再从该缓存区中读取文件内容;writeFile方法是将要写入的文件内容完整的读入缓存区,然后一次性的将缓存区中的内容写入文件中。
这里的读写操作,将文件内容视为一个整体,为其分配缓存区并且一次性将文件内容读取到缓存区中,在这个期间,Node.js将不能执行任何其他处理。所以当读写大文件的时候,有可能造成缓存区“爆仓”。
(2)read和write
read方法读取文件内容是不断地将文件中的一小块内容读入缓存区,最后从该缓存区中读取文件内容;
下面通过代码来看一下表现:
// new-file.txt 的文本内容:abcdefghijklmnopqrstuvwxyz
fs.open('./txt/new-file.txt', 'r', (err, fd) => {
if(err) throw err;
var buf = Buffer.alloc(225);
fs.read(fd, buf, 0, 12, 6, (err, bytesRead, buffer) => {
if(err) throw err;
console.log(buf.slice(0, bytesRead).toString(), buffer.toString());
})
})
// 汉字在 Buffer 里面占三个位置
console.log(Buffer.from('我看看占了多少位置', 'utf-8').length);
输出结果为:
write方法写入内容时,Node.js执行以下过程: 1、将需要写入的数据写入到一个内存缓存区; 2、待缓存区写满后再将缓存区中的内容写入到文件中; 3、重复执行步骤1和步骤2,直到数据全部写入文件为止;
下面通过代码来看一下表现:
// new-file.txt 的文本内容:abcdefghijklmnopqrstuvwxyz 我是原有的内容
fs.open('./txt/new-file.txt', 'r+', (err, fd) => {
if(err) throw err;
var buf = Buffer.from('我是被写入的内容');
fs.write(fd, buf, 0, 24, 26, (err, bytesWritten, buffer) => {
if(err) throw err;
console.log(buf.slice(0, bytesWritten).toString(), buffer.toString ());
})
})
// 执行完毕后,new-file.txt 的文本内容:abcdefghijklmnopqrstuvwxyz 我是被写入的内容
输入的结果如下:
以上读写操作,Node.js会将文件分成一块一块逐步操作,在读写文件过程中允许执行其他操作,但有的时候我们并不关心整个文件的内容,而只关注从文件中读取到的某些数据,以及读取到数据时需要执行的处理,这时我们也可以使用文件流(createReadStream和createWriteStream)来处理
(3)createReadStream和createWriteStream
createReadStream方法创建一个将文件内容读取为流数据的ReadStream对象:
下面通过代码来看一下表现:
// new-file.txt 的文本内容:abcdefghijklmnopqrstuvwxyz 我是原有的内容
var readStream = fs.createReadStream('./txt/new-file.txt', { flags: 'r', encoding: 'utf-8', start: 0, end: 25 });
readStream.on('open',function(fd) {
console.log('开始读取文件');
});
readStream.on('pause', function() {
console.log('暂停读取文件');
})
readStream.on('data', function(data) {
console.log('读取到数据:');
console.log(data);
readStream.pause();
});
setTimeout(() => {
console.log('1s 后取消暂停');
readStream.resume();
}, 1000)
输出结果如下:
以上方法可以对读写文件的过程中进行监听,并定义相关的方法pause和resume暂停或恢复文件的读取操作,可以监听写入时缓存区数据是否已满或者是否已全部输出,readStream的监听事件如下:
createWriteStream方法创建一个将流数据写入文件中的WriteStream对象:
下面通过代码来看一下表现:
// new-file.txt 的文本内容:abcdefghijklmnopqrstuvwxyz 我是原有的内容
var writeStream = fs.createWriteStream('./txt/new-file.txt', { flags: 'r+', encoding: 'utf-8', start: 26 });
writeStream.on('finish', () => {
console.log('写入结束')
})
var words = Buffer.from('我是写入的内容');
writeStream.write(words, (err) => {
if(err) throw err;
writeStream.emit('finish');
});
// 执行完毕后,new-file.txt 的文本内容:abcdefghijklmnopqrstuvwxyz 我是写入的内容
writeStream 可监听的事件有: