我来讲一下内置模块,但我就讲一些常用的哈,不然难度很大,而且后期大多数都用的是第三方模块

http模块

我先讲这个模块,这是创建 web 服务器的模块,通过该模块提供的 http.createServer() 方法,就能很方便的把一台普通的电脑,摇身一变,成一台 Web 服务器,从而对外提供 Web 资源服务

没错,用这个模块,就可以,创建一个简单的服务器了,很简单吧

服务器和普通电脑的区别在于,服务器上安装了 web 服务器软件

该模块可以基于 node.js 提供的 http 模块,通过几行简单的代码,就能轻轻松松的手写一个服务器软件,从而对外提供 web 服务

我都说到这了,那我就简单的科普一下,服务器相关的概念吧,>0<,别跑

IP 地址

IP 地址就是互联网上每台计算机的唯一地址,所以 IP 地址 是具有唯一性的

IP 地址 的格式:通常用“点分十进制”表示成(a.b.c.d)的形式,其中,a,b,c,d 都是 0~255 之间的十进制整数

实在不懂的话,就理解成 ip 地址是用来定位计算机的位置的,就行了,关于IP地址的演变等,这里就不细说了

域名和域名服务器

尽管 IP 地址 能够唯一的标记网络上的计算机,但 IP地址 是一长串数字,不直观,而且不便于记忆,于是人们又发明了另一套字符型的地址方案,即所谓的域名地址(Domain Name)

IP地址 和 域名 是一一对应的关系,这份对应关系存放在一种叫做域名服务器 (DNS,Domain name server) 的电脑中。使用者只需通过好记的域名访问对应的服务器即可使用,对应的转换工作由域名服务器实现。所以,域名服务器就是提供 IP 地址 和 域名 之间的转换服务的服务器而已

实在不懂的话,就可以理解成 域名服务器 是媒婆 ,她给 ip地址 和 域名 搭线架桥,结婚在社会层面的达成道德和法律的契约 ,相互绑定 ,这就好理解了吧 ,写这个教程好累 呼呼

端口号

在一台电脑中,可以运行成百上千个 web 服务,每个web 服务 都对应一个唯一的端口号,客户端发送过来的网络请求,通过端口号,可以被准确地交给对应的 web 服务 进行处理

端口号用来定位具体的应用程序,而端口号的范围是 0~65536 之间
在计算机中有一些常用默认的端口号,http 服务的 80 端口 ,https 服务的 443 端口等等
在实际开发过程中我么也只会用到一些常见的端口,例如:8080,3000,8088…

关键是这些端口号在日常开发中已经够用了

如果这都理解不了话,那还是别学了哈哈哈哈,劝退

如何简单创建一个 Web 服务器

关于监听事件我在本章下面讲的很清楚,所以不理解也很正常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 导入 http 模块
const http = require("http")
// 创建服务器实例
const server = http.createServer()
// 为服务器实例绑定 request 事件,也就是可监听客户端发送过来的网络请求
// 可以使用服务器实例的 .on() 方法,为服务器绑定一个 request 事件
server.on("requiest", (req, res) => {
// req 接受浏览器传来的参数
// res 返回渲染的内容
res.writeHead(200, { "Content-Type": "text/html;charset=utf-8" })
res.write(
`
<h1>你好</h1>
<h2>我不好</h2>
`
)
})
// 用服务器 实例的 .listen() 方法,可以启动当前的 web 服务器实例
server.listen(3000, () => {
console.log("端口正在运行")
})

我们可以将写入服务器要处理的事情,分为以下几个步骤

  1. 提供服务:对数据的服务
  2. 客户端发来请求
  3. 服务端接收请求
  4. 服务端处理请求
  5. 给个反馈(发送响应)
  6. 注册 require 的事件,当客户端请求过来时,自动执行这个回调函数
  7. 回调函数接收两个参数:request,response
  8. request:请求对象,可以获取请求过来的路径信息
  9. response:响应对象,可以给客户发送响应数据

响应式数据的content-type

向请求的客户端发送响应头, 该函数在一个请求内最多只能调用一次,如果不调用,则会自动生成一个默认的响应头, 因为在实际开发中,我们需要返回对应的数据以及对应的的文本格式 所以我们需要设置对应的响应头,响应头决定了对应的返回数据的格式以及编码格式,如果在使用该模块时,不写的话,会导致中文内容出现乱码

原因是在服务端默认发送的数据是 utf-8 格式的数据,但是在浏览器中不知道发送过来的数据是utf8的数据,所以会按照当操作系统的默认编码区解析,最终导致造成乱码,在中文操作系统中默认解析方式是 gbk,解决方案是告诉浏览器,发送的是什么类型的数据,是 utf-8 的数据,通过设置发送数据头来告诉浏览器,发送的数据格式是什么格式的数据

声明文本格式的数据 res.Header("Content-Type","text/plain; charset=utf-8")

声明 html 格式的数据 res.setHeader("Content-Type", "text/html; charset=utf-8")

该格式识别图片 res.Header(‘Content-Type’:‘image/jpg;charset=UTF8’)

该格式识别css res.Header(‘Content-Type’:‘text/css;charset=UTF8’)

该格式json res.setHeader('content-type', 'application/json;charset=utf-8')

该格式js res.setHeader('content-type', 'application/javascript;charset=utf8')

res.writeHead( )的使用方式和上面的类似,但我推荐这种的使用方式

1
response.writeHead(statusCode, [reasonPhrase], [headers])

接收参数:

  • 第一个是HTTP状态码,如200(请求成功),404(未找到)等
  • 第二个是告诉浏览器发送的是什么数据类型
  • 第三个就是具体发送的是什么数据
1
2
3
// 该格式可以识别HTML结构,编码格式是UTF-8

res.writeHead(200,{‘Content-Type’:‘text/html;charset=UTF8’});

我们举个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const http = require("http")
const server = http.createServer( )

server.on("request", (req, res)=> {
if (req.url === "/") {
// text/plain 文本格式的数据
res.setHeader("Content-Type","text/plain; charset=utf-8")
res.end("hello 世界")
} else if (req.url === "/html") {
// text/html html格式的数据
res.setHeader("Content-Type", "text/html; charset=utf-8")
res.end("<h2>这是html页面</h2>")
}
}).listen(3000)

既然都学到这了,那我就来点难的吧

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
28
29
30
31
32
//index.js
const http = require("http")
const server = http.createServer()

const demo = require("./demo")

server.on("request", (req, res) => {
res.setHeader("Content-Type", "text/html; charset=utf-8")
res.end(demo.htm(req.url))
}).listen(3000)
//demo.js
function htm(url) {
switch (url) {
case "/home":
return `<h1>姐就是女王,自信放光芒</h1>`
break
case "/list":
return `["demo1", "demo2", "demo3"]`
break
case "/obj":
return `{
name: "你好呀",
age: 38
}`
default:
return `<h1>404</h1>`
}
}

module.exports = {
htm,
}

这才刚刚开始而已,赶快进行下一个吧

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
let  http = require("http")

let server = http.createServer()

server.on("request", (request, response) => {
// 获取请求的路径
let url = request.url

// 可以根据不同的路径返回不同数据
// switch (url) {
// case "/":
// // response.write 写入响应数据
// response.write("hello nodejs");
// break;
// case "/login":
// response.write("login");
// break;
// case "/user":
// response.write("user");
// break;
// }
// 通知浏览器,结束会话
// response.end()


// 上面的写法也可以简写为如下格式
if(url = "/userlist"){
let userlist = [
{
name:"李四",
age:15
},
{
name:"张三",
age:16
}
]
// 直接用end函数,同时返回数据
response.end(JSON.stringify(userlist))
}
})


// 设置端口,开设3030端口,运行程序
server.listen(3030, () => {
console.log("服务启动成功,http://localhost:3030/");
})

url模块

这个模块主要是用来对网址进行处理和解析的实用工具

该模块提供了两种处理网址的API:一种是基于 node.js 特定的旧版的 API ,另一个是基于 WHATWG 网址标准 的新版的 API,也就是新老API之间的自家竞争

在讲模块方法之前,我们先来比较一下,这两者之间的区别

新老版本之间的解析网址字符串

WHATWG 的 API 比 传统 的根据安全性,所以我更推荐前者

