Node学习文档

Node.js

模块化


CommonJS

CommonJS引入模块

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
/* 
早期的网页中,是没有一个实质的模块规范的
我们实现模块化的方式,就是最原始的通过script标签来引入多个js文件
问题:
1.无法选择要引入模块的那些内容
2.在复杂的模块场景下非常容易出错
...
于是,我们就急需在js中引入一个模块化的解决方案

在node中,默认支持的模块化规范叫做CommonJS
在CommonJS中,一个js文件就是一个模块

CommonJS规范
- 引入模块
- 使用require("模块的路径") 函数来引入模块
- 引入自定义模块时
- 模块名要以./ 或 ../ 开头
- 扩展名可以省略
- 在CommonJS中,如果省略了js文件的扩展名
node会自动为文件补全扩展名,如果没有js,它会寻找json文件
js --> json --> node(特殊)
- 引入核心模块时
- 直接写核心模块的名字即可
- 也可以在核心模块前添加node:
*/

const m1 = require("./m1.js");
console.log(m1.a);

CommonJS导出模块

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
/* 
在定义模块时,模块中的内容默认是不能被外部看到的
可以通过exports来设置要向外部暴露的内容

访问exports的方式有两种:
exports
module.exports
- 当我们在其他模块中引入当前模块时,require函数返回的就是exports
- 可以将希望暴露给外部模块的内容设置为exports的属性
*/

/*
可以通过exports 一个一个的导出值
也可以直接通过module.exports同时导出多个值
*/
// exports.a = "孙悟空";

module.exports = {
a: "haha"
}

module.exports = {
name: "孙悟空",
age: 18,
gender: "男"
}

CommonJS原理

1
2
3
4
5
6
7
8
9
10
11
12
13
/* 
所有的CommonJS的模块都会被包装到一个函数中

(function(exports, require, module, __filename, __dirname){

});
*/

console.log(arguments);

console.log(__filename); // __filename表示当前模块的绝对路径

console.log(__dirname); // 当前模块所在目录的路径

module.exports和exports

1
2
3
4
5
6
7
8
9
10
11
12
/* 
module.exports和exports有什么关系或区别?
- CommonJS中是没有module.exports的概念的
- 但是为了实现模块的导出,Node中使用的是Module的类,每一个模块都是Module的一个实例,也就是module
- 所以在Node中真正用于导出的其实根本不是exports,而是module.exports
- 因为module才是导出的真实实现者

但是,为什么exports也可以导出呢?
- 这是因为module对象的exports属性是exports对象的一个引用
- 也就是说module.exports = exports
- 但是我们常写的是module.exports = {} ,这就改变了内存地址,exports就没作用了
*/

require的细节

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
/* 
我们现在已经知道,require是一个函数,可以帮助我们引入一个文件(模块)中导出的对象
require的查找规则如下:
- 情况一:X是一个Node核心模块,比如path、http
- 直接返回核心模块,并且停止查找

- 情况二:X是以 ./ 或 ../ 或 /(根目录)开头的
- 第一步:将X当作一个文件在对应的目录下查找:
1.如果有后缀名,按照后缀名的格式查找对应的文件
2.如果没有后缀名,会按照如下顺序:
- 直接查找文件X
- 查找X.js文件
- 查找X.json文件
- 查找X.node文件
- 第二步:没有找到对应的文件,将X作为一个目录
1.查找X/index.js文件
2.查找X/index.json文件
3.查找X/index.node文件
- 如果还没有找到,那就报错:not found

- 情况三:直接是一个X(没有路径),并且X不是一个核心模块
- 会在当前目录下的node_modules文件夹中去查找
- 如果当前目录下没有,就去上一级目录下的文件夹中的node_modules文件夹中查找
- 以此类推
- 如果上面的路径都没有找到,那么报错:not found
*/

ES Module

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
/* 
默认情况下,node中的模块化标准时CommonJS
要想使用ES的模块化,可以采用一下两种方案
1.使用mjs作为扩展名
2.修改package.json将模块化规范设置为ES模块
当我们设置 "type":"module" 当前项目下所有的js文件默认为es module
*/

// 导入m3模块,es模块不能省略扩展名(官方标准)

// 通过as来指定别名
import { a as hello, b, c } from "./m3.mjs";

// 开发时要尽量避免import * 情况
import * as m3 from "./m3.mjs";

// 导入模块的默认导出,一个模块中只有一个默认导出
// 默认导出的内容,可以随意命名
import hello, { a, b, c } from "./m3.mjs"
console.log(hello, a, b, c);