使用 WHATWG API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const Url = new URL("https://user:pass@sub.example.com:8080/p/a/t/h?query=string#hash")
console.log(Url)
// 返回的数据
// URL {
// href: 'https://user:pass@sub.example.com:8080/p/a/t/h?query=string#hash',
// origin: 'https://sub.example.com:8080',
// protocol: 'https:',
// username: 'user',
// password: 'pass',
// host: 'sub.example.com:8080',
// hostname: 'sub.example.com',
// port: '8080',
// pathname: '/p/a/t/h',
// search: '?query=string',
// searchParams: URLSearchParams { 'query' => 'string' },
// hash: '#hash'
// }

使用旧版 API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const  Url = require("url")
const myurl = Url.parse("https://user:pass@sub.example.com:8080/p/a/t/h?query=string#hash")
console.log(myurl)
//返回的数据
// Url {
// protocol: 'https:',
// slashes: true,
// auth: 'user:pass',
// host: 'sub.example.com:8080',
// port: '8080',
// hostname: 'sub.example.com',
// hash: '#hash',
// search: '?query=string',
// query: 'query=string',
// pathname: '/p/a/t/h',
// path: '/p/a/t/h?query=string',
// href: 'https://user:pass@sub.example.com:8080/p/a/t/h?query=string#hash'
// }

WHATWG 网址 API

URL 类

new URL(input[, base])

这个是用于实例化 URL 对象的,也就是将传入的 URL 字符串解析成 URL 对象,所以连导入都不要导入,方便

参数

  • input:表示要解析的绝对或相对的 URL。如果 input 是相对路径,则必填 base。 如果 input 是绝对路径,则可以忽略 base
  • base:如果 input 不是绝对路径,则为要解析的基本 URL
1
2
const Url = new URL("https://user:pass@sub.example.com:8080/p/a/t/h?query=string#hash")
console.log(Url)

解析成功之后返回的URL对象,新旧方式返回的属性,略有不同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 // URL {
// href: 'https://user:pass@sub.example.com:8080/p/a/t/h?query=string#hash',
// origin: 'https://sub.example.com:8080',
// protocol: 'https:',
// username: 'user',
// password: 'pass',
// host: 'sub.example.com:8080',
// hostname: 'sub.example.com',
// port: '8080',
// pathname: '/p/a/t/h',
// search: '?query=string',
// searchParams: URLSearchParams { 'query' => 'string' },
// hash: '#hash'
// }

URL类中的属性

属性作用
hash获取及设置 URL 的片段部分。即 # 符号之后的内容
host获取及设置 URL 的主机部分
hostname获取及设置 URL 的主机名部分,不包含端口
href获取及设置序列化的 URL,整个URL 字符串
origin获取只读的序列化的 URL 的 origin
username获取及设置 URL 的用户名部分
password获取及设置 URL 的密码部分
pathname获取及设置 URL 的路径部分
port获取及设置 URL 的端口部分
protocol获取及设置 URL 的协议部分
search获取及设置 URL 的序列化查询部分
searchParams获取表示 URL 查询参数的 URLSearchParams 对象。该属性属于只读属性,但是可以通过 search 属性去修改

URL类中的方法

方法作用
url.toString()返回序列化的 URL,返回值与 url.href 和 url.toJSON() 的相同
url.toJSON()返回序列化的 URL。返回值与 url.href 和 url.toString() 的相同。使用JSON.stringify() 序列化时将自动调用该方法
1
2
3
4
5
6
const myurl = new URL("http://user:pass@host.com:8080/p/a/t/h?query=string#hash")
console.log(myurl.toString())
console.log(myurl.toJSON())
//终端控制台显示
// http://user:pass@host.com:8080/p/a/t/h?query=string#hash
// http://user:pass@host.com:8080/p/a/t/h?query=string#hash

URLSearchParams类

URLSearchParams 类提供对 URL 查询部分的读写权限。这个类与 querystring 模块有相似的目的。这个类是专门为 URL 查询字符串而设计的

到坚持到这了,嘻嘻,加油

构造函数

这个类有四个构造函数,用于不同的情况

构造函数作用
new URLSearchParams()实例化一个新的空的 URLSearchParams 对象
new URLSearchParams(string)将 string 解析成一个查询字符串, 并且使用它来实例化一个新的 URLSearchParams 对象。 如果以 ‘?’ 开头,则忽略
new URLSearchParams(obj)通过使用查询 哈希映射 实例化一个新的 URLSearchParams 对象。 obj 的每一个属性的键和值都将被强制转换为字符串
new URLSearchParams(iterable)以一种类似于 Map 的构造函数的迭代映射方式实例化一个新的 URLSearchParams 对象

直接通过 URL 类的 searchParams 属性实例化 URLSearchParams 类

1
2
const myurl = new URL("http://user:pass@host.com:8080/p/a/t/h?query=string#hash")
console.log(myurl.searchParams.get("query"))//string

直接通过 URLSearchParams 类的构造函数实例化

1
2
3
4
5
const myurl = new URL("http://user:pass@host.com:8080/p/a/t/h?query=string#hash")
const search = new URLSearchParams(myurl.searchParams)
console.log(search) //URLSearchParams { 'query' => 'string' }
//等价于
// const search = new URLSearchParams(myurl.search)

方法

方法作用
append(name, value)在查询字符串中附加一个新的键值对
delete(name)删除所有键为name的键值对
entries()在查询中的每个键值对上返回一个迭代器
forEach(fn)在查询字符串中迭代每个键值对,并调用给定的函数
get(name)返回键是name的第一个键值对的值。如果没有对应的键值对,则返回null
getAll(name)返回键是name的所有键值对的值,如果没有满足条件的键值对,则返回一个空的数组
has(name)如果存在至少一对键是 name 的键值对则返回 true
keys()返回每一个键值对上的键
set(name, value)与 name 相对应的值设置为 value。如果已经存在键为 name 的键值对,则将第一对的值设为 value 并且删除其他对。 如果不存在,则将此键值对附加在查询字符串后
sort()按现有名称就地排列所有的名称-值对
toString()返回查询参数序列化后的字符串
values()返回每一个键值对上返回一个值
urlSearchParams[Symbol.iterator]()返回一个键值对形式迭代器

我们挑一些方法来用,>0<

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
const myurl = new URL("http://user:pass@host.com:8080/p/a/t/h?query=string#hash")
const search = new URLSearchParams(myurl.searchParams)

search.append("chen", "陈")
search.append("ruo", "若")
search.append("query", "da")
console.log(search)
// URLSearchParams { 'query' => 'string', 'chen' => '陈', 'ruo' => '若', 'query' => 'da' }


search.delete("query")
console.log(search)
// URLSearchParams { 'chen' => '陈', 'ruo' => '若' }


search.entries()
console.log(search)
// URLSearchParams { 'chen' => '陈', 'ruo' => '若' }


search.forEach((value, name, searchParams) => {
// 通过 forEach() 方法循环得到键值对时,fn方法会返回三个参数,分别是:键、值、所有的键值对
console.log(name, value, searchParams)
// chen 陈 URLSearchParams { 'chen' => '陈', 'ruo' => '若' }
// ruo 若 URLSearchParams { 'chen' => '陈', 'ruo' => '若' }
})


console.log(search.get("chen"))
// 陈
console.log(search.get("c"))
// null


console.log(search.getAll("chen"))
// [ '陈' ]
console.log(search.getAll("c"))
// []


console.log(search.has("chen"))
//true
console.log(search.has("c"))
//false


console.log(search.keys())
//URLSearchParams Iterator { 'chen', 'ruo' }


search.set("wang", "王")
search.set("si", "思")
console.log(search.toString())
//chen=%E9%99%88&ruo=%E8%8B%A5&wang=%E7%8E%8B&si=%E6%80%9D


search.sort()
console.log(search.toString())
// chen=%E9%99%88&ruo=%E8%8B%A5&si=%E6%80%9D&wang=%E7%8E%8B


console.log(search.values())
// URLSearchParams Iterator { '陈', '若', '思', '王' }

for (let [key, value] of search) {
console.log(key + ":" + value)
}
// chen:陈
// ruo:若
// si:思
// wang:王

http模块补充

补充主要讲的是跨域

GET

我们抓取b站热门的数据

思路很简单,就是前端向node发送ajax请求,node客户端向b站抓取数据,然后返回node客户端 再转发到前端,很简单吧,我怕讲复杂了,你们不懂

话不多说,直接上代码

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
//index.html
fetch("http://localhost:3000/api")
.then(res => res.json())
.then(res => {
//data 的数组
console.log(res.data.list)

for (let a in res.data.list) {
//data 数据 每个遍历了一遍
console.log(res.data.list[a])
}
})