/*
通过ES模块化,导入的内容都是常量
es模块都是运行在严格模式下的
ES模块化,在浏览器中同样支持,但是通常我们不会直接使用
通常都会结合打包工具使用
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* 
ES 模块化
*/

// 向外部导出内容
export let a = 10;
export let b = "哈哈";
export let c = true;
// 等价于 export {a, b, c}

// 设置默认导出
export default function sum(a, b) {
return a + b;
}

HTML中引入ES Module

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>

<body>
<!-- 注意事项二:在我们打开对应的html时,如果html中有使用模块化的代码,那么必须开启一个服务来打开 -->
<script src="./foo.js" type="module"></script>
<script src="./main.js" type="module"></script>
</body>

</html>
1
2
3
4
5
6
// 注意事项一:在浏览器中直接使用esmodule时,必须在文件后边加上后缀名.js
import { name, age, sayHello } from "./foo.js";

console.log(name);
console.log(age);
sayHello();

ES Module集中导入

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
/* 
// 将工具函数集中导入导出
import { formatCount, formatDate } from './info.js';
import { parseLyric } from './parse.js';

export {
formatCount,
formatDate,
parseLyric
}

// 优化一:(推荐)
export { formatCount, formatDate } from './info.js';
export { parseLyric } from './parse.js';

// 优化二:
export * from './info.js';
export * from './parse.js';
*/

/*
import函数的使用
// 1. 不允许在逻辑代码中编写import导入声明函数,只能在模块的顶层作用域中使用
// 2. 如果需要动态导入模块,可以使用import函数,返回一个promise对象
let flag = true;
if (flag) {
import('./info.js').then(res => {
console.log(res);
});
}
*/

包管理工具


package.json的键含义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* 
package.json的各个属性的含义
- 必填的属性
- name: 项目的名称
- version: 当前项目的版本号
- 其他常用属性
- description: 项目的描述,很多时候是作为项目的基本描述
- author: 项目的作者(发布到npm上的时候会用到)
- license: 项目开源的协议

- private: 是否是私有的项目,如果是私有的项目,那么就不能发布到npm上

- main: 设置程序的入口,如果没有设置,那么默认是index.js,
比如我们使用axios的时候,如果有main属性,实际上是找到对应的main属性查找文件

- scripts: 用来配置一些命令的快捷方式

- dependencies: 项目的依赖,无论是开发依赖还是生产依赖都需要依赖的包

- devDependencies: 项目的开发依赖,只有在开发的时候才需要依赖的包

- peerDependencies: 对等依赖,如果我们的包依赖于某个包,但是这个包不会打包到我们的项目中,而是需要用户自己安装
*/

依赖的版本管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* 
- npm的包通常需要遵从semver版本规范
- semver版本规范是X.Y.Z的形式
- X主版本号(Major):当你做了不兼容的API修改
- Y次版本号(Minor):当你做了向下兼容的功能性新增(新功能增加,但是不影响之前的版本)
- Z修订号(Patch):当你做了向下兼容的问题修正(bug修复)
- ^和~的区别
- x.y.z:表示一个明确的版本号
- ^x.y.z:表示的是x版本号不变,y和z永远安装最新的版本
- ~x.y.z:表示的是x.y不变,z永远安装最新的版本

- npm安装依赖的命令
- 默认安装开发和生产依赖
- npm install axios
- npm i axios
- 开发依赖
- npm install webpack --save-dev
- npm install webpack -D
- npm i webpack -D
*/

package-lock.json的含义

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
/* 
package-lock.json文件解析
- name: 项目的名称
- version: 项目的版本号
- lockfileVersion: lock文件的版本号
- requires: 使用requires来跟踪模块的依赖关系
- dependencies: 项目的依赖
比如当前项目依赖axios,但是axios依赖follow-redirects
axios中的属性如下
- version: 版本号
- resolved: 依赖的包的下载地址,registry仓库中的位置
- requires/dependencies: 记录当前模块的依赖
- integrity: 用来从缓存中获取索引,在通过索引去获取压缩包文件

npm install会检测是否有package-lock.json文件
- 没有lock文件
- 分析依赖关系,这是因为我们可能会依赖其他的包,并且多个包之间会产生相同的依赖情况
- 从registry仓库中下载压缩包(如果我们设置了镜像,那么会从镜像服务器下载压缩包)
- 获取到压缩包后会对压缩包进行缓存(从npm5开始,会将下载的包进行缓存)
- 将包解压到node_modules中(require的时候会从node_modules中查找)
- 有lock文件
- 检测lock中包的版本是否和package.json中的版本一致(会按照semver规则进行匹配)
- 不一致,那么会重新构建依赖关系,直接会走顶层的流程
- 一致,会优先查找缓存
- 没有找到,会从register仓库中下载,直接会走顶层的流程
- 找到了,会获取缓存中的压缩文件,并且将压缩文件解压到node_modules中

为什么有了package.json,还需要package-lock.json文件呢?
- 当项目中已有package-lock.json 文件,在安装项目依赖时,将以该文件为主进行解析安装指定版本依赖包,
而不是使用 package.json 来解析和安装模块。因为 package.json 指定的版本不够具体,而package-lock
为每个模块及其每个依赖项指定了版本,位置和完整性哈希,所以它每次的安装都是相同的。

- package-lock.json 文件主要作用有以下两点:
- 当删除 node_module 目录时,想通过 npm install 恢复所有包时,提升下载速度。
- 锁定版本号,防止自动升级新版本。

- 正因为有了 package-lock.json 文件锁定版本号,所以当你执行 npm install 的时候,
node 不会自动更新 package.json 文件中的模块,必须用 npm install packagename(自动更新小版本号)
或者npm install packagename@x.x.x(指定版本号)来进行安装才会更新,package-lock.json
文件中的版本号也会随着更新。
*/

npx的使用

1
2
3
4
5
6
7
8
9
10
11
/* 
如何使用项目(局部)的命令,常见的有几种方式:
- 1.在终端中使用如下命令(在项目根目录下):
- ./node_modules/.bin/webpack --version
- 2.在package.json中的scripts中配置
- "webpack": "webpack --version"
- npm run webpack
- 3.使用npx
- npx webpack --version
- npx会帮我们自动查找当前项目中的node_modules中的.bin目录下的命令
*/

npm 发布自己的包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 
如何发布自己的包到npm仓库
- 1.注册npm账号:https://www.npmjs.com/
- 2.网页中登录npm账号
- 3.在命令行登录:npm login
- 4.创建或修改package.json文件
- 5.发布到npm registry上:npm publish

- 更新仓库:
- 1.修改package.json中的版本号
- 2.重新发布:npm publish

- 删除发布的包:
- 1.删除包:npm unpublish 包名@版本号

- 让发布的包过期:
- 1.设置包的过期时间:npm deprecate 包名@版本号 "包名已经过期了,请使用新的包"
*/

软链接与硬链接

1
2
3
4
5
6
7
8
9
/* 
硬链接:
- 硬链接(Hard Link)是电脑文件系统中的多个文件平等的共享同一个文件存储单元
- 删除一个文件名字后,还可以用其他名字继续访问该文件

符号链接(软链接):
- 符号链接(Symbolic Link)是一种特殊的文件,它包含了另一文件的路径名的信息
- 其包含有一条绝对路径或者相对路径的形式指向其他文件或者目录的引用
*/

pnpm的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* 
pnpm到底做了什么?
- 当使用npm或yarn时,如果你有100个项目,并且所有项目都有一个相同的依赖包,那么,你的硬盘上就需要
保存100份该依赖包的副本,这样就会占用大量的硬盘空间。
- 如果是使用pnpm,依赖包将被存放在一个统一的位置,因此:
- 如果你对同一依赖包使用相同的版本,那么磁盘上只有这一个依赖包的一份文件
- 如果你对同一依赖包需要使用不同的版本,则仅有版本之间不同的文件会被存储起来
- 所有文件都保存在硬盘上的统一的位置:
- 当安装软件包时,其包含的所有文件都会硬链接到此位置,而不是占用额外的硬盘空间
- 这让你可以在项目之间方便地共享相同版本的依赖包

pnpm创建的是非扁平的node_modules目录
- 当使用npm或yarn时,所有的依赖包都会被安装到项目的node_modules目录下
- 其结果就是,我们在编写代码的时候可以访问本不属于当前项目所设定的依赖包
- 当使用pnpm时,依赖包会被安装到项目的node_modules/.pnpm/目录下
- node_modules下的依赖包是一个软链接,指向.pnpm目录下的真实依赖包

一些命令:
- 获取当前活跃的store目录:pnpm store path
- 从store中删除当前未被引用的包来释放store的空间:pnpm store prune
*/