//index.js
const http = require("http")
const https = require("https")
const server = http.createServer()

server.on("request", (req, res) => {
let urlobj = req.url
//改请求头信息
res.writeHead(200, {
"Content-Type": 'application/json;charset=utf-8',
//允许跨域
"access-control-allow-origin": "*"
})
switch (urlobj) {
case "/api":
bzhanget((data) => {
res.end(data)
})
break
default:
res.end("404")
}
}).listen(3000)
function bzhanget(a) {
let jihe = ""
//由于b站是http安全协议,所以使用https
https.get("https://api.bilibili.com/x/web-interface/popular?ps=20&pn=1",
(res) => {
//遍历
res.on("data", (svg) => {
jihe += svg
})
//遍历结束
res.on("end", () => {
a(jihe)
})
})
}

POST

我们来抓取 小米有品 的数据,试试 POST

整体思路和上面的一模一样

话不多说,直接上代码

如果 https.request() 又不懂的话 ,可看 https 安全超文本传输协议

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
//index.html
fetch("http://localhost:3000/api")
.then(res => res.json())
.then(res => {
//data 的数组
console.log(res.data.list)

for (let a in res.data.list) {
//data 数据 每个遍历了一遍
console.log(res.data.list[a])
}
})

//index.js
const http = require("http")
const https = require("https")
const server = http.createServer()

server.on("request", (req, res) => {
let urlobj = req.url
res.writeHead(200, {
"Content-Type": 'application/json;charset=utf-8',
"access-control-allow-origin": "*"
})
switch (urlobj) {
case "/api":
bzhanpost((data) => {
res.end(data)
})
break
default:
res.end("404")
}
}).listen(3000)


function bzhanpost(a) {
let jihe = ""
let options = {
hostname: "m.xiaomiyoupin.com",
port: "443",
path: "/mtop/market/search/placeHolder",
method: "POST",
headers: {
"Content-Type": 'application/json;charset=utf-8'
}
}
let req = https.request(options,
(res) => {
res.on("data", (svg) => {
jihe += svg
})
res.on("end", () => {
a(jihe)
})
})
//设置请求负载
req.write(JSON.stringify([{},{"baseParam":{"ypClient":1}}]))

req.end()
}

events模块

这是典型的异步发布模式,由于 node.js 中不存在浏览器中冒泡,捕获这些行为,但 node.js 中实现了 events 模块,而且几乎所有常用的node模块都继承了events模块

而 events 只对外暴露一个对象,那就是 EventEmitter,而它只有两个作用,一个是事件的发射,一个是事件的监听

监听器函数(listeners) 可以添加给对象,对象发出事件时,对应的函数就会被执行。在监听器函数中,this引用的是它(监听器函数),但不要用箭头函数,这是人话吗

所有发出事件的对象都是 events.EventEmitter 的实例,万恶之源了属于是,我们可以通过 require(‘events’).EventEmitter 直接得到 EventEmitter 类,也可以先创建 require(‘events’) 的变量,再用这个变量创建 EventEmitter 实例 ,当 EventEmitter 对象遇到错误时,通常会触发 error 事件

什么是事件驱动

简单来说就是通过有效的方法来监听事件状态的变化,并在发生变化时做出相应的动作

添加监听器

为事件绑定事件处理程序,可以用 emitter.addListener(event,listener)emitter.on(event,listener),它们作用完全一样,我推荐第二种方法

传入参数是事件(event)和处理函数(listener)

我们举个例子

1
2
3
4
5
6
7
8
const http = require("http")
const server = http.createServer()
server.on("request",(req,res)=>{
res.writeHead(200,{"Content-Type":"text/plain"})
res.write("shiyishi")
console.log("shiyishi")
res.end()
}).listen(3000)

众所周知,既然有监听器,那我就多讲它一点吧

只执行一次的监听器

使用 **emitter.once(event,listener)** 绑定的事件监听器只执行一次,然后就会被删除掉

这是一次性的,保护环境,节约资源,满分,没错我说的是反话

我们举个例子

1
2
3
4
5
6
7
8
const http = require("http")
const server = http.createServer()
server.once("request",(req,res)=>{
res.writeHead(200,{"Content-Type":"text/plain"})
res.write("shiyishi")
console.log("shiyishi")
res.end()
}).listen(3000)