Node模块-path

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
/* 
path
- 表示的路径
- 通过path可以用来获取各种路径
- 要使用path,需要先对其进行引入
- 方法:
path.resolve([...paths])
- 用来生成一个绝对路径
相对路径:./xxx ../xxx xxx
绝对路径:
- 在计算机本地
c:\xxx
/User/xxx
- 在网络中
http://www.xxxx/...
https://www.xxx/...

- 如果直接调用resolve,则返回当前的工作目录
D:\code\web\Node\node-course
D:\code\web\Node\node-course\03_包管理器
- 注意,我们通过不同的方式执行代码时,它的工作目录是有可能发生变化的

- 如果将一个相对路径作为参数,则resolve会自动将其转换为绝对路径
此时根据工作目录的不同,它所产生的绝对路径也不同

- 一般会将一个绝对路径作为第一个参数,一个相对路径作为第二个参数,这样它会自动计算出最终的路径
const result = path.resolve(__dirname, "./hello.js");
以后在使用路径时,尽量通过path.resolve()来生成路径
*/

const path = require("node:path");

const result = path.resolve(__dirname, "./hello.js");

Node模块-fs


fs文件读取的api

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
const path = require("node:path");
const fs = require("node:fs");

// 1.同步读取
const res1 = fs.readFileSync(path.resolve(__dirname, "./aaa.txt"), {
encoding: "utf8"
});

console.log("同步读取", res1);

// 2.异步读取:回调函数
fs.readFile(path.resolve(__dirname, "./aaa.txt"), {
encoding: "utf8"
}, (err, data) => {
if (err) {
console.log("读取文件错误:", err);
return;
}
console.log("异步读取:回调函数", data);
});

// 3.异步读取:Promise
fs.promises.readFile(path.resolve(__dirname, "./aaa.txt"), {
encoding: "utf8"
}).then(res => console.log("异步读取:Promise", res))
.catch(err => console.log("读取文件错误:", err));

fs文件写入的api

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const fs = require("node:fs");
const path = require("node:path");

const content = "hello hello how are you";

fs.promises.writeFile(path.resolve(__dirname, "./bbb.txt"), content, {
encoding: "utf8",
flag: "a"
/*
w 打开文件写入,默认值,会覆盖原有内容
w+ 打开文件进行读写(可读可写),如果不存在则创建文件,会覆盖原有内容
r 打开文件读取,读取时的默认值
r+ 打开文件进行读写,如果不存在那么抛出异常
a 打开要写入的文件,将流放在文件末尾,如果不存在则创建文件
a+ 打开文件进行读写(可读可写),将流放在文件末尾,如果不存在则创建文件
*/
}).then(res => console.log("写入成功"))
.catch(err => console.log("写入失败"));

fs的其他操作

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
const fs = require("node:fs/promises");
const path = require("node:path");

// 创建文件夹
/*
fs.mkdir()
*/

// 读取文件夹
// 1.读取文件夹,获取到文件夹中文件的字符串
fs.readdir(path.resolve(__dirname, "./ear"))
.then(res => console.log(res))
.catch(err => console.log(err));

// 2.读取文件夹,获取到文件夹中文件的信息
fs.readdir(path.resolve(__dirname, "./ear"), { withFileTypes: true })
.then(res => {
res.forEach(item => {
if (item.isDirectory()) {
console.log(item.name, "文件夹");
} else {
console.log(item.name, "文件");
}
});
}).catch(err => console.log(err));

// 3.递归地读取文件夹中所有的文件
const readDirectory = (url) => {
fs.readdir(path.resolve(__dirname, url), { withFileTypes: true })
.then(res => {
res.forEach(item => {
if (item.isDirectory()) {
readDirectory(`${url}/${item.name}`);
} else {
console.log(item.name, "文件");
}
});
}).catch(err => console.log(err));
}

// readDirectory("./ear");

// 文件夹重命名
fs.rename(path.resolve(__dirname, "./ccc.txt"), path.resolve(__dirname, "./aaa.txt"))
.then(res => console.log("重命名成功"))
.catch(err => console.log(err));
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
const fs = require("node:fs");
const path = require("node:path");

/*
fs.readFile() 读取文件
fs.appendFile() 创建新文件,或将数据添加到已有文件中
fs.mkdir() 创建目录
fs.rmdir() 删除目录
fs.rm() 删除文件
fs.rename() 重命名 (剪切)
fs.copyFile() 复制文件 (复制)
*/

fs.promises.appendFile(path.resolve(__dirname, "./hello.txt"), "今天天气真不错")
.then(r => {
console.log("添加成功");
});

// 复制一个文件
fs.promises.readFile("D:\\apps\\picture\\Saved Pictures\\iu\\454837.jpg")
.then(buffer => fs.appendFile(path.resolve(__dirname, "./iu.jpg"), buffer))
.then(() => {
console.log("操作结束");
});


/*
创建目录
mkdir可以接收一个 配置对象作为第二个参数,通过该对象可以对方法的功能进行配置
recursive 默认值为false
- 设置true以后,会自动创建不存在的上一级目录
*/
fs.promises.mkdir(path.resolve(__dirname, "./hello/abc"), { recursive: true })
.then(r => {
console.log("操作成功");
})
.catch(err => {
console.log("创建失败");
});

// 删除目录
fs.promises.rmdir(path.resolve(__dirname, "./hello"), { recursive: true })
.then(r => {
console.log("删除成功");
})

// 重命名
fs.promises.rename(path.resolve(__dirname, "../iu.jpg"), path.resolve(__dirname, "./iu.jpg"))
.then(r => console.log("成功"));

Node模块-events


events模块的基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// events模块中的事件总线
const EventEmitter = require("node:events");

// 创建EventEmitter的实例
const emitter = new EventEmitter();

const handleEarFn = (...args) => {
console.log("监听ear事件", args);
}

// 监听事件
emitter.on("ear", handleEarFn);

// 发射事件
setTimeout(() => {
emitter.emit("ear", "ear", 18, 1.88);

// 取消事件监听
emitter.off("ear", handleEarFn);

setTimeout(() => {
emitter.emit("ear");
}, 1000);
}, 1000);

events模块的其他方法

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
const EventEmitter = require("node:events");

const ee = new EventEmitter();

ee.on("why", () => { });
ee.on("why", () => { });
ee.on("why", () => { });

ee.on("ear", () => { });
ee.on("ear", () => { });

// 1.获取所有监听事件的名称
console.log(ee.eventNames());

// 2.获取监听最大监听个数
console.log(ee.getMaxListeners());

// 3.获取某个事件名称对应的监听器个数
console.log(ee.listenerCount("why"));

// 4.获取某一个事件名称对应的监听器函数(数组)
console.log(ee.listeners("ear"));

/*
emitter.once(eventName,listener) 事件监听一次
emitter.prependListener() 将监听事件添加到最前面
emitter.prependOnceListener() 将监听事件添加到最前面,但是只监听一次
emitter.removeAllListeners([eventName]) 移除所有的监听器
*/

Node中类-Stream


Buffer和字符串的转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 1.创建Buffer
// const buf1 = new Buffer("hello");
// console.log(buf1);

// 2.创建Buffer
const buf2 = Buffer.from("world");
console.log(buf2);

// 3.创建Buffer(字符串包含中文)
const buf3 = Buffer.from("你好呀");
console.log(buf3);

// 4.手动指定Buffer创建过程的编码
const buf4 = Buffer.from("哈哈哈", "utf16le");
console.log(buf4);
console.log(buf4.toString("utf16le"));

Buffer的其他使用

1
2
3
4
5
6
7
8
9
10
11
12
const path = require("node:path");

// 1.创建一个Buffer对象
// 8个字节大小的Buffer内存空间
const buf = Buffer.alloc(8);
console.log(buf);

// 2.手动对每个字节进行操作
console.log(buf[0]);
console.log(buf[1]);
buf[2] = "a".charCodeAt();
console.log(buf);

Node中流-Stream


认识可读流

  • 什么是Stream呢?
    • 我们的第一反应应该是流水,源源不断的流动;
    • 程序中的流也是类似的含义,我们可以想象当我们从一个文件中读取数据时,文件的二进制(字节)数据会源源不断的被读取到我们程序中;
    • 而这个一连串的字节,就是我们程序中的流;
  • 所以,我们可以这样理解流:
    • 是连续字节的一种表现形式和抽象概念;
    • 流应该是可读的,也是可写的;
  • 我们可以直接通过readFile或者writeFile方式读写文件,为什么还需要流呢?
    • 直接读写文件的方式,虽然简单,但是无法控制一些细节的操作;
    • 比如从什么位置开始读,读到什么位置、一次性读取多少个字节;
    • 读到某个位置后,暂停读取,某个时刻恢复继续读取等等;
    • 或者这个文件非常大,比如一个视频文件,一次性全部读取并不合适;

可读流的基本使用

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
const path = require("node:path");
const fs = require("node:fs");

// 1.一次性读取
// 缺点一:没有办法精准控制从哪里读取,读取什么位置
// 缺点二:读取到某一个位置,然后暂停读取,恢复读取
// 缺点三:文件非常大的时候,多次读取
fs.promises.readFile(path.resolve(__dirname, "./aaa.txt"))
.then(res => console.log(res))
.catch(err => console.log(err));