现在轮到清除监听器了,说好听点叫移除,:( bushi

移除监听器

移除监听器使用 emitter.removeListener(event,listener)

该函数要放在要移除监听器的后面,不然清不掉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const http = require("http")
const server = http.createServer()
function demo(req, res) {
res.writeHead(200, { "Content-Type": "text/plain" })
res.write("shiyishi")
console.log("shiyishi")
res.end()
}
server.on("request", demo)
//移除绑定的监听器 demo
server.removeListener("request", demo)
//移除后再绑定一个监听器
server.on("request", (req, res) => {
res.writeHead(200, { "Content-Type": "text/plain" })
res.write("shiyishi")
console.log("shiyishi")
res.end()
})
server.listen(3000)

然而 错误的移除事件 不能移除匿名的事件处理程序,必须像上面一样移除具名函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const http = require("http")
const server = http.createServer()

server.on("request", (req, res) => {
res.writeHead(200, { "Content-Type": "text/plain" })
res.write("shiyishi")
console.log("shiyishi")
res.end()
})
//错误的移除事件方法
server.removeListener("request", (req, res) => {
res.writeHead(200, { "Content-Type": "text/plain" })
res.write("shiyishi")
console.log("shiyishi")
res.end()
})

server.listen(3000)

移除一个不过瘾,是不是,要移就将所有的监听器全部移除,玩的就是一个清净,人间清醒

移除所有监听器

移除所有监听器使用 emitter.removeAllListener([event])

需要传入某种类型的事件参数,不传的话会把所有类型的事件监听都移除掉,玩的就是西海岸哈哈哈哈

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
const http = require("http")
const server = http.createServer()

server.on("request", (req, res) => {
res.writeHead(200, { "Content-Type": "text/plain" })
res.write("shiyishi")
console.log("shiyishi")
res.end()
})
server.on("request",(req,res)=>{
res.writeHead(200, { "Content-Type": "text/plain" })
res.write("hahhahha")
console.log("hahhahahhah")
res.end()
})
//移除绑定的所有监听器
server.removeAllListeners("request")
//移除后再绑定一个监听器
server.on("request",(req,res)=>{
res.writeHead(200, { "Content-Type": "text/plain" })
res.write("chenruo")
console.log("chenruoha")
res.end()
})
server.listen(3000)

设置监听器最大绑定数

EventEmitter 支持多个事件监听器,默认最大值是10。也就是说可以在某个事件添加10个监听函数,默认情况下,超过10个就会发出警告提示

你想的没错有反转,是可以自定义的,嘿嘿嘿

可以通过 emitter.setMaxLisstener(n) 来设置同一事件的监听器最大绑定数,如果设置为0时,也就是无限制

自定义事件

从此开启自定义时代,人人皆是开发者

我们可以使用 emitter.emit(event,[arg1],[arg2],[...]) 触发自定义的事件,放心没有忌口

1
2
3
4
5
6
7
8
const http = require("http")
const server = http.createServer()
//绑定自定义事件 demo
server.on("demo", function (data) {
console.log(data)
})
//触发自定义事件
server.emit("demo", "这只是一个试样而已")

查看事件绑定的监听器的数量

如果我们想查看事件绑定的监听器的数量呢

没错我们有现成的方法,哈哈哈

可以使用 EventEmitter.listenerCount(emitter,event) 查看事件监听器的数量,也可以用 emitter.listeners('event').length 来查看哦

推荐第二个,第一个用点老了,不好用,旧的不去,新的不来,(bushi

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
const http = require("http")
const server = http.createServer()
const event = require("events") //加载events模块

server.on("request", (req, res) => {
res.writeHead(200, { "Content-Type": "text/plain" })
res.write("1111")
console.log("1111")
res.end()
})
server.on("request", (req, res) => {
res.writeHead(200, { "Content-Type": "text/plain" })
res.write("2222")
console.log("2222")
res.end()
})
server.listen(3000)

console.log("开始了")

const event1 = event.EventEmitter.listenerCount(server, "request")
const event2 = server.listeners("request").length

console.log(event1)
console.log(event2)

关于排错

我们用一个很简单的例子来理解吧

1
2
3
4
5
6
7
8
9
10
11
12
server.on("request", (req, res) => {
res.writeHead(200, { "Content-Type": "text/plain" })
res.write("1111")
console.log("1111")
// res.end()
})
server.on("request", (req, res) => {
res.writeHead(200, { "Content-Type": "text/plain" })
res.write("2222")
console.log("2222")
res.end()
})

第一个在 res.write() 后调用了 res.end() 结束了写入流,所以再次调用 res.write(‘2222’) 就会报错,可以注释掉第一个 res.end() 调用,就可以正常使用了

我们一不做而不息,我们来看看这个模块的设置吧

查询已注册监听器的事件名称

如果我们想知道已注册监听器的事件的名称,以数组的形式返回

我们可以使用 emitter.eventNames() 方法来调用它

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const event = require("events")
const eventdemo = new event()

eventdemo.on("demo1", () => {
console.log("你好啊")
})
eventdemo.on("demo2", () => {
console.log("我不好")
})
eventdemo.on("demo3", () => {
console.log("你好的很")
})

console.log(eventdemo.eventNames())
//[ 'demo1', 'demo2', 'demo3' ]

关于这个模块已经讲的差不多了,我不想再讲了

在讲 fs 模块之前,我们先讲一下 Buffer 缓冲区这个概念,有助于理解 fs 模块

Buffer缓冲区

什么是 Buffer 缓冲区

在 node.js 应用中,需要处理网络协议、操作数据库、处理图片、接收上传文件等,在网络流和文件的操作中,要处理大量的二进制数据,而 Buffer 就是在内存中开辟一片区域(初次初始化为8KB),用来存放二进制数据,可以理解成就是一个临时的容器

如果数据到达的速度比进程消耗的速度快,那么少数早到达的数据会处于等待区等候被处理。反之,如果数据到达的速度比进程消耗的数据慢,那么早先到达的数据需要等待一定量的数据到达之后才能被处理

这里的等待区也就指的是缓冲区,通常位于计算机的 RAM 中

简单来讲,node.js 不能控制数据传输的速度和到达时间,但可以决定何时发送数据,如果还没到发送时间,则将数据放在 Buffer 中,也就是在RAM中,直至将它们发送完毕

特征

  • Buffer 的结构与数组类似,操作方法也与数组类似
  • 数组不能存储二进制文件,而 Buffer 是专门存储二进制数据的存在
  • Buffer 存储的是二进制数据,显示是以 16 进制的形式呈现
  • Buffer 每一个元素范围是 00ff,即 0255、00000000~11111111
  • 每一个元素占用一个字节内存
  • Buffer 是对底层内存的直接操作,因此大小一旦确定就不能修改

使用方式

Buffer 类在全局作用域中,无须 require 导入哦

创建 Buffer 的方法有很多种,我们讲讲下面的几种常见的形式:

方法作用
Buffer.from(str[, encoding])将一个字符串转换为 Buffer
Buffer.alloc(size)创建指定大小的 Buffer
Buffer.alloUnsafe(size)创建指定大小的 Buffer,可能包含敏感数据(分配内存时不会清除内存残留的数据)
buf.toString()将 Buffer 数据转为字符串

Buffer.from()

1
2
3
4
5
6
7
8
let ba = Buffer.from("10")
let bb = Buffer.from("10", "utf-8")
let bc = Buffer.from([10])
let bd = Buffer.from(bc)
console.log(ba)//<Buffer 31 30>
console.log(bb)//<Buffer 31 30>
console.log(bc)//<Buffer 0a>
console.log(bd)//<Buffer 0a>

Buffer.alloc()

1
2
3
4
const bAlloc1 = Buffer.alloc(10)// 创建一个大小为 10 个字节的缓冲区
const bAlloc2 = Buffer.alloc(10, 1) // 建一个长度为 10 的 Buffer,其中全部填充了值为 `1` 的字节
console.log(bAlloc1) // <Buffer 00 00 00 00 00 00 00 00 00 00>
console.log(bAlloc2)// <Buffer 01 01 01 01 01 01 01 01 01 01>

在上面创建buffer后,则能够以 toString() 的形式进行交互,默认情况下采取 utf8 字符编码形式

1
2
3
4
5
6
const buffer = Buffer.from("你好")
console.log(buffer)
// <Buffer e4 bd a0 e5 a5 bd>
const str = buffer.toString()
console.log(str)
// 你好

如果编码与解码不是相同的格式则会出现乱码的情况

1
2
3
4
5
6
7
const buffer = Buffer.from("你好","utf-8 ")
console.log(buffer)
// <Buffer e4 bd a0 e5 a5 bd>
const str = buffer.toString("ascii")
console.log(str);
// d= e%=

当设定的范围导致字符串被截断的时候,也会存在乱码情况

1
2
3
4
5
6
7
8
9
10
11
12
const buf = Buffer.from('Node.js 技术栈', 'UTF-8')

console.log(buf)
// <Buffer 4e 6f 64 65 2e 6a 73 20 e6 8a 80 e6 9c af e6 a0 88>
console.log(buf.length)
// 17

console.log(buf.toString('UTF-8', 0, 9))
// Node.js �
console.log(buf.toString('UTF-8', 0, 11))
// Node.js 技

应用场景

及然知道怎么用了,那我们就要知道在那个时候会用到它吧

主要的应用场景在

  • I/O操作
  • 加密解密
  • zlib.js

I/O操作

通过流的形式,将一个文件的内容读取到另外一个文件

1
2
3
4
5
6
7
const fs = require('fs')

const inputStream = fs.createReadStream('input.txt') // 创建可读流
const outputStream = fs.createWriteStream('output.txt');// 创建可写流

inputStream.pipe(outputStream);// 管道读写

加解密

在一些加解密算法中会遇到使用 Buffer,例如 crypto.createCipheriv( ) 的第二个参数 key 为 string 或 Buffer 类型

zlib.js

zlib.js 为 node.js 的核心库之一,其利用了缓冲区(Buffer)的功能来操作二进制数据流,提供了压缩或解压功能

剩下的内容,就请勇士你自己去探索吧,一起冒险吧

fs模块

我现在不就开始讲了吗

node.js 文件系统(fs模块)中的方法均有异步同步两个版本,例如 读取文件内容的函数有异步的 fs.readFile() 和同步的 fs.readFileSync()
异步的方法函数最后一个参数为回调函数,回调函数的第一个 参数包含了错误信息(error)
建议使用异步方法,比起同步,异步方法性能更高,速度更快,而且没有阻塞,实际上用异步的情况也比较多

说白了,就是一个管理文件的模块而已>0>

我在下面只介绍一些常用的API

文件

我们先从如何操控文件开始吧

打开文件

异步模式下打开文件的语法

1
fs.open(path, flags[, mode], callback)

path 文件的路径;

flags 文件打开的行为;

mode 设置文件模式(权限),文件创建默认的权限为 0666(可读可写);

callback 回调函数,两个参数 callback(err, fd)

flags 参数可以是以下值

我们举个例子

1
2
3
4
5
6
7
8
9
10
let fs = require("fs")

fs.open("text.txt", "r+", (err, data) => {
if (err) {
throw err
}
else {
console.log("调用成功")
}
})

获取文件信息

异步模式获取文件大小、时间等信息的语法

1
fs.stat(path, callback)

path 文件路径;

callback 回调函数,带有两个参数 (err, stats) ,stats 是 fs.tats 对象;
fs.stat(path) 执行后,会将 stats 类的实例返回给其回调函数,通过 stats 类中的提供方法判断文件的相关属性;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let fs = require("fs")

fs.stat("./text.txt", (err, stats) => {
if (err) throw err
// 返回 fs.Stats 对象
console.log(stats)
// 检查是否是文件
console.log("isFile:" + stats.isFile())
// 检查是否是文档
console.log("isDirectory:" + stats.isDirectory())
if (stats.isFile()) {
// 检查文件的大小 字节为单位
console.log("size:", stats.size)
// 文件的创建时间
console.log("birth time:", stats.birthtime)
// 文件更改时间
console.log("modified time:" + stats.mtime)
}
})

stats类中的方法有

最常用的也就前两个了

方法作用
stats.isFile()如果是文件返回 true,否则返回 false
stats.isDirectory()如果是目录返回 true,否则返回 false
stats.isBlockDevice()如果是块设备返回 true,否则返回 false
stats.isCharacterDevice()如果是字符设备返回 true,否则返回 false
stats.isSymbolicLink()如果是软链接返回 true,否则返回 false
stats.isFIFO()如果是FIFO,返回true,否则返回 false。FIFO是UNIX中的一种特殊类型的命令管道
stats.isSocket()如果是 Socket 返回 true,否则返回 false

写入文件内容

如果目标文件不存在, node.js 会自动创建该文件然后进行写入

覆盖式写入

异步模式下写入文件的语法

1
fs.writeFile(file, data[, options], callback)

fs.writeFile() 直接打开文件默认是 w 模式,所以如果文件存在,该方法写入的内容会覆盖旧的文件内容,也可以改变默认模式
file 文件名路径或文件描述符

data 要写入文件的数据,可以是 String 字符串或 Buffer 缓冲对象

options 参数是一个对象 {encoding, mode, flag},默认编码 utf8,模式0666,flag为 w

实例

1
2
3
4
5
let fs = require("fs")
fs.writeFile("./text.txt","你好呀",(err)=>{
if(err) throw err
console.log("成功")
})
追加式写入

异步模式下写入文件的语法

1
fs.appendFile(file, data[, options], callback)

和上面类似,但是在文件原内容的基础上,添加内容数据,注意是在原内容的后面添加的

实例

1
2
3
4
5
let fs = require("fs")
fs.appendFile("./text.txt","你好呀",(err)=>{
if(err) throw err
console.log("成功")
})

读取文件内容

简单文件读取
1
fs.readFile(path[, options], callback)

path:文件路径

options:配置选项,若是字符串则指定编码格式

  • encoding:编码格式
  • flag:打开方式

callback:回调函数

  • err:错误信息
  • data:读取的数据,如果未指定编码格式则返回一个 Buffer
1
2
3
4
5
6
let fs = require("fs")

fs.readFile("./text.txt", "utf-8", (err, data) => {
if (err) throw err
console.log(data)
})
常用文件方式读取

异步模式下读取文件的语法,该方法使用了文件描述符来读取文件

1
fs.read(fd, buffer, offset, length, position, callback)

fd 通过 fs.open() 方法返回的文件描述符;

buffer 数据写入的缓冲区;

length 要从文件中读取的字节数;

position 文件读取的起始位置;

callback 回调函数,三个参数,err 错误信息,bytesRead 读取的字节数,buffer 缓冲区对象

1
2
3
4
5
6
7
8
9
10
11
12
let fs = require("fs")
fs.open("./text.txt", "r+", (err, fd) => {
if (err) throw err
let buf = new Buffer.alloc(1024)
fs.read(fd, buf, 0, buf.length, 0, (err, data) => {
if (err) throw err
console.log(data + "字节")
// 仅输出读取的字节
if (data > 0)
console.log(buf.slice(0, data).toString())
})
})
流式文件读取
  • 简单文件读取的方式会一次性读取文件内容到内存中,但文件较大时,会占用过多内存影响系统性能,且读取速度慢
  • 大文件适合用流式文件读取,它会分多次将文件读取到内存中
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
let fs = require("fs")
// 创建一个可读流
let rs = fs.createReadStream("./text.txt")
// 创建一个可写流
let ws = fs.createWriteStream("./demo.js")
// 监听流的开启和关闭
// 这几个监听不是必须的
rs.once("open", () => {
console.log("可读流打开")
})
rs.once("close", () => {
console.log("可读流关闭")
//数据读取完毕,关闭可写流
ws.end()
})
ws.once("open", () => {
console.log("可写流打开")
})
ws.once("close", () => {
console.log("可写流关闭")
})
//要读取一个可读流中的数据,要为可读流绑定一个data事件,data事件绑定完毕自动开始读取数据
rs.once("data", (data) => {
console.log(data)
//将读取到的数据写入到可写流中
ws.write(data)
})

简洁的写法

1
2
3
4
5
6
7
let fs = require("fs")
// 创建一个可读流
let rs = fs.createReadStream("./text.txt")
// 创建一个可写流
let ws = fs.createWriteStream("./demo.js")
// pipe()可以将可读流中的内容,直接输出到可写流中
rs.pipe(ws)

关闭文件

异步模式下关闭文件的语法,使用了文件描述符来读取文件

1
fs.close(fd, callback)

fd 通过 fs.open() 方法返回的文件描述符;

callback 回调函数,没有参数;

1
2
3
4
5
6
7
8
let fs = require("fs")
fs.open("./text.txt",(err,data)=>{
if(err) throw err
//关闭文件
fs.close(data,()=>{
console.log("关闭成功")
})
})

截取文件

异步模式下截取文件的语法格式,使用文件描述符来读取文件

1
fs.ftruncate(fd, len, callback)

fd 通过 fs.open() 方法返回的文件描述符;

len 文件内容截取的长度;

callback 回调函数,没有参数;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let fs = require("fs")
fs.open("./text.txt", "r+", (err, fd) => {
if (err) throw err
fs.ftruncate(fd, 5, err => {
if (err) throw err
let buf = new Buffer.alloc(1024)
fs.read(fd, buf, 0, buf.length, 0, (err, data) => {
if (err) throw err
console.log(data + "字节")
// 仅输出读取的字节
if (data > 0){
console.log(buf.slice(0, data).toString())
}
fs.close(fd,err=>{
console.log("关闭")
})
})
})
})

删除文件

1
fs.unlink(path, callback)

path 文件路径;

callback 回调函数,没有参数;

1
2
3
4
5
let fs = require("fs")
fs.unlink("./text.txt",err=>{
if(err)throw err
console.log("删除成功")
})

检查文件是否存在

如果只是为了检查文件是否存在,但没有更多的操作,建议使用 fs.access()

不建议在调用 fs.open() 、fs.readFile()、fs.writeFile() 之前使用 fs.stat() 检查文件的存在性,而是应该直接地打开、读取或写入文件
不建议在调用 fs.open() 、fs.readFile()、fs.writeFile() 之前使用 fs.access() 检查文件的可访问性,这样做会引入竞态条件,因为其他进程可能会在两个调用之间更改文件的状态,而是应该直接地打开、读取或写入文件

1
2
3
4
let fs = require("fs")
fs.access("./text.txt", fs.constants.F_OK, err => {
console.log(`结果:${err ? `不存在` : `存在`}`)
})

重命名文件

1
fs.rename(oldPath, newPath, callback)

异步的把 oldPath 文件重命名为 newPath 提供的路径名,如果 newPath 已存在,则覆盖它

1
2
3
4
5
6
let fs = require("fs")

fs.rename("./text.txt", "demo.txt", err => {
if (err) throw err
console.log("重命名成功")
})

目录

我们现在开始讲文件目录相关的操作

创建目录

1
fs.mkdir(path[, options], callback)

path 文件路径;

options 参数一 recursive 是否以递归的方式创建目录,默认是 false,参数二 mode 设置目录权限,默认为 0777;

callback 回调函数,没有参数;

1
2
3
4
5
6
7
let fs = require("fs")

// dir 目录必须存在
fs.mkdir('dir/dir2', err => {
if (err) throw err
console.log('创建目录成功')
})
1
2
3
4
5
6
7
let fs = require("fs")

// 不管创建的目录 dir 和 dir/dir1 是否存在,也就是说不管文件的上层目录是否存在,而创建,上面就相反了
fs.mkdir('dir/dir1', { recursive: true }, err => {
if (err) throw err
console.log('创建目录成功')
})

读取目录

1
fs.readdir(path, callback)

path 文件路径;

callback 回调函数,回调函数有两个参数 err, files err 为错误信息, files 为目录下的文件数组列表;

1
2
3
4
5
6
let fs = require("fs")

fs.readdir("demo", "utf-8", (err, data) => {
if (err) throw err
console.log(data)
})

删除目录

1
fs.rmdir(path, callback)

path 文件路径;

callback 回调函数,没有参数

1
2
3
4
5
6
7
8
9
let fs = require("fs")

fs.rmdir("demo/demo1",err=>{
if(err) throw err
fs.readdir("demo",(err,data)=>{
if(err) throw err
console.log(data)
})
})

关于fs模块,我就讲到这了,下面的就靠你自己了,冲冲冲

路径动态拼接问题 __dirname

我们在使用 fs 模块操作文件时,如果提供的操作路径是以 ./../ 开头的相对路径时,容易出现路径动态拼接错误的问题

原因:代码在运行的时候,会以执行 node.js 命令时所处的目录,动态拼接出被操作文件的完整路径

解决方案:在使用 fs 模块操作文件时,直接提供完整的路径,从而防止路径动态拼接的问题

__dirname 是用来提供动态的获取当前文件所属目录的绝对路径

1
2
3
fs.readFile(__dirname + '/files/1.txt', 'utf8', function(err, data) {
...
})

zlib模块

这个模块可以很方便的实现文件的压缩和解压,常用于服务端的 gzip 压缩,用来提高网页加载速度,也就是压缩原有文件体积,加快文件访问速度

浏览器通过 HTTP 请求头部里加上 Accept-Encoding ,告诉服务器,“你可以用 gzip,或者 defalte 算法来进行压缩资源”

这个模块,我们只需要知道,因为我们以后常用的还是第三方模块来压缩文件和解压,所以只需了解即可

文件的压缩和解压的实现

我们简单来对文件的压缩和解压进行的实现和了解

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
28
29
30
31
32
33
34
const zlib = require("zlib")
const { createReadStream, createWriteStream } = require("fs")
const { pipeline } = require("stream")

// 压缩过程中错误捕获方法
const onerror = err => {
if (err) {
console.log("出错了" + err)
process.exitCode = 1
}
}
//压缩或者解压方法 type值为zip执行压缩方法,type值为ungzip执行解压缩方法
function zipfunc(source, destination, type) {
const gzip = zlib.createGzip()
const ungzip = zlib.createGunzip()
switch (type) {
case "zip":
return pipeline(source, gzip, destination.onerror)
case "ungzip":
return source.pipe(ungzip).pipe(destination)
default:
return pipeline(source, gzip, destination, onerror)
}
}

//压缩
const source = createReadStream("./demo.txt")
const destination = createWriteStream("./txt.gz")
zipfunc(source, destination, "zip")

//解压
const sources = createReadStream("./txt.gz")
const destinations = createWriteStream("./demo.txt")
zipfunc(sources, destinations, "ungzip")

在执行压缩操作时,会生成txt.gz文件,在执行解压操作时,会生成demo,txt文件

我顺便讲一下,这里的相关知识点,虽然偏题但有点重要

pipline

stream.pipeline() 方法,是用来在流和生成器之间进行管道转发错误并且正确清理并在管道完成时提供回调

很难理解是不是,熬一熬就懂了,慢慢来

管道是一种机制,也就是将一个流的输出作为另一流的输入。它通常用于从一个流中获取数据并将该流的输出传递到另外的流。管道操作没有限制,换句话说,管道就是用于分步骤处理流的数据,也就是程序流

在进行文件压缩的时候使用 stream.pipeline() 提供一个可以完成数据流处理的管道,管道内可以传输多个流,管道任务结束后提供回调,简直美滋滋

语法

1
stream.pipeline(source[, ...transforms], destination, callback)

参数

  • source:可读流
  • ...tranforms:双工流(同时实现 Readable 和 Writable 接口的流)
  • destination:可写流
  • callback:管道完成时的回调

pipe

readable.pipe() 方法,可以将可写流绑定到可读流中,使其自动切换到流动模式并将其所有数据推送到绑定的可写流。 也就是说,pipe 方法的主要用途是从可读流中读取数据写入可写流,做的是流类型转换的事情

语法

1
readable.pipe(destination[, options])

可以看官方的示例,简单易懂,将 readable 中的所有数据通过管道传输到名为 file.txt 的文件中

1
2
3
4
5
const fs = require('fs')
const readable = getReadableStreamSomehow()
const writable = fs.createWriteStream('file.txt')
// 可读流的所有数据进入 'file.txt'
readable.pipe(writable)

也可以将多个 Writable 流绑定到单个 Readable 流

readable.pipe() 方法返回对目标流的引用,从而可以建立管道流链

1
2
3
4
5
const fs = require('fs')
const r = fs.createReadStream('file.txt')
const z = zlib.createGzip()
const w = fs.createWriteStream('file.txt.gz')
r.pipe(z).pipe(w)

什么是stream流

到底什么是流

我们现在为上面的内容来填坑

流是用于在 node.js 中处理流数据的抽象接口, stream 模块提供了用于实现流接口的 API

流可以是可读的、可写的、或两者兼而有之,所有的流都是 EventEmitter 的实例,它才是老大

stream(流)是一种抽象的数据结构。就像数组或字符串一样,流是一堆数据的集合

但不同的是,流可以每次输出少量的数据,而且它不用存在于内存之中

比如,对服务器发起 http 请求的 request/response 对象就是 Stream流

使用流可以将文件资源拆分成小块进行处理,资源就像水流一样进行传输,减轻服务器压力,我直呼高啊

这样就是我为什么在写fs的时候,写了流式文件读取,这叫什么,这叫预判先知,这波大气层

两个流用一个管道相连,stream1 的末尾可以连接上 stream2 的开端

我的重点目标不过是你的起点目标而已,泪目

只要 stream1 有数据,就会流到 stream2

1
2
let demo = fs.createReadStream("./demo.txt")
demo.pipe(res)

demo是一个文件流,下面的 demo 就是我们的 http 流 res。 本来这两个流压根是没有关系的,现在我们想把文件流的数据传递给 http 流中。 很简单,用 pipe 连接就行啦,促成良缘,哈哈哈

管道原理

管道也可以认为是两个事件的封装

  • 监听 data 事件,当 stream1 一有数据就塞给 stream2
  • 监听 end 事件,当 stream1 停了,就停掉 stream2
1
2
3
4
5
6
stream1.on("data",(chunk)=>{
stream2.write(chunk)
})
stream1.on("end",()=>{
stream2.end()
})

都谈到这了,你们肯定没意见多看一会,对吧

Stream 分类
名称特点
Readable可读
Writable可写
Duplex可读可写(双向)
Transform可读可写(变化)

我们对可读和可写很好理解,但双向的怎么理解呢

Duplex 可以读写,但是读的内容和写的内容是相互独立的,没有交叉,读写的文件相互分离,而 Transform 是自己写自己读,读写的文件是一体的,很好理解吧

可读流有两种状态

  • 静止态 paused
  • 流动态 flowing

我们可以将可读流看成一家实体企业,节假日停止运作时就是静态的,工作日运作时就是流动态

  • 可读流默认是处于 paused 态的
  • 一旦添加 data 事件监听,它就变为 flowing 态
  • 删掉 data 事件监听,又会变成 paused 态
  • 而使用 pause() 可以将它变为 paused 态
  • 而使用 resume() 可以将它变为 flowing 态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const http = require("http")
const server = http.createServer()
const fs = require("fs")

server.on("request",(req,res)=>{
//默认 静止态
let demo = fs.createReadStream("./demo.txt")
//变为 流动态
demo.pipe(res)
//变为 静止态
demo.pause()
setInterval(()=>{
//恢复 流动态
demo.resume()
},1000)
})
finish事件

在调用 stream.end() 之后,而且缓冲区数据都已经传给底层系统之后,触发 finish 事件

我们往文件中写入数据时,不是直接存入硬盘中,而是先放入缓冲区。 当数据到达一定大小后,才会写入硬盘,我们不生产水我们只是大自然的搬运工

点到为止,我们要讲武德哈

node.js 中的 stream

可读流VS可写流

可读流可写流
HTTP Response 客户端HTTP Request 客户端
HTTP Request 服务端HTTP Response 服务端
fs read streamfs write stream
zlib streamzlib stream
TCP socketsTCP sockets
child process stdout & stderrchild process stdin
process.stdinprocess.stdout,process.stderr

你是不是有点看不懂,是不是,其实我也有点哈

压缩

使用 gzip 算法

gzip 是一种数据格式,默认且目前仅使用deflate算法压缩data部分

1
2
3
4
5
6
7
8
let fs = require("fs")
let zlib = require("zlib")

let gzip = zlib.createGzip()

let rs = fs.createReadStream("./demo.txt")
let ws = fs.createWriteStream("./text.txt")
rs.pipe(gzip).pipe(ws)

使用 deflate 算法

deflate 是同时使用了 LZ77算法 与 哈夫曼编码(Huffman Coding) 的一个无损数据压缩算法

Brotli 通过变种的 LZ77 算法、Huffman 编码以及二阶文本建模等方式进行数据压缩,与其他压缩算法相比,它有着更高的压缩效率

这个不理解我也帮不了你

1
2
3
4
5
6
7
8
let fs = require("fs")
let zlib = require("zlib")

let deflate = zlib.createDeflate()

let rs = fs.createReadStream("./demo.txt")
let ws = fs.createWriteStream("./text.txt")
rs.pipe(deflate).pipe(ws)

解压

使用 gunzip 算法

1
2
3
4
5
6
7
8
let fs = require("fs")
let zlib = require("zlib")

let gunzip = zlib.createGunzip()

let rs = fs.createReadStream("./demo.txt")
let ws = fs.createWriteStream("./text.txt")
rs.pipe(gunzip).pipe(ws)

使用 inflate 算法

1
2
3
4
5
6
7
8
let fs = require("fs")
let zlib = require("zlib")

let inflate = zlib.createInflate()

let rs = fs.createReadStream("./demo.txt")
let ws = fs.createWriteStream("./text.txt")
rs.pipe(inflate).pipe(ws)

crypto模块

该模块的作用是为了提供通用的加密和哈希算法,我们用纯 js 代码实现这些功能不是不可能,但在速度上会非常慢, node.js 用 C/C++ 实现这些算法后,通过 crypto 模块暴露为 js 接口,这样不但方便而且速度还快

MD5 是一种常用的哈希算法,用于给任意数据一个“签名”,通常用一个十六进制的字符串表示

算了都讲到这了,那我就浅讲一下算法吧,先来一点算法知识吧

什么是加密

在计算机领域,由于没有100%安全的系统,为了防御黑客攻击,往往最常用的防御手段,就是运用密码学,没错就是给数据进行加密

密码学这一词汇,就是从 crypto 和 graphy 两个词汇组成,点题了属于是

为了加密信息,要用到加密算法,将明文转换为密文,可以理解成将原有的数据转换为一堆乱码,而你不懂这其中的规则来破解,就不知道这是什么意思,就是和古代的暗号类似的东西

将明文转为密文的过程称为“加密”,而将密文恢复成原来的明文,也就是逆过程,称为“解密”

加密技术中核心的技术,莫过于是算法和加密

  • 算法,也就是将普通的数据或者可以理解的数据与一串数字(密钥)结合,产生不可理解的密文的步骤
  • 加密,也就是对数据进行编码和解密的一种算法。在安全保密中,可通过适当的 钥加密技术 和 管理机制 来保证网络的信息通信安全

其本质就是发送人用密钥加密,而接收人用相同的密钥进行解密的过程,这种也叫对称加密

替换加密

顾名思义,就是替换数据,将数据中的固定字母进行替换,导致别人不懂数据内容

但这种,有一个致命的缺陷,就是有些字母在英文中出现频率较高的,只要熟练的密码破译师可以从统计数据中发现规律,进而破译密码,这种破译的事情,在历史上也出现很多,比如玛丽女王要暗杀伊丽莎白女王的计划,就被破译,导致玛丽女王被处决,历史上还有很多

移位加密

也就是将数据改变,它的读取顺序,只有知道暗号规则的人,才可以解密

其中比较出名的是纳粹时期德国使用的 英格玛 加密通讯信息

如果你输入A-A-A ,可能会变成B-D-K,映射会随着每次按键而改变

数据加密标准

由 IBM 和 NSA 于1977年开发的,DES 最初用的是56比特的二进制密钥,意味着它有72千万亿个不同的密钥

在2000年,由电子边疆基金会组织研制的一台25万美元的计算机在两天内把 DES 的所有可能密钥,都暴力破解了一遍,只用了22.5小时,成功破解 DES加密算法,这意味着 DES 不再安全

高级加密标准

AES 的优势,很简单,没错比上面一个要长的密钥,有128位,192位,256位,这意味着什么呢,以128位的密钥为例,哪怕用现在地球上所有计算机的算力也需要上亿年才能试遍所有的组合的密钥

AES 将数据切成一块一块,每块16个字节,然后用密钥进行一系列替换加密和移位加密,然后再加上一些其他操作,进一步加密信息,每一块数据,都会重复这个过程起码10次以上

AES 在性能和安全性之间取得了平衡,如今 AES 被广泛运用比如在 WPA2协议 在 WiFi 中访问 HTTPS 网络

分类

我们想过没有如果一般密钥被黑客拦截了,那门黑客们,不就可以进行通信了吗,对了,它们的解决方案,就是所谓的 密钥交换

密钥交换是一种不发送密钥,但依然让两台计算机在密钥上达成共识的算法,通信双方交换数据建立共享密钥的过程,也就是 单向函数

单向函数是一种数学操作,很容易算出结果,但想从结果逆向推算出输入非常困难

因为对于每一个输入,函数值都容易计算(多项式时间),但是给出一个随机输入的函数值,算出原始输入却比较困难(无法在多项式时间内使用确定性图灵机计算)。 单向函数是否存在仍然是计算机科学中的一个开放性问题

对称加密

同一个密钥可以同时用作信息的加密和解密,这也称为 单密钥加密

我们上面讲的替换加密和移位加密,也属于 对称加密的范畴

所谓对称,也就是采用这种加密方法的双方使用方式用同样的密钥进行加密和解密。密钥是控制加密及解密过程的指令。算法是一组规则,规定如何进行加密和解密

常用的算法有:DES、3DES、TDEA、Blowfish、RC2、RC4、RC5、IDEA、SKIPJACK等

优点

算法公开、计算量小、加密速度快、加密效率高

缺点

在数据传送前,发送方和接收方必须商定好秘钥,然后使双方都能保存好秘钥

其次如果一方的秘钥被泄露,那么加密信息也就不安全了

另外,每对用户每次使用对称加密算法时,都需要使用其他人不知道的独一秘钥,这会使得收、发双方所拥有的钥匙数量巨大,密钥管理成为双方的负担

非对称加密

分为公钥和私钥

公钥向外界公开,私钥只有解密的人,才可知,公钥只能加密但不能解密,私钥可以以解密公钥,像盒子和钥匙是一个道理,只有钥匙才可以开盒子,所以它叫不对称的

反过来,私钥加密后,可以用公钥解密,这种做法用于 签名,服务器可以用私钥加密,所有人都可以用服务器的公钥来进行解密,像一个不可伪造的签名,因为只有私钥的所有者,才可以对公钥进行加密,来证明正确的服务器或者个人

比较流行的技术是 RSA ,那我就简单的讲一下,这个算法吧,呜呜呜

RSA 算法是由麻省理工学院的三人创造的,而 RSA 是这三人的名字的首字母缩写,拼在一起

通常是先生成一对RSA密钥,分为私钥和公钥,可以在网络服务器中注册,为了提高安全性和保密性,RSA密钥至少为500位长度,一般推荐1024位的,因为一寸长一寸强,这可以使加密的计算量很大,但为了减少计算量,在传送信息时,常采用传统的加密方法和公开密钥加密方法相结合的方式,采用改进的 DES 或 IDES 对话密钥加密,然后使用 RSA 密钥加密对话密钥和信息摘要

到现在,我就开始讲它的算法原理了,肯定有人会说,都到现在了,你还没讲这个模块的一点方法呀,小陈,我只想说,我乐意,嘻嘻嘻

RSA 算法原理,是根据数论,寻求两个大素数比较简单,而将它们的乘积进行因式分解却极其困难,因此可以将乘积公开作为加密密钥,关于数学方面的公式,你们感兴趣的自己去找,我怕再讲下去,就没人了

正式学习

node.js 中,使用 OpenSSL 类库作为内部实现加密解密的手段, OpenSSL 是一个经过严格测试的可靠的加密与解密算法的实现工具

crypto 模块是对 OpenSSL 的封装,主要功能有 哈希、对称加密以及非对称加密

哈希算法

哈希算法又称为散列算法,可以用来将任意长度的输入变换成固定长度的输出,常见的比如有md5,sha1等

特点

  • 相同的输入会产生相同的输出
  • 不同的输出会产生不同的输入
  • 任意的输入长度输出长度是相同的
  • 不能从输出推算输入的值
  • 只能加密不能反向解密

是不是觉得很眼熟,没错就是上面讲的内容,所以前文是为下文做铺垫

md5 是一种常用的哈希算法,用于给任意数据一个“签名”,这个签名通常用一个十六进制的字符串表示

1
2
3
4
5
6
7
8
9
10
const crypto = require("crypto")

const hash = crypto.createHash("md5")

hash.update("Condor")
hash.update("Hero")

const hashCode = hash.digest("hex")

console.log(hashCode) //9f29506741761b010f98f908ab8f9e04

crypto.createHash() 方法传入需要加密的摘要算法,例如 MD5、SHA1、SHA256 和 SHA512,等,如果把案例的 MD5 改成 SHA1 ,加密之后的结果为 d17dfb160fce1ca89ecf94027a0bf39b6dda7f4e

update() 方法,默认字符串编码为 UTF-8,也可以传入 Buffer,update() 可以多次被调用,多次调用只是简单的把要加密的结果拼接起来

digest() 方法,表示加密之后的结果,以什么编码方式输出

  • latin1 可以认为是 ASCII 扩展
  • hex 十六进制
  • base64 前端更熟悉的 base64 编码方式

crypto.createHash() 是根据原始文件,直接生成 md5 ,由此可以完全使用 md5 批量生成,然后存储起来进行撞库,我们为了防止这种现象的发生,会在生成 md5 的时候,我们会给原始的内容,手动加点东西,也就是密钥,然后再生成 md5 ,但 node.js 方便了我们 ,有了 createHmac() 方法,可以认为 Hmac 理解为用随机数,也就是强版的哈希算法,Hmac 是 Hash 的加强

1
2
3
4
5
6
7
8
9
10
const crypto = require("crypto")

const hash = crypto.createHmac("md5", "customz_key")
// 可任意多次调用update()
hash.update("Condor")
hash.update("Hero")

const hashCode = hash.digest("hex")

console.log(hashCode)//bded9377f8766692d7c6ccdb38542d58

base64的编码和解码方式

需要一个引入一个新的 API 来完成这个。这个 API 就是 Buffer.from,它可把数据例如字符串转化成 buffer

base64 编码,对应浏览器中的 btoa

1
2
3
4
const name = "CondorHero"
const nameBuffer = Buffer.from(name)
const enecodedName = nameBuffer.toString("base64")
console.log(enecodedName) //Q29uZG9ySGVybw==

base64 编码,对应浏览器中的atob

1
2
3
4
const base64Name = "Q29uZG9ySGVybw=="
const decodeBuffer = Buffer.from(base64Name, "base64") // 第二个参数不能省略了
const decodeName = decodeBuffer.toString("utf-8")
console.log(decodeName)

Buffer.toString() 方法有三个参数

1
(method) Buffer.toString(encoding?: BufferEncoding, start?: number, end?: number): string
  • BufferEncoding 编码
  • start 截取从 start 开始
  • end 截取到 end 结束

Hmac算法

攻击者可以借助“彩虹表”来破解哈希表,彩虹表在这里就不解释了

应对彩虹表的解决方案,是给密码加盐值(salt),将 pwd 和 salt 一起计算 hash 值。其中,salt 是随机生成的,越长越好,并且需要和用户名、密码对应保存在数据表中

通过加盐,实现了哈希长度扩展,但是攻击者可以通过提交密码和哈希值进行破解攻击。服务器会把提交的密码和 salt 构成字符串,然后和提交的哈希值进行对比。如果系统不能提交哈希值,不会受到此类攻击

从来没有绝对的安全的方案,但是不推荐使用密码加盐,而是使用 Hmac算法 ,因为可以使用任意的 Hash 函数,例如 md5 => HmacMD5、sha1 => HmacSHA1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const crypto = require("crypto")

function encryptData(data, key, algorithm) {
if (!crypto.getHashes().includes(algorithm)) {
throw new Error("不支持此哈希函数")
}

const hmac = crypto.createHmac(algorithm, key);
hmac.update(data);
return hmac.digest("hex")
}

// output: 30267bcf2a476abaa9b9a87dd39a1f8d6906d1180451abdcb8145b384b9f76a5
console.log(encryptData("root", "7(23y*&745^%I", "sha256"))

对称加密 DES/AES

我们在之前已经简单介绍了DES了

那我们直接开始,不再墨迹了

crypto.createCipheriv() 方法

1
crypto.createCipheriv(algorithm,key,iv [,options])

algorithm:加密解密的类型

iv 是初始化向量,可以 为空 或者 16 字节的字符串

key 是加密密钥,根据选用的算法不同,密钥长度也不同,对应关系如下:

  1. des-cbc 对应 8 位长度密钥
  2. aes128 对应 16 位长度密钥
  3. aes192 对应 24 位长度秘钥
  4. aes256 对应 32 位长度密钥

总之,位数越长越难破解

DES 加密模式有: Electronic Codebook (ECB) , Cipher Block Chaining (CBC) , Cipher Feedback (CFB) , Output Feedback (OFB)。这里以密文分组链接模式 CBC 为例,使用了相同的 key 和 iv (Initialization Vector)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const crypto = require("crypto")

// DES 加密
function deEncrypto(message, key) {

const cipher = crypto.createCipheriv("des-cbc", key, key)
let crypted = cipher.update(message, "utf8", "base64")
crypted += cipher.final("base64")
return crypted
}
// DES 解密
function desDecrypt(text, key) {
const cipher = crypto.createDecipheriv("des-cbc", key, key)
let decrypted = cipher.update(text, "base64", "utf8")
decrypted += cipher.final("utf8")
return decrypted
}

const enCode = deEncrypto("CondorHero", "01234567")
console.log(enCode) // /yAMqF2n0wIcXg5/HuTz8A==

const deCode = desDecrypt(enCode, "01234567")
console.log(deCode) // CondorHero

签名和验证算法

我们除了不可逆的哈希算法、数据加密算法,还有专门用于签名和验证的算法。这里也需要用 openssl 生成公钥和私钥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const crypto = require("crypto")
const fs = require("fs")
const assert = require("assert")
//利用私钥进行加密,得到签名值;最后利用公钥进行验证

const privateKey = fs.readFileSync("./demo.txt")
const publicKey = fs.readFileSync("./public.txt")

const data = "需要传的数据"

//第一步:用私钥对传输的数据,生成对应的签名
const sign = crypto.createSign("sha256")
//添加数据
sign.update(date, "utf-8")
sign.end()
//根据私钥,生成签名
const signature = sign.sign(privateKey, "hex")
//第二步:借助公钥签名的准确性
const verify = crypto.createVerify("sha256")
verify.update(data, "utf-8")
verify.end()
assert.ok(verify.verify(publicKey, signature, "hex"))

path模块

是 node.js 官方提供的,用来处理路径的模块。它提供了一系列的方法和属性,用来满足用户对文件路径的处理需求,总有一款适合你,嘿嘿嘿

获取路径中的文件名

path.basename () 方法会返回 path 的最后一部分,第二部分表示要去掉的后缀,没有则不去掉文件后缀

1
2
3
4
5
6
const path = require("path")
const pathtext = "H:/学习库/node/内置模块.html"

console.log(path.basename(pathtext))//内置模块.html

console.log(path.basename(pathtext,".html"))//内置模块

获取路径中的目录名

path.dirname () 方法会返回 path 的路径目录名

1
2
3
4
const path = require("path")
const pathtext = "H:/学习库/node/内置模块.html"

console.log(path.dirname(pathtext)) //H:/学习库/node

返回 path 的扩展名

path.extname () 方法,顾名思义就是返回文件路径的后缀名

1
2
3
4
const path = require("path")
const pathtext = "H:/学习库/node/内置模块.html"

console.log(path.extname(pathtext)) //.html

解析地址

path.parse () 方法,返回一个对象,其属性表示 path 的有效元素

1
2
3
4
5
6
7
8
9
10
11
const path = require("path")
const pathtext = "H:/学习库/node/内置模块.html"

console.log(path.parse(pathtext))
//{
// root: 'H:/',
// dir: 'H:/学习库/node',
// base: '内置模块.html',
// ext: '.html',
// name: '内置模块'
// }

根据对象还原地址

path.format () 方法,从对象返回路径字符串,也就是上面的互补而已

1
2
3
4
5
6
7
8
9
10
11
12
const path = require("path")
const pathtext =
{
root: 'H:/',
dir: 'H:/学习库/node',
base: '内置模块.html',
ext: '.html',
name: '内置模块'
}

console.log(path.format(pathtext))
// H:/学习库/node\内置模块.html

拼接路径

path.join () 方法会将所有给定的 path 片段连接到一起(使用平台特定的分隔符作为定界符),然后规范化生成的路径

1
2
3
const path = require("path")

console.log(path.join("H:","demo","demoer")) //H:\demo\demoer

检测路径是否为绝对路径

path.isAbsolute () 方法,用来检验路径是否为绝对路径

1
2
3
4
5
6
const path = require("path")

const pathtext = "C:/demoer/demo.js"

console.log(path.isAbsolute(pathtext)) //true
console.log(path.isAbsolute("./demo.txt")) //false

获取当前文件所处目录的绝对路径并拼接地址

1
2
3
4
5
6
const path = require("path")

const pathtext = "C:/demoer/demo.js"

console.log(path.join(__dirname +"demo"))
//H:\学习库\nodedemo

内置模块就讲到这了