// 2.通过流读取文件
// 2.1 创建一个可读流
// start:从什么位置开始读取
// end:读取到什么位置后结束(包括end位置字节)
const readStream = fs.createReadStream(path.resolve(__dirname, "./aaa.txt"), {
start: 8,
end: 22,
// 每次读取几个字节
highWaterMark: 3
});

// 2.2 监听读取到的数据
readStream.on("data", (data) => {
console.log(data.toString());

// 暂停读取
readStream.pause();

setTimeout(() => {
// 继续读取
readStream.resume();
}, 1000);
})

// 其他监听事件
readStream.on("open", (fd) => {
console.log("通过流将文件打开", fd);
});

readStream.on("end", () => {
console.log("已经读取到end位置");
});

readStream.on("close", () => {
console.log("文件读取结束,并且被关闭");
});

可写流的基本使用

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
const fs = require("node:fs");
const path = require("node:path");

// 1.一次性写入内容
fs.promises.writeFile(path.resolve(__dirname, "./bbb.txt"), "hello world", {
encoding: "utf-8",
flag: "a+"
}).then(res => console.log("写入成功"))
.catch(err => console.log(err));

// 2.创建一个写入流
const writeStream = fs.createWriteStream(path.resolve(__dirname, "./ccc.txt"), {
flags: "a+",
// 如果要使用start,则需要设置flags为r+
start: 1,
});

writeStream.write("ear");

writeStream.on("open", () => {
console.log("文件被打开");
})

writeStream.on("close", () => {
console.log("文件被关闭");
})

writeStream.on("finish", () => {
console.log("写入完成");
})

// 写入完成时,需要手动去调用close方法
// writeStream.close();

// end方法
// 将最后的内容写入到文件,并且关闭文件
writeStream.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
const fs = require("node:fs");
const path = require("node:path");

// 1.方式一:一次性读取和写入文件
fs.promises.readFile(path.resolve(__dirname, "./aaa.txt"))
.then(data =>
fs.promises.writeFile(path.resolve(__dirname, "./ddd.txt"), data)
.then(res => console.log("写入完成"))
.catch(err => console.log("写入失败")))
.catch(err => console.log("读取失败"));

// 2.方式二:创建可读流和可写流
const readStream = fs.createReadStream(path.resolve(__dirname, "./aaa.txt"));
const writeStream = fs.createWriteStream(path.resolve(__dirname, "./ddd.txt"));

readStream.on("data", (data) => writeStream.write(data));

readStream.on("end", () => writeStream.close());

// 3.在可读流和可写流之间建立一个管道
const readStream = fs.createReadStream(path.resolve(__dirname, "./aaa.txt"));
const writeStream = fs.createWriteStream(path.resolve(__dirname, "./ddd.txt"));

readStream.pipe(writeStream);

Node服务器-http


http基本介绍

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
/* 
HTTP协议
- 网络基础
- 网络的服务器基于请求和响应的
https:// 协议名

lilichao.com 域名 domain
整个网络中存在无数个服务器,每一个服务器都有它自己的唯一标识,这个标识被称为ip地址
192.168.1.17 但是ip地址不方便记忆
域名就相当于ip地址的别名

/hello/index.html 网站资源路径

当在浏览器中输入地址后发生了什么?
1.DNS解析,获取网站的ip地址
2.浏览器需要和服务器建立连接(tcp/ip)(三次握手)
客户端如何和服务器建立(断开)连接
- 通过三次握手和四次挥手
- 三次握手(建立连接)
- 三次握手是客户端和服务器建立连接的过程
1.客户端向服务器发送请求
SYN
2.服务器收到连接请求,向客户端返回消息
ACK SYN
3.客户端向服务器发送同意连接的信息
ACK
- 四次挥手(断开连接)
- 四次挥手是客户端和服务器断开连接的过程
1.客户端向服务器发送请求,通知服务器数据发送完毕,请求断开连接
FIN
2.服务器向客户端返回数据,知道了
ACK
3.服务器向客户端返回数据,收完了,可以断开连接
FIN ACK
4.客户端向服务器发数据,可以断开了
ACK
3.向服务器发送请求(http协议)
4.服务器处理请求,并返回响应(http协议)
5.浏览器将响应的页面渲染
6.断开和服务器的连接(四次挥手)


请求和响应实际上就是一段数据,只是这段数据需要遵循一个特殊的格式,这个特殊的格式由HTTP协议来规定


TCP/IP 协议族(了解)
- TCP/IP协议族中包含了一组协议,这组协议规定了互联网中所有的通信的细节
- 网络通信的过程由四层组成
应用层
- 软件的层面,浏览器 服务器都属于应用层
传输层
- 负责对数据进行拆分,把大数据拆分为一个一个小包
网络层
- 负责给数据包,添加信息
数据链路层
- 传输信息

- HTTP协议就是应用层的协议,用来规定客户端和服务器通信的报文格式的
- 报文(message)
- 浏览器和服务器之间通信是基于请求和响应的
- 浏览器向服务器发送请求(request)
- 服务器向浏览器返回响应(response)
- 浏览器向服务器发送请求相当于浏览器给服务器写信
服务器向浏览器返回相应,相当于服务器给浏览器回信
这个信在HTTP协议中就被称为报文
- 而HTTP协议就是对这个报文的格式进行规定

- 请求报文(request)
- 客户端发送给服务器的报文称为请求报文
- 请求报文的格式如下:
请求首行
- 请求首行就是请求报文的第一行
GET /index.html?username=sunwukong HTTP/1.1
第一部分 get 表示请求的方式,get表示发送的是get请求
现在常用的方式就是get和post请求
get请求用来向服务器请求资源
post请求主要用来向服务器发送数据

第二部分 /index.html?username=sunwukong
表示请求资源额路径,问号后边的内容叫做查询字符串
查询字符串是一个名值对结构,一个名字对应一个值,使用=连接,多个名值对之间使用&分隔
get请求通过查询字符串将数据发送给服务器
由于查询字符串会在浏览器地址栏中直接显示
所以,它安全性较差
同时,由于url地址长度有限制,所以get请求无法发送较大的数据

post请求通过请求体来发送数据
- 在chrome中通过 载荷 可以查看
post请求通过请求体发送数据,无法在地址栏直接查看,所以安全性较好
请求体的大小没有限制,可以发送任意大小的数据

如果向服务器发送数据,能用post尽量使用post

第三部分 HTTP/1.1 协议的版本

请求头
- 请求头也是名值对结构,用来告诉服务器我们浏览器的信息
- 每一个请求头都有它的作用:
Accept 浏览器可以接收的文件类型
Accept-Encoding 浏览器允许的编码
Accept-Language 浏览器可以接受的语言
User-Agent 用户代理 它是一段用来描述浏览器信息的字符串
空行
- 用来分隔请求头和请求体

请求体
- post请求通过请求体来发送数据


- 响应报文(response)
响应首行
HTTP/1.1 200 OK
200 响应状态码
OK 对响应状态码的描述
- 响应状态码的规制
1xx 请求处理中
2xx 表示成功
3xx 表示请求的重定向
4xx 表示客户端错误
5xx 表示服务器错误

响应头
- 响应头也是一个一个名值对结构,用来告诉浏览器响应的信息
Content-Type 用来描述响应体的类型
Content-Length 用来描述响应体大小

空行
- 用来分隔响应头和响应体

响应体
- 响应体就是服务器返回给客户端的内容


- 服务器
- 服务器的一个主要功能
1.可以接收到浏览器发送的请求报文
2.可以向浏览器返回响应报文
*/

http服务器的基本使用

  • server通过listen方法来开启服务器,并且在某一个主机和端口上监听网络请求
    • 也就是当我们通过 ip:port 的方式发送到我们监听的Web服务器上时
    • 我们就可以对其进行相关的处理
  • listen函数有三个参数:
    • 端口port:可以不传,系统会默认分配端口,可惜写入到环境变量中
    • 主机host:通常可以传入localhost、ip地址127.0.0.1、或者ip地址0.0.0.0,默认是0.0.0.0
      • localhost: 本质上是一个域名,通常情况下会被解析成127.0.0.1
      • 127.0.0.1:回环地址(Loop Back Address),表达的意思其实是我们主机自己发出去的包,直接被自己接收
        • 正常的数据库包 应用层 - 传输层 - 网络层 - 数据链路层 - 物理层
        • 而回环地址,是在网络层直接就被获取到了,是不会经过数据链路层和物理层的
        • 比如我们监听 127.0.0.1时,在同一个网段下的主机中,通过ip地址是不能访问的
      • 0.0.0.0:
        • 监听IPV4上所有的地址,在根据端口找到不同的应用程序
        • 比如我们监听 0.0.0.0 时,在同一个网段下的主机中,通过ip地址时可以访问的
    • 回调函数:服务器启动成功时回调函数
1
2
3
4
5
6
7
8
const http = require("node:http");

// 创建一个http对应的服务器
const server = http.createServer((req, res) => {
res.end("hello world");
});

server.listen(8000, () => console.log("服务器启动成功"));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const http = require("node:http");

const server = http.createServer((req, res) => {
// request对象中包含哪些信息
// 1.url信息
console.log(req.url);
// 2.method信息(请求方式)
console.log(req.method);
// 3.headers信息
console.log(req.headers);

res.end("hello world");
});

server.listen(8000, () => console.log("服务器开启成功"));

http服务器区分不同url和method

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const http = require("node:http");

const server = http.createServer((req, res) => {
const url = req.url;
const method = req.method;

if (url === "/login") {
if (method === "POST") {
res.end("登录成功");
} else {
res.end("不支持的请求方式");
}
} else if (url === "/products") {
res.end("商品列表");
} else if (url === "/lyric") {
res.end("我遇见你是最美丽的意外");
}
});

server.listen(8000, () => console.log("服务器启动成功"));

request参数解析-query参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const http = require("node:http");
const url = require("node:url");
const qs = require("node:querystring")

const server = http.createServer((req, res) => {
// 1.参数一:query类型参数
// /home/list?offset=100&size=20
// 1.1 解析url
const urlString = req.url;
console.log("1:", urlString); // /home/list?offset=100&size=20
const urlInfo = url.parse(urlString);
console.log("2:", urlInfo); // Url { ... }

// 1.2解析query: offset=100&size=20
const queryString = urlInfo.query;
console.log("3:", queryString); // offset=100&size=20
const queryInfo = qs.parse(queryString);
console.log("4:", queryInfo); // { offset: '100', size: '20' }

res.end("hello")
});

server.listen(8000, () => console.log("服务器启动成功"));

request参数解析-body参数

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

const server = http.createServer((req, res) => {
req.setEncoding("utf-8");

// request对象本质上是一个readable可读流
let isLogin = false;
req.on("data", data => {
const loginInfo = JSON.parse(data);
if (loginInfo.name === "ear" && loginInfo.password === "123123") {
isLogin = true;
} else {
isLogin = false;
}
});

req.on("end", () => {
if (isLogin) res.end("登陆成功");
else res.end("账号或密码错误");
});
});

server.listen(8000, () => console.log("服务器启动成功"));

request参数解析-header参数

1
2
3
4
5
6
7
8
9
10
11
12
13
const http = require("node:http");

const server = http.createServer((req, res) => {
console.log(req.headers);
console.log(req.headers["content-type"]);

const token = req.headers["authorization"];
console.log(token);

res.end("查看header的信息");
});

server.listen(8000, () => console.log("服务器启动成功"));

response响应对象-响应方式

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

const server = http.createServer((req, res) => {
// 1.响应数据方式一:write,这种方式是直接写出数据,但是把那个没有关闭流
res.write("hello world");
res.write("hahahaha");

// 2.响应数据方式二:end,这种方式是写出最后的数据,并且写出后会关闭流
res.end("完成");

// 如果我们没有调用end方法,那么客户端会一直等待服务器响应
});

server.listen(8000, () => console.log("服务器启动成功"));

response响应对象-响应状态码

1
2
3
4
5
6
7
8
9
const http = require("node:http");

const server = http.createServer((req, res) => {
res.statusCode = 201;

res.end("hello");
});

server.listen(8000, () => console.log("服务器启动成功"));

response响应对象-响应header

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

const server = http.createServer((req, res) => {
// 1.单独设置某一个header
// res.setHeader("Content-Type","text/plain;charset=utf8;");

// 2.http status 一起设置
res.writeHead(200, {
"Content-Type": "application/json;charset=utf8;"
});

const list = [
{
name: "ear", age: 18
},
{
name: "kobe", age: 30
}
];

res.end(JSON.stringify(list));
});

server.listen(8000, () => console.log("服务器启动成功"));

在node中发送请求-http

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const http = require("node:http");

// 1.使用http模块发送get请求
http.get("http://localhost:8000", (res) => {
// 从可读流中获取数据
res.on("data", (data) => console.log(JSON.parse(data.toString())));
})

// 2.使用http模块发送post请求
const req = http.request({
method: "POST",
hostname: "localhost",
port: 8000
}, (res) => res.on("data", (data) => console.log(JSON.parse(data.toString()))));

req.end();

在node中发送请求-axios

1
2
3
const axios = require("axios");

axios.get("http://localhost:8000").then(res => console.log(res.data));