ES+ Git Node.js Express MongoDB Ajax Axios

ES6+

ECMAScript是标准,JavaScript是其主要实现。

ES5的严格模式

1
'use strict'

严格模式下八进制的限制
严格模式下不允许使用0开头的八进制数字表示法(如012)。会抛出Uncaught SyntaxError: Octal literals are not allowed in strict mode。不过允许使用十六进制表示法(如0x12)。
设计原因: 数字前导零存在语义歧义(数学上012应与12等价),属于历史遗留问题。

严格模式下变量声明的限制
不允许未声明直接使用变量(如username = 100),会抛出username is not defined错误。
非严格模式对比: 普通模式下会自动创建全局变量,属于隐式变量声明。

let 与 const

◆ES6新增了let命令。

  • 不存在变量提升:let不像var那样会发生“变量提升”现象。所以,变量一定要在声明后使用,否则报错。
  • 暂时性死区:只要块级作用域内存在let命令,它所声明的变量就“绑定”这个区域,不再受外部的影响。
  • 不允许重复声明:let不允许在相同作用域内重复声明同一个变量。

    ES6规定暂时性死区和不存在变量提升,主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。 这样的错误在ES5中是很常见的,现在有了这种规定,避免此类错误就很容易了。

◆ES5只有全局作用域和函数作用域,ES6增加了块级作用域。
这其实带来了很多不合理的场景,内层变量可能会覆盖外层变量,用来计数的循环变量泄露为全局变量。

立即执行函数设计目的:隔离变量,避免污染全局作用域。 ES5 中 var 声明的变量无块级作用域,即使写在 {} 里,也会泄漏到外层(甚至全局)。IIFE 的解决方式就是用函数作用域包裹变量,强制隔离,避免泄漏。

所以,块级作用域的出现,实际上使得获得广泛应用的立即执行匿名函数(IIFE)不再必要了。

◆const与let不同的是,常量不可变,声明时必须立即赋值。

解构赋值

ES6允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这就是解构(Destructuring)。

数组解构赋值

只要具有索引结构(通过下标访问元素),就可以解构。所以伪数组也可以。(字符串,arguments,DOM元素集合等)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**  错误 易混点
const data = ['one','two','three'];
lat list = [v1, v2, v3];
list = data;
**/

// 正确 左边是一个语法结构,不能存储变量! 右边是数组数据
const data = ['one','two','three'];
const [v1,v2,v3] = data;

// 解构赋值交换两个变量的值
[v1, v2] = [v2, v1]
// 以前:引入第三个临时变量t来交换

// 解构赋值用于函数传参 当实参数组元素多于形参时,只会按顺序匹配前几个元素!
function func([a, b ,c]){
console.log("a"+"b"+"c");
}
func(["小红","小明","小华"]);
func(data);

关于默认值
解构赋值允许指定默认值。.ES6内部使用严格相等运算符(===)判断一个位置是否有值。所以,如果一个数组成员不严格等于undefined,默认值是不会生效的。

1
2
3
4
5
6
var [f = true] = []; // f = true

var [a, b = 3] = [1, undefined]; //a = 1, b = 3

// null不严格等于undefined
var [x, y = 3] = [1, null] //x = 1, y = null

对象的解构赋值

对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。

1
2
3
4
5
6
7
8
9
10
11
var {foo, bar} = {foo: "aaa", bar: "bbb"}; 
foo // "aaa"
bar // "bbb"

var{foo: baz} = {foo: "aaa"};
baz // "aaa"

let obj = {first: "hello", second: "world"};
let {first: f, second: l} = obj;
f // "hello"
l // "world"

所以,对象的解构赋值是以下形式的简写:

1
var {foo: foo, bar: bar} = {foo: "aaa", bar: "world"};

对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。

指定默认值

1
2
3
4
5
var {n1,n2:n2=22,n3=33,n4} = {n1:11};
n1 // 11
n2 // 22
n3 // 33
n4 // undefined

解构数组为对象

1
2
3
4
var {length, push, map} = [1, 2, 3, 4, 5];
length // 5
push // 函数
map // 函数

数组本质也是对象,具有 length 等内置属性。它可以解构出数组的内置方法和属性。

解构字符串为对象

1
2
3
4
var {length, indexOf, forEach} = "hello world";
length // 11
indexOf // 函数
forEach // undefined (函数没有forEach方法)

字符串新增特性

模板字符串
特点:

  1. 内部可以直接换行
  2. 内部可以直接插入变量或表达式 例如:${name}${age + 10}
    典型应用:用模板字符串动态生成HTML内容很方便

ES5 方法
charAt()
charCodeAt() 字符 -> 索引
indexOf() 返回子串首次出现的位置
lastIndexOf() 返回子串最后出现的位置
slice()
substring()
substr()
toLowerCase()
toUpperCalse()
split() 将字符串分割为数组
search() 正则搜索
match() 正则匹配
replace() 正则替换

ES6 + 方法
repeat(n) 字符串重复,返回新字符串
includes(‘a’) 判断是否包含a,返回布尔值
startsWith() 判断是否以某个值开始,返回布尔值
endsWith() 判断是否以某个值结尾,返回布尔值
trim() 去掉两端的空格
trimStart() 去掉前面的空格(ES2019)
trimEnd() 去掉后面的空格(ES2019)
padStart() 前面填充字符串(ES2017)
padEnd() 后面填充字符串(ES2017)
replaceAll() 替换字符串中指定内容,替换所有(ES2021)

1
2
3
4
5
'Hello World'.padStart(20) // 在前面补9个空格使总长度达20
'Hello World'.padStart(20, '@') // 在前面补9个@
'Hello World'.padEnd(30, '0') // 会在后面补19个0

' Hello World '.trimStart() // 去除前面所有空格

数值新增特性

二进制:使用0b前缀(如0b10表示二进制的2)
八进制:使用0o前缀(如0o10表示八进制的8)
十六进制仍然使用0x前缀。

输出特性:无论使用何种进制表示,控制台输出都会转换为十进制形式

Number 构造函数本身新增的方法和属性:
ES5

1
2
Number.MAX_VALUE;
Number.MIN_VALUE;

ES6+
Number.MAX_SAFE_INTEGER 读取最大的安全整数
Number.MIN_SAFE_INTEGER 读取最小的安全整数
Number.EPSILION 两个数字间最小差值,就是JS的数字精度

Number.isNaN() 同全局对象的 isNaN()
Number.isFinite() 同全局对象的 isFinite()
Number.parseInt() 同全局对象的 parseInt()
Number.parsetFloat() 同全局对象的 parsetFloat()
Number.isInteger() 判断参数是否是整数,返回布尔值
Number.isSafeInteger() 判断参数是否是安全整数,返回布尔值

Git

  1. github https://github.com/
  2. 码云 https://gitee.com/

开始

Git 是一款开源免费的分布式的版本控制系统。是 Linux 之父 Linus Torvalds(林纳斯·托瓦兹)为了方便管理 linux 代码代码而开发的。

功能:

  • 代码备份
  • 版本回退
  • 多人协作
  • 权限控制

Git 官方文档地址: https://git-scm.com/book/zh/v2

常用Linux命令
ls 将当前目录下所有的文件或子目录列举
ls -l 详细信息

rm -rf 删除(小心rm -rf /,全部格式化删除
clear 删除当前的执行记录

cd 进入到指定的目录

常用快捷键
ctrl + c 终止当前命令
ctrl + l 清除当前命令行
tab 自动补全路径
键盘上下方向键 调取历史命令

Vim的使用

1
2
3
4
5
6
7
8
9
vim <文件名.txt>

i # 进入输入模式,才能对文件进行编辑

esc # 回到命令模式

:w # 保存 vim :w! 强制保存
:q # 退出 vim
:wq # 保存并退出 vim

Git基础命令

初始化:每个新项目需要执行一次git init初始化,初始化会在项目目录下创建.git隐藏目录。

.git目录 :

  • hooks 目录包含客户端或服务端的钩子脚本,在特定操作下自动执行。
  • info 包含一个全局性排除文件,可以配置文件忽略。
  • logs 保存日志信息。
  • objects 目录存储所有数据内容,本地的版本库存放位置。
  • refs 目录存储指向数据的提交对象的指针(分支)。
  • config 文件包含项目特有的配置选项。
  • description 用来显示对仓库的描述信息。
  • HEAD 文件指示目前被检出的分支。
  • index 暂存区数据。

    不要手动去修改 .git 文件夹中的内容。

  1. 工作区:代码编辑区,开发者直接编辑文件的地方。所有修改首先在工作区进行。
  2. 暂存区:修改待提交区。通过git add命令将工作区修改添加到暂存区。
  3. 版本库:存储项目历史版本。通过git commit命令将暂存区内容提交到版本库。

    除了.git文件,其他都是工作区。

添加暂存区:

1
2
3
4
git add <file>    # 添加指定文件到暂存区
git add -u # 添加所有被删除或被修改的文件到暂存区(不包括新增文件)
git add . # 添加所有修改和新建的文件到暂存区(不包括删除的文件)
git add -A # 添加所有被删除、被替换、被修改和新增的文件到暂存区!

提交版本库:

1
2
git commit -m "提交日志"         # 把暂存区的东西提交到版本库
git commit -am "提交日志" # 把工作区的修改一步到位添加暂存并提交到版本库

查看状态和变化:

1
2
3
4
git status;
//对工作区和版本库进行比较。
//绿色 在暂存区
//红色 还在工作区

如果 git status 命令的输出对于你来说过于简略,而你想知道具体修改了什么地方,可以用 git diff 命令。

1
2
git diff             # 查看当前工作区和版本库的差异 (不包括新增的文件)
git diff --cached # 查看暂存区和版本库中的变化

撤销修改和撤销暂存

工作区的修改没有添加到暂存区

1
2
3
4
5
git restore <文件名>    # 恢复工作区指定文件
git restore . # 恢复工作区所有的修改(恢复之后,新增的文件不会被删除)

git checkout -- <file> # 同上 作用一致
git checkout -- . # 同上 作用一致

工作区的修改已经添加到暂存区
如果工作区的修改已经添加到暂存区,先清除暂存区,再恢复工作区。

1
2
git restore --staged <文件名>      # 把指定文件从暂存区移除
git restore --staged . # 把所有文件从暂存区移除

历史版本回滚

查看历史版本号:

1
2
3
git log		# 查看提交记录
git log -n # 查看最近的 n 次提交几次,n 是个数字
git log --oneline # 每次提交记录只用一行显示

通过指定版本号回滚:

1
git reset --hard <commitID>  # 版本号前七位即可

如果需要查看被回滚掉的提交的版本号:

1
git reflog

快捷回滚:

1
2
3
git reset --hard HEAD^    # 恢复到上个版本
git reset --hard HEAD^^ # 恢复到上上个版本
git reset --hard HEAD^^^ # 恢复到上上上个版本

Git忽略文件

哪些文件需要被 git 忽略:

  1. 忽略操作系统自动生成的文件,比如缩略图等;
  2. 忽略编译生成的中间文件、可执行文件等,也就是如果一个文件是通过另一个文件自动生成的,那自动生成的文件就没必要放进版本库,比如 Java 编译产生的.class文件;
  3. 忽略你自己的带有敏感信息的配置文件,比如存放口令的配置文件。

设置忽略文件 .gitignore
忽略文件的文件名是 .gitignore 的文件, 文件内可以设置项目的忽略规则。

忽略文件可以放在项目中的任意目录中,放在哪个目录作用范围就是哪个目录; 一般忽略文件会放在项目的根目录下。

Node与模块化

官方网站地址: https://nodejs.org/en/
中文网站地址 : http://nodejs.cn/

开始

Node.js,也称 Node,是一个基于 Chrome V8 引擎的 JavaScript 运行环境(宿主),与浏览器是等价的。

Node.js不是一种独立的语言,也不是一个JS框架或库,运行在Node.js上的JS不能使用DOM,BOM,但是可以使用Node.js提供的各种API。

有什么用?

  • 基于Node进行后端开发
  • 前端工程化支持(Wepack/Gulp等工具链)
  • 开发桌面应用(框架Electron、ReactNative等)
  • 开发小工具(自动化脚本、爬虫程序等)

特点:

  • 单线程
  • 非阻塞
  • 事件驱动

前端是指运行在客户端上的代码,特指WEB前端时即运行在浏览器上的代码,如HTML、CSS和JavaScript构建的页面。
后端是指运行在服务器端的程序,主要负责业务逻辑实现、数据库操作等功能,也称为服务端。

后端开发的组成

  • 后端编程语言(PHP、Java、C#、Go、Python、JavaScript)
  • Web服务器程序(Tomcat、Apache、Nginx、IIS等)
  • 数据库程序(MySQL、Oricle、MongoDB)

后端架构分层

  • 表现层:Node.js
  • 业务层:Java等
  • 底层:C/C++

cmd :不支持ls等Linux命令
PowerShell : 比cmd强大
GitBash : 支持完整Linux命令

REPL方式运行
(临时计算或快速测试)
进入REPL:命令行或终端运行 node ,就进入了 repl 模式
退出REPL:.exit 或者 按两下 ctrl+c 或者 ctrl+d
常用REPL命令:

  • ctrl + c - 按下两次 - 退出 Node REPL。
  • ctrl + d - 退出 Node REPL.
  • 向上/向下键 - 查看输入的历史命令
  • tab 键 - 列出当前变量(对象)

脚本运行

内置全局常量

  • __dirname :当前文件文件夹绝对路径
  • __filename:当前文件完整绝对路径 + 文件名
  • module:当前模块对象
  • exports:导出对象
  • require:导入方法

Buffer

Buffer 是 Node.js 提供的用于存储二进制数据的类数组对象(长得像数组,但本质是底层内存的直接映射,速度快),它的大小在创建时固定,无法动态调整,且存储的是 0~255 之间的整数值(对应字节)。

普通字符串只能存文字。图片、视频、文件、网络数据流都是二进制,JS原生不好处理,所以Node专门搞了个Buffer用来装这些二进制数据
类数组:写法像数组,可以buf[0]、遍历、有 length,但不是普通数组。

1 Byte = 8 Bit (1Bit对应的是一个二进制位) = 2个十六进制位
UTF-8 下:
1 个英文字符 = 1 字节,比如 H e l l o 每个字母都是 1 字节
1 个中文汉字 (UTF-8) = 3 字节,汉字复杂,编码内容多。

存储容量换算:

创建Buffer

1
2
3
4
5
6
7
8
9
10
11
// 创建空的Buffer -> 申请一段干净的内存,自动全部填0
const b1 = Buffer.alloc(12); // 创建12字节Buffer并自动填充0
console.log(b1); // 二进制太长,控制台用2个十六进制位展示一个字节:<Buffer 00 00 00 00 00>

// 从字符串创建 -> 把字符串自动转成二进制存进 Buffer
const b2 = Buffer.from('Hello'); // 根据字符串 "Hello" 生成 Buffer
console.log(b2); // <Buffer 48 65 6c 6c 6f>(Hello 的 utf8 二进制编码)
console.log(b2.toString()); // 转回字符串:Hello

// 从数组创建 -> 每一个数字=1个字节,全是0~255的整数,手动写好每个字节的数值,直接生成Buffer
const b3 = Buffer.from([72, 101, 108, 108, 111]);

读取Buffer

1
2
3
4
5
6
7
8
9
10
const b = Buffer.from("hello 嘻嘻嘻"); // 总长度为hello(5) + 空格(1) + 嘻×3(9) = 15字节

console.log(b); // 16进制
console.log(b[0]); // 用下标取到的是十进制

b.forEach((item, index) => {
console.log(item, index); // 十进制(0-255),下标
})

b.toString();

buffer 每个元素能表示的最大数字是 255,如果超过 255 的数字,会舍去高位(二进制)。
超过256,前面多出来的高位,都是完整的 256 倍数。取余,就是把所有「完整的 256 倍数」全部扔掉,只剩不足 256 的尾数。

1
2
3
buff3[0] = 365;                    // ‭365的二进制为0001 0110 1101‬ 
// 365 % 256 = 109
console.log(buff3[0]); // 输出109 -> 109二进制为0110 1101

内置模块

Node 当中的模块分为三种:内置模块,第三方模块(npm 下载的,比如express、axios)以及自定义模块(自己写的 js 文件)。 不论哪一种模块,在使用时都必须先引入模块。

1
2
3
const 变量 = require('模块');
//比如path模块
const path = require('path');

关于抛错:

  1. try 里面的错误会被catch捕获,不论是代码错误还是主动抛出,捕获到错误之后由程序员处理,系统不会报错。
  2. try-catch 不论是否抛出错误,都不影响后面的语句的执行。
  3. try 内部,错误后面的语句不会执行。
1
2
3
4
5
6
7
8
9
try {
// 系统报错 调用不存在的函数
getInfo();

// 主动抛出的错误
// throw new Error('xiaole is not defiend');
} catch (err) {
console.log('捕获到错误:', err.errno, err.message);
}

Path模块

  • path.join([path1][, path2][, ...]) 用于连接路径。该方法的主要用途在于,会正确使用当前系统的路径分隔符,Unix系统是”/“,Windows系统是”\“。
  • path.isAbsolute(path) 判断参数path是否是绝对路径。
  • path.dirname(p) 返回路径中目录的部分 。
  • path.basename(p) 返回路径中的最后一部分,文件名部分。
  • path.extname(p) 返回路径中文件的后缀名。
  • path.resolve() 将路径或者路径片段序列化为绝对路径 (常用)。

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
// 引入模块
const fs = require('fs');
const path = require('path');

// 要读取文件的路径
const filename = path.join(__dirname, './data/a.txt');


// ----------------------------------------------------
// 异步方式 读取文件内容
/*
fs.readFile(filename, (err, data) => {
if (err) {
console.log('文件读取失败:', err);
return; //不加return的话会继续执行后面代码然后报错
}
// console.log(data); // Buffer 数据
console.log(data.toString());
});
console.log('开始读取...');
*/

// 指定编码方式 直接对读取到二进制数据进行编码
fs.readFile(filename,'utf-8', (err, data) => {
if (err) {
console.log('文件读取失败:', err.errno, err.code);
return;
}
console.log(data);

});
console.log('开始读取...');


// ------------------------------------------------------
// 同步方式读取文件内容
// try {
// // const data = fs.readFileSync(filename);
// const data = fs.readFileSync(filename, 'utf-8');
// console.log(data);
// } catch (error) {
// console.log('文件读取失败:', error.errno, error.code);
// }
// 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
38
39
40
41
42
43
44
// 导入模块
const fs = require('fs');
const path = require('path');


// 要写入文件的地址
// const filename = path.join(__dirname, './data/b.txt');
const filename = path.resolve('./data/b.txt');

// 要写入的内容
const data01 = '你好小乐' + Math.random() + '\n';
// const data02 = Buffer.alloc(20, 100);

// -----------------------------------------------------
// 异步f方式 写入文件
fs.writeFile(filename, data01, err => {
if (err) {
console.log('写入失败!', err.errno, err.code);
} else {
console.log('写入成功!');
}
});
//从头开始写入 再次运行不会多加这个内容 而是覆盖


// ----------------------------------------------------------
// 同步方式 写入文件
try {
fs.writeFileSync(filename, data01);
console.log('写入成功!');
} catch (err) {
console.log('写入失败!');
};


// -------------------------------------------------------------------
// 同步方式写入 追加写
try {
for (let i = 0; i <= 10000; i ++) {
fs.appendFileSync(filename, data01);
}
} catch (err) {
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
// 导入模块
const fs = require('fs');

// ---------------------------------------------------
// 重命名 将a.txt 改成 a.md
fs.rename('./data/a.txt', './data/a.md', err => {
if (err) {
console.log('重命名失败!');
} else {
console.log('重命名成功!');
}
});


// ---------------------------------------------------
// 其实也移动文件的位置
fs.rename('./data/a.md', './a.md', err => {
if (err) {
console.log('重命名失败!');
} else {
console.log('重命名成功!');
}
});

删除文件:

1
2
3
4
5
6
7
8
9
10
11
const fs = require('fs');

fs.unlink('./a.md', err => {
if (err) {
console.log('文件删除失败!');
} else {
console.log('文件删除成功!');
}
})

// fs.unlinkSync()同步

创建目录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 异步方式 创建目录 递归方式创建多级目录 
fs.mkdir(newDir, {recursive:true},err => {
if (err) {
console.log('创建目录失败:', err.message);
} else {
console.log('创建目录成功!');
}
});

// 同步方式 创建目录
try {
fs.mkdirSync(newDir);
// fs.mkdirSync(newDir,{recursive:true}); 递归方式创建多级目录
} catch (err) {
console.log('目录创建失败:', err.message);
}

URL模块

querystring 模块

JSON 格式的处理

JSON全称是 JavaScript Object Notation (JavaScript 对象表示法) ,是一种轻量级的数据交换格式。

JSON 的语法与 JS 定义数组和对象的语法区别:

  1. json 中的字符串必须使用双引号。
  2. json 中的属性名必须使用双引号包裹。
  3. json 中的最后一个属性不能有逗号。
  4. json 中的属性值不能是表达式。
1
2
3
4
5
// 将JSON格式的字符串转为对象或数组!
const obj = JSON.parse(json_data);

// 对象、数组 -> JSON
const json = JSON.stringify(strs);

模块规范概述

四种模块化规范:CommonJS规范、AMD规范、CMD规范、ES6 Module规范。

  1. CommonJS规范是Node.js 默认规范。Node 后端专用。
    同步加载:加载完模块,代码才继续往下走。
    运行时加载:代码执行到 require 才引入。
    适合服务端、本地文件(本地读取快,同步没问题)。
    浏览器原生不支持,必须打包(webpack)。
  2. ES6 Module规范,现在浏览器 + Node.js + Vue/React/ 小程序 全都用它。
    编译时加载(静态):代码没运行,就先分析模块。
    支持异步、按需导入。
    原生浏览器直接支持。
    Node 开启:package.json 加 “type”:”module”。
    未来唯一主流规范。

CommonJS模块规范

引入模块

1)使用require()引入模块,内置模块直接写模块名,自定义模块需要些相对路径。
2)自定义模块的地址必须以 ./../ 开头。如果模块文件的地址没有以 ./../ 开头,会被认为是内置模块或第三方模块的模块名。
3)扩展名可以省略。如.js或.json。无扩展名时会依次查找同名js文件、json文件和目录。

1.扩展名是.js的模块文件: 读取文件内容并编译执行并获取模块中暴露的数据。
2.扩展名是.json的模块文件: 读取文件时会自动用 JSON.parse() 解析返回结果作为获取的数据。

整个目录作为一个模块
1)会默认加载该目录下 package.json 文件中 main 属性定义的入口文件。
2)如果没有package.json, 或者 main 属性对应的文件不存在,则自动找 index.jsindex.json 作为入口文件。

  1. 模块作用域隔离
    每个模块里的变量、函数,默认只能自己用
    不导出,外面绝对访问不到,不会和其他文件变量冲突。
  2. 模块缓存机制
    同一个文件,require 引入多次,只会执行一次
    后面再次引入,直接读取缓存,提升性能。
  3. 模块执行顺序
    引入模块时,会从头到尾执行一遍被引入文件的代码

导出数据(模块暴露数据)

模块中定义了变量但未暴露,外部就无法访问。require返回的是空对象{},无法访问模块内部变量。
本质原理:module.exports实际上是模块对外暴露的接口对象,require()的返回值就是这个对象。

1
2
require() 的返回值 = module.exports 的值
不暴露 → require 返回 {}

模块中通过module.exports赋值暴露数据,绝大多数暴露的是对象类型,因为信息承载量更大。当模块暴露的是对象时,可以使用解构赋值直接获取属性。要注意解构出的方法如果直接调用,this会指向全局对象而非原模块对象。

通过给module.exports添加getMessagesetMessage两个方法,可以实现暴露多个数据。

1
2
3
4
5
6
module.exports.getMessage = () => {
//内容
}
module.exports.setMessage = () => {
//内容
}

关于exports赋值的原理 :对象存储在堆中,变量名(如exports)存储在栈中,然后指向堆中的对象。module.exports默认指向一个空对象,exports是它的别名,也指向这个空对象。给exports添加属性实际上是给这个空对象添加属性。如果修改exports的值(重新赋值),则exports会指向新的对象,与module.exports的引用关系断开,导致无法暴露数据。

1
2
3
4
5
6
7
8
9
10
11
module.exports = 真正出口 
exports = 快捷方式(别名)

一开始:
exports 指向 module.exports 指向的空对象

所以:
exports.a = 1 等效 module.exports.a = 1

但:
exports = {} ❌ 这等于:快捷方式指向新东西,和出口断开了 → 无效

ES6 模块规范

ES6 模块怎么开启?
方式 1:文件后缀改成 .mjs
方式 2:package.json 加一句

1
2
3
{ 
"type": "module"
}

引入模块

1
2
3
4
5
6
7
// 解构导入 
import {writeFile, readFile} from "node:fs"; // fs 模块里只拿出这两个方法
writeFile('data.txt', 'hello',err=>{});

// 导入fs模块的所有导出成员,并挂载到fs这个命名空间对象上
import * as fs from 'node:fs';
fs.writeFile('data.txt', 'hello',err=>{});

导出数据(模块暴露数据)

使用 export default 可以在模块中暴露单个数据,注意文件中 export default 语句只能出现一次。

而使用 export 可以暴露多个数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 第一种写法 一边定义一边暴露
export const year = 1918;
export function fn() {};
export const obj = {name:'mingge',age:100};


// 第二种写法 最后统一导出
const firstName = 'Lee';
const lastName = 'KeQiang';
const year = 1918;
function fn() {};
const obj = {name:'mingge',age:100}

export {firstName, lastName, year, fn, obj};

export:
多份导出。
导入:import { 原名 }。
不能乱改名。
export default:
单个唯一。
导入:import 随便起名。
不用花括号。

NPM

NPM官网:npm | Home
npm类似于手机应用商店或电脑软件管家,是一个集中管理各种开发包的工具平台。官网上托管了大量开发者共享的包,包括流行框架如Vue、React等。此时需要通过命令行工具进行下载,而非直接从网站下载。

基础命令行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
npm -v
npm init -y # 快速初始化
npm install # 等价于npm i

npm install 包名 -g # 全局安装 主要安装命令行工具

# 安装指定的版本
npm install 包名@版本号
npm install 包名@版本号 -g

# 删除包
npm remove 包名
npm remove 包名 -g

# 更新包
npm update 包名
npm update 包名 -g

# 安装依赖
npm install # 根据package.json 安装所需依赖

# 清除缓存
npm cache clean --force # force 表示强制清除

package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"name": "01-project", // 包名
"version": "1.0.0", // 版本
"description": "", // 描述信息
"main": "index.js", // 入口文件
"scripts": { // 可执行的名
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "", // 作者信息
"license": "ISC", // 开源许可

"dependencies": { // 依赖信息
"bootstrap": "^5.1.3",
"jquery": "^3.6.0"
}
"devDependencies": { // 开发中的依赖
"babel": "^6.23.0"
}
}

当删除node_modules后运行程序会报错,因为缺少依赖模块。package.json中记录了项目所需的所有依赖包及其版本。通过npm install命令可以自动安装所有依赖。

版本锁定规则
^3.0.0:锁定大版本(3.x.x)
~3.1.0:锁定小版本(3.1.x)
3.1.1:锁定完整版本
package-lock.json:精确锁定安装时的具体版本。

依赖包列表
依赖类型:

  • dependencies:生产依赖(开发和运行都需要,项目本身需要)
  • devDependencies:开发依赖(仅开发过程需要,项目本身不需要)
    安装方式:
  • npm install 包名 --save:安装为生产依赖
  • npm install 包名 --save-dev:安装为开发依赖

模块的查找过程

  1. 看到 ./ 或 ../ → 自定义文件模块
  2. 不是相对路径 → 先找内置模块
  3. 不是内置 → 找第三方模块
  4. 第三方模块加载过程:
    ① 先从脚本就所在的目录中查找有没有 node_modules 目录,如果有进入查找模块。
    ② 如果脚本同级目录没有 node_modules 目录,去上级目录查找 node_modules 目录,如果有进入查找模块。
    ③ 以此类推,一直查找到根目录

远程仓库与npm

克隆的项目不包含node_modules目录,必须执行npm install才能正常运行项目,依赖安装应在项目根目录下执行。

配置命令别名

配置 package.json 中的 scripts 属性:

1
2
3
4
5
6
{
"scripts": {
"server": "node server.js",
"start": "node index.js",
},
}

配置完成之后,可以使用别名执行命令:

1
2
npm run server
npm run start

不过 start 别名比较特别,使用时可以省略 run:

1
npm start

所以,对于陌生的项目,可以通过查看 scripts 属性来参考项目的一些操作。

cnpm

使用npm下载包时,会去npm官网下载,而npm服务器在国外,可能不稳定。

cnpm使用国内镜像作为npm源,解决官方源在国外导致的下载不稳定问题。淘宝镜像每10分钟同步一次npm官方仓库,提供国内高速下载。

总结特点:

  • 与npm命令完全兼容,仅需替换命令前缀。
  • 支持所有npm操作(安装、初始化、发布等)。
  • 镜像服务器位于阿里云,网络稳定性更高。

全局安装 cnpm 命令,安装完成后使用 cnpm 命令代替 npm 命令。

1
2
3
npm install -g cnpm --registry=https://registry.npmmirror.com

cnpm -v

yarn

Yarn 由 Meta 推出,核心优势是缓存、并行下载、精确版本锁定。

全局安装yarn:

1
npm install -g yarn

基础命令:

1
2
3
4
5
6
yarn --version #查看版本
yarn init #初始化项目
yarn add 包名 #安装依赖(相当于npm install)
yarn add 包名 --dev #开发依赖(相当于--save-dev)
yarn global add 包名 #全局安装
yarn remove 包名 #移除依赖

发布npm包

  1. 准备项目
    npm init 生成 package.json;写好代码,用 module.exports 导出。
  2. 必须切回官方源
1
npm config set registry https://registry.npmjs.org/

用淘宝镜像无法发布,必须换回 npm 官方源!
3. 登录并发布

1
2
npm login // 输入:用户名、密码、邮箱
npm publish // 发布
  1. 更新包
    必须改版本号 才能再次发布,改完 package.json 的 version,再执行 npm publish

HTTP协议

基础

IP地址
IP地址是IP协议提供的统一地址格式,为互联网上每个网络和主机分配的唯一标识。每个联网设备(电脑、手机、智能家居等)都必须有唯一IP地址。采用分层结构解决地址不足问题(局域网使用私有IP,对外共用公网IP)。当前主流IPv4格式为点分十进制,地址组合数为256^4种。IPv6是为解决地址不足问题推出的新格式。

域名
DNS:把网址翻译成 IP 地址。

端口号
客户端通过IP地址找到服务器后,需要通过特定端口号访问服务器上的具体应用程序。每个应用程序对应一个端口号,这是客户端与服务器应用程序通信的最终通道。

HTTP(hypertext transport protocol)协议,中文叫超文本传输协议,是一种基于TCP/IP的应用层通信协议,这个协议详细规定了浏览器和万维网服务器之间互相通信的规则。主要规定了两个方面的内容:

  • 客户端:用来向服务器发送数据,可以被称之为请求报文
  • 服务端:向客户端返回数据,可以被称之为响应报文

通信过程:
1.用户在浏览器地址栏输入域名并回车后,会发送请求报文到服务器
2.服务器接收请求报文后返回响应报文(包含HTML、CSS、JS等资源)。
3.浏览器解析响应报文并渲染显示给用户。

请求报文与响应报文

请求报文

  1. 请求行(请求方式+URL+协议版本)
  2. 请求头
  3. 空行
  4. 请求体(POST才有数据)

响应报文

  1. 响应行(协议版本+响应状态码+响应状态描述)
  2. 响应头
  3. 空行
  4. 响应体

响应状态码:

  • 1xx:指示信息–表示请求已接收,继续处理。
  • 2xx:成功–表示请求已被成功接收、理解、接受。
  • 3xx:重定向–要完成请求必须进行更进一步的操作。
  • 4xx:客户端错误–请求有语法错误或请求无法实现。(你写错了)
  • 5xx:服务器端错误–服务器未能实现合法的请求。(后端挂了)
    更多响应状态码: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Reference/Status

使用Node创建http服务

创建服务
写一个程序,该程序可以接收到客户端浏览器的请求,并能为客户端浏览器做出响应; 该程序是后端程序,运行在服务器上,需要 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
// 导入内置模块
const http = require('http');

/*
1. 创建 http 服务的方法的参数是个回调函数
2. 回调函数在接收到请求的时候自动执行
3. 回调函数在执行的时候,接收到两个参数,分别是请求对象,响应对象
4. createServer 方法返回一个对象
*/
// 创建服务
const server = http.createServer((req, res) => {
console.log('接收到一个请求, 客户端IP为', req.socket.remoteAddress);
// 结束请求,返回内容给浏览器
res.end('<h1>Welcome to My WebSite</h1>');
});

// 启动服务 给http服务对象监听端口,服务启动成功,回调函数就执行
server.listen(8080, () => {
console.log('http server is running on 8080');
});

// 第二个参数可以设置访问ip
// server.listen(8080, '127.0.0.1', () => {
// console.log('http server is running on 8080');
// });

/*
注意:
1. 修改代码之后,要重新启动服务,先 ctrl+c 结束,再重新运行
2. 如果端口号被占用,可以换一个端口或者关闭占用端口的进程
*/

获取请求报文信息

常见的获取请求的信息:

1
2
3
4
req.method // 请求方式 GET/POST 
req.url // 请求地址
req.headers // 请求头
req.socket.remoteAddress // 客户端IP

获取请求报文信息

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 http = require('http');
const {table} = require('table');

// 创建服务
const server = http.createServer((req, res) => {

// 请求行
console.log('请求方式:', req.method);
console.log('URL:', req.url);
console.log('协议版本:', req.httpVersion);
// 获取IP地址
console.log('客户端IP:', req.socket.remoteAddress);
// 获取请求头
console.log(req.headers); // 返回一个对象
// console.log('客户端浏览器信息:', req.headers['user-agent']);

console.log(table(Object.entries(req.headers)));

// 设置响应头
res.setHeader('Content-type', 'text/html;charset=utf-8');
// 做出响应
res.end('<h1>Welcome to My Website</h1>');
});

// 启动服务
server.listen(8080, '127.0.0.1', () => {
console.log('http server is runing on 8080');
})

获取url中的查询字符串

1
2
3
4
5
// 第一种方式 解析url
const url = require('url');

const urlInfo = url.parse(req.url, true);
console.log(urlInfo.query);
1
2
3
4
5
6
7
8
9
// 第二种方式 解析url
const {URL} = require('url');

// 需要手动拼接成完整的url,否则会报错
const urlInfo = new URL('http://127.0.0.1/' + req.url);
console.log(urlInfo.searchParams);
// 使用 get 方法获取相应的信息
console.log(urlInfo.searchParams.get('a'));
console.log(urlInfo.searchParams.get('b'));

GET 和 POST 区别
HTML表单属性设置

  • action:提交到哪个地址。
  • method:GET / POST。
  • name:参数名,必须写才能提交
    表单请求方式设置
    表单默认使用GET方式提交,通过method="post"属性可将请求方式改为POST
    数据位置差异
  • GET请求:表单数据会拼接在URL查询字符串中
  • POST请求:表单数据会放在请求体中传输。

获取POST请求体信息
因为 HTTP 请求POST数据是流式传输的,所以要一段一段接收,最后拼起来。每一块数据叫chunk(块),类型是Buffer(二进制数据)。
所以必须监听

1
2
3
4
5
6
7
8
9
10
 // 给请求对象监听data事件  请求对象本质上就是以读取流
req.on('data', (chunk) => {
// += 会让 buffer 自动转为 string
reqBody += chunk;
});

// 给请求对象监听 end 事件, 读取完毕触发该事件
req.on('end', () => {
console.log('POST 数据接收完毕:', reqBody); // 是查询字符串格式,可以使用 querystring 模块处理成对象
});

只有 POST/PUT 等请求方式才能有请求体,get 、delete 等方式没有请求体!

设置响应报文

设置响应行

1
2
response.statusCode = 200;		// 设置响应状态码
response.statusMessage = 'OK'; // 设置响应状态描述

◆设置响应头

1
2
3
// response.setHeader('响应头名字', '响应头内容')
res.setHeader('Content-Type', 'text/html; charset=utf-8'); //告诉浏览器:我返回的是 HTML,编码是 UTF-8(不乱码)

1
2
3
4
5
// 同时设置 响应状态码、设置响应状态描述、响应头
res.writeHead(200, 'OK', {
'Content-Type': 'text/html; charset=utf-8',
'My-Header': 'hello'
})

设置响应体

1
2
3
4
// resposne 本质上是一个可写流对象 可以分段写内容
res.write('<h1>你好</h1>');
res.write('<p>这是第一段</p>');
res.write('<p>这是第二段</p>');

结束响应

1
2
3
4
res.end(); // 结束

// 或者设置响应体并结束响应
response.end('响应体内容');

Express

原生 http = 自己砍柴、生火、做饭(麻烦、底层、累、容易错)
Express = 全自动电饭煲(开箱即用、简单、高效、企业直接用)

安装:

1
2
npm init 
npm install express;

核心:

1
2
3
4
5
6
7
const app = express(); // 创建网站服务 
// 如何理解? app = 你的整个网站后台服务,express ()创造它,app管理它、使用它、启动它!

app.use(...) // 给服务加功能
app.get(...) // 给服务加路由
app.post(...) // 给服务加接口
app.listen(...) // 启动服务

创建http服务

  • 模块导入: 使用const express = require('express')导入Express模块
  • 服务创建: 直接调用express()函数即可创建服务实例,通常命名为app
  • 服务启动: 通过app.listen()方法启动服务,指定端口号和回调函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 导入模块
const express = require('express');
// 创建服务
const app = express();

// 使用内置的中间件 express.static() 托管静态文件 指定静态文件所在的目录
// app.use(express.static(path.join(__dirname, 'public')));

app.get('/', (req, res) => {
res.send(<h1>欢迎访问我的网站</h1>);
});

// 启动服务
app.listen(8080, () => {
console.log('http server is running on :8080');
});

只要你把文件放进 public 文件夹:

路由

地址 ↔ 路由(app.get / app.post) ↔ 处理函数

路由方法

1
2
3
4
5
app.get()     // 查(获取数据、页面) 
app.post() // 增(提交表单、登录)
app.put() // 改(修改数据)
app.delete() // 删(删除数据)
app.all() //【超级重点】匹配所有请求方式

路径匹配

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
// 精确匹配,只能匹配 /home/index
app.get('/home/index', (req, res) => {
res.send('<h1>这里是首页</h1>' + req.url);
});

// 字符串模糊匹配
app.get('/admin/*', (req, res) => {
res.send('<h1>这里是字符串的模糊匹配</h1>' + req.url);
});

// 正则模糊匹配(了解)
app.get(/\.html$/, (req, res) => {
res.send('<h1>正则的模糊匹配成功!</h1>' + req.url);
});

// URL中带参数 /news/23112 /news/abab
app.get('/news/:id', (req, res) => {
res.send('<h1>带参数的路径!</h1>' + req.params.id);
});

// 字符串的模糊匹配 * 表示任意数量的任意字符
// 终极404路由 写在最后 前面所有路由都没匹配到 → 显示404
app.all('*', (req, res) => {
res.status(404).send('<h1>404 您要找的页面不存在!</h1>');
});

把同一个地址的 GET 和 POST 写在一起:

1
2
3
4
5
6
7
app.route('/login') 
.get((req,res) => { // 显示页面
res.sendFile('login.html')
})
.post((req,res) => { // 提交表单
res.send('登录成功')
})

请求对象与响应对象

请求对象

1
2
3
4
5
6
req.ip // 获取客户端IP 
req.method // 获取请求方法 GET/POST...
req.query // 获取 ? 后面的参数(GET)
req.params // 获取路由动态参数 /user/:id
req.get('key') // 获取请求头
req.body // 获取 POST 提交的数据(必须配中间件)

响应对象

1
2
3
4
5
6
7
res.status(200) // 设置状态码 
res.send('内容') // 返回文本/HTML(自动处理编码)
res.json({}) // 返回 JSON 数据【写接口必用】
res.sendFile(path) // 返回一个文件(HTML页面)
res.set('key',val) // 设置响应头
res.redirect('/') // 重定向(跳转到其他地址)
res.download(path) // 下载文件

中间件

请求流程:浏览器请求 ➜ 中间件 ➜ 路由 ➜ 响应给浏览器

1
2
3
4
5
6
7
app,use('/log', (req, res, next)) => { 
// 1. 拿到请求对象 req
// 2. 操作响应对象 res
console('经过中间件');
// 3. next() 放行,往下执行
next()
}
  • 不写 next():请求卡住、页面一直转圈,后面路由永远不执行
  • 写了 next():正常放行,继续走下一个中间件 / 路由

应用级中间件
例1:要定义一个访问日志中间件
第一步,创建文件accesslog.js,作为单独模块,定义中间件的代码。

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
// accesslog.js
const moment = require('moment');
const fs = require('fs');
const path = require('path');

module.exports = (req, res, next) => {
// 从请求报文中获取信息
const ip = req.ip.slice(7);
const method = req.method;
const url = req.url;
const dt = moment().format('YYYY-MM-DD HH:mm:ss');

// 拼接日志内容
const logMsg = `${ip} ${dt} ${method} ${url}\n`;
console.log(logMsg);

// 写入文件
fs.appendFile(path.resolve(__dirname, '../logs/access.log'), logMsg, err => {
if (err) {
throw err;
}
// 成功写入日志 放行
next();
});
};

第二步,在应用的入口文件挂载中间件。

1
2
3
4
5
6
7
8
9
// 导入自定义中间件
const accessLog = require('./middleware/accesslog');

// 创建服务
const app = express();

// 在所有路由方法的前面
// 挂载访问日志中间件
app.use(accessLog);

例2:要定义一个错误处理中间件
错误处理中间件有4个参数,定义错误处理中间件时必须使用这 4 个参数。即使不需要 next 对象,也必须声明它,否则中间件会被识别为一个常规中间件,不能处理错误。
错误处理中间件需要挂载在所有路由和中间件的后面,如果路由回调函数或前面的中间件中出现错误,会自动进入错误处理中间件!

第一步:创建文件 catcherror.js,作为一个模块。

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 moment = require('moment');
const fs = require('fs');
const path = require('path');

module.exports = (err, req, res, next) => {
// 从请求报文中获取信息
const ip = req.ip.slice(7);
const method = req.method;
const url = req.url;
const dt = moment().format('YYYY-MM-DD HH:mm:ss');

// 拼接日志内容
const errMsg = `${ip} ${dt} ${method} ${url} \n ${err.stack} \n\n\n\n`;

// 写入文件
fs.appendFile(path.resolve(__dirname, '../logs/error.log'), errMsg, err => {
if (err) {
throw err;
}
});

// 响应 500
res.status(500).send('<h1>500 服务器出错!</h1>');
};

第二步:在应用的入口文件挂载中间件。

1
2
3
4
5
6
7
8
9
// 导入自定义中间件
const catchError = require('./middleware/catcherror');

// 创建服务
const app = express();

// 在所有路由方法的后面
// 挂载访问日志中间件
app.use(catchError);

应用级中间件(app)管全部,路由级中间件(router)管自己的模块。

路由模块化

可使用 express.Router 类创建模块化、可挂载的路由系统。Router实例是一个完整的中间件和路由系统,因此常称其为一个 mini-app

创建一个路由模块,加载中间件,定义一些路由,将它们挂载至应用的路径上。

第一步:创建路由文件 index.js

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 express = require('express');

// 创建路由对象
const route = express.Router();

// 路由
route.get('/', (req, res) => {
res.redirect('/index');
});

// 路由
route.get('/index', (req, res,next) => {
res.send(`
<h1>首页</h1>
<hr>
<a href="/login">登录</a>
`);
});

// 将路由对象作为暴露数据
module.exports = route;


可以继续创建路由文件 login.js

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 express = require('express');

// 创建路由对象
const route = express.Router();

// 路由
route.get('/', (req, res) => {
res.send(`
<h1>登录</h1>
<hr>
<form action="/login" method="post">
<input placeholder="请输入用户名" type="text" name="username">
<input placeholder="请输入密码" type="password" name="userpwd">
<button>提交</button>
</form>
`);
});

// 路由
route.post('/', (req, res) => {
res.send('<h2>提交成功!</h2>');
});

// 将路由对象作为暴露数据
module.exports = route;

第二步:在入口文件中,将路由文件挂载到应用上:

1
2
3
4
5
6
7
// 导入路由模块
const indexRouter = require('./routes/index');
const loginRouter = require('./routes/login');

// 挂载路由模块
app.use(indexRouter); //
app.use('/login', loginRouter); // 挂载路由 指定路径

模板引擎

项目生成器 express-generator

全局安装:

1
npm install -g express-generator

运行命令生成目录结构并指定模板引擎为 ejs:

1
express --view=ejs

安装依赖:

1
npm install

启动项目:

1
npm start

MongoDB

开始

  1. 下载地址: https://www.mongodb.com/try/download/community
  2. 配置环境变量:C:\Program Files\MongoDB\Server\5.0\bin (默认)
  3. 启动:
    (1)系统启动:此电脑->管理->服务->找到MongoDB启动
    (2)命令行启动:
1
2
mongod --dbpath "D:\CodeApp\关于前端\MongoDB\data"
# 成功标志:waiting for connections on port 27017
  1. 可视化工具Compass: https://www.mongodb.com/try/download/compass

三个重要概念
数据库(database): 数据库是一个仓库,一个 MongoDB 服务可以创建多个数据库。
集合(collection): 集合类似于 JavaScript 中的数组,一个数据库中可以创建多个集合。
文档(document): 文档是数据库中的最小单位,类似于 JavaScript 中的对象,表示一条数据信息,一个集合中可以有多个文档。

命令行工具

连接 MongoDB 服务

1
2
3
4
5
6
# 本地无密码连接
mongo
# 本地有密码
mongo 127.0.0.1:27017/数据库名 -u 用户名 -p 密码
# 远程服务器
mongo 8.8.8.8:27017/数据库名 -u 用户名 -p 密码

数据库操作

1
2
3
4
5
6
7
8
9
10
11
12
# 显示所有的数据库
show dbs
show databases

# 切换到指定的数据库 如果没有就创建
use 数据库名

# 显示当前所在的数据库
db

# 删除当前数据库
db.dropDatabase()

集合操作

1
2
3
4
5
6
7
8
9
10
11
# 创建集合
db.createCollection()

# 显示当前数据库中的所有集合
show collections

# 删除集合
db.集合名.drop()

# 重命名集合
db.集合名.renameCollection('新名字')

文档操作
数据库的基本操作包括四个,即CURD,增加(create),删除(delete),修改(update),查询(read)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 创建文档并插入到集合中
db.集合名.insert({name: 'March', age: 20, address: '四川'});

# 查询文档
db.集合名.find()
db.集合名.find(查询条件)
db.集合名.findOne(查询条件)

# 更新文档
# 更新第一个,而且是用新文档替换旧文档
db.集合名.update({name:March}, {address:'重庆'}) # 把第一个含有name:March的文档直接替换成{address;'重庆'}
# 更新指定的属性
db.集合名.update({name:'xiaole'},{$set:{age:19}})

# 删除集合中的文档
db.集合名.remove(查询条件)

条件控制

  1. 运算符
    在 mongodb 不能使用运算符,需要使用替代符号:
    • > 使用 $gt
    • < 使用 $lt
    • >= 使用 $gte
    • <= 使用 $lte
    • !== 使用 $ne
  2. 逻辑或
    $in 满足其中一个即可,$or 逻辑或的情况,$and 逻辑与的情况。
  3. 正则匹配
    条件中可以直接使用 JS 的正则语法。
1
2
3
4
5
6
7
8
db.集合名.find({age:{$in:[18,24,26]}}) 

db.集合名.find({$or:[{age:18},{age:24}]});

db.集合名.find({$and: [{age: {$lt:20}}, {age: {$gt: 15}}]});

# 使用正则
db.集合名.find({name:/March/}); # name属性里面包含March的

使用Node操作数据库

Mongoose:是 Node.js 操作 MongoDB 的工具库。之前你要写一堆 MongoDB 原生命令,麻烦、容易错。有了 Mongoose,你写 JS,它帮你搞定数据库。

1
npm install mongoose

Schema定义,模型相当于实例化。

基础操作

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
// 1. 导入 mongoose 模块
const mongoose = require('mongoose');
// 不想有警告,可以写下面代码
mongoose.set('strictQuery', false);

// 2. 连接 MongoDB 服务
mongoose.connect('mongodb://127.0.0.1:27017/project01');

// 3. 监听连接成功的事件和连接失败的事件
// 当连接成功事件触发
mongoose.connection.on('open', () => {
// 4. 创建文档结构 对应users集合
const usersSchema = new mongoose.Schema({
name: String,
age: Number,
address: String,
ctime: Date
});

// 5. 根据 schema 创建与集合对应的模型
const usersModel = mongoose.model('users', usersSchema);

// 6. 使用模型进行数据的 CURD
// 向集合中添加一个文档
usersModel.create({
name: 'March',
age: 20,
address: '四川',
ctime: Date()
}, (err, res) => {
if (err) {
console.log('添加文档失败!');
} else {
console.log('添加文档成功!添加的数据是:', res);
// 关闭连接 实际开发不需要
// mongoose.connection.close();
}
});
});

// 当连接失败事件触发
mongoose.connection.on('error', err => {
console.log('数据库连接失败!');
throw err;
});

Schema 的类型:

类型 描述
String 字符串
Number 数字
Boolean 布尔值
Array 数组,也可以使用 [] 来标识
Date 日期
Buffer Buffer 对象
Mixed 任意类型,需要使用 mongoose.Schema.Types.Mixed 指定
ObjectId 对象 ID,需要使用 mongoose.Schema.Types.ObjectId 指定
Decimal128 高精度数字,需要使用 mongoose.Schema.Types.Decimal128 指定

插入数据

1
2
3
4
5
6
7
8
9
SongModel.deleteOne({_id:'5dd65f32be6401035cb5b1ed'}, function(err, data){
console.log(err);
console.log(data);
});

// 批量插入
songsModel.create(require('./data.json').song_list, (err, res) => {
// ...
});

删除数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
SongModel.deleteOne({_id:'5dd65f32be6401035cb5b1ed'}, function(err, data){
console.log(err);
console.log(data);
});

// 删除所有符合条件的
SongModel.deleteMany({author:'Jay'}, function(err, data){
console.log(err);
console.log(data);
});

//删除所有的数据
SongModel.deleteMany({}, function(err, data){
console.log(err);
console.log(data);
});

更新数据

1
2
3
4
5
6
7
8
9
10
11
12
// 修改符合条件的第一个数据
// 这里就不是替换了 只更新了某一个属性
SongModel.updateOne({author: 'JJ Lin'}, {author: '高小乐'}, function (err, data) {
console.log(err);
console.log(data);
});

// 修改符合条件的所有数据
SongModel.updateMany({author: 'Leehom Wang'}, {author: '高小乐'}, function (err, data) {
console.log(err);
console.log(data);
});

查询数据

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
// 1.查询一条数据
SongModel.findOne({author: '高小乐'}, function(err, data){
console.log(err);
console.log(data);
});
SongModel.findById('5dd662b5381fc316b44ce167',function(err, data){
console.log(err);
console.log(data);
});

// 2.批量查询数据
// 不加条件查询
SongModel.find(function(err, data){
console.log(err);
console.log(data);
});
// 加条件查询
SongModel.find({author: '高小乐'}, function(err, data){
console.log(err);
console.log(data);
});

// 3.字段筛选
SongModel.find().select({_id:0,title:1}).exec(function(err,data){
console.log(data);
});

// 4.数据排序
// 1表示升序 -1表示降序
SongModel.find().sort({hot:1}).exec(function(err,data){
console.log(data);
});

// 5.数据截取
SongModel.find().skip(10).limit(10).exec(function(err,data){
console.log(data);
});

会话控制

HTTP协议特性: HTTP是一个无状态协议,无法区分多次请求是否来自同一客户端。服务器端只能接收和响应请求,但不会记录请求来源。

常见的会话控制解决方案有

  • Cookie: 存储在客户端的小型数据片段。
  • Session: 存储在服务器端的用户会话信息。
  • Token: 开发者自定义的认证机制(非HTTP原生)。

会话控制主要在后端实现,前端开发需要理解原理并配合后端要求进行开发。

cookie本质是一个存储在浏览器的文本键值对,随着http请求自动传递给服务器(浏览器每次给服务器发请求,都会自动把 Cookie 带上)。cookie 通过请求头和响应头实现。

完整的工作流程:
服务器以响应头的形式将如何设置 cookie 发送给浏览器。
② 浏览器收到以后会设置 cookie 并保存。
③ 浏览器再次访问服务器时,会以请求头的形式将 cookie 发送。
④ 服务器就可以通过检查浏览器发送的 cookie 来识别出不同的浏览器。

特点

  • 以网站域名为单位隔离存储
  • 不同浏览器间cookie不共享

删除 Cookie 只会让你失去当前的登录状态,不会影响你的账号密码本身。只要你输入正确的账号密码,服务器会重新验证并给你发一个新的 Cookie,让你正常登录。

在 express 中,通过配置 cookie-parser 中间件,可以将 cookie 解析为一个对象,并为 request 对象添加了一些操作 cookie 属性方法。
安装:

1
npm install cookie-parser

引入:

1
const cookieParser = require("cookie-parser");

挂载中间件:

1
app.use(cookieParser());

使用:

1
2
3
4
5
6
7
8
9
10
// 响应体设置 cookie (添加或修改) 注意:cookie的属性设置使用小驼峰
res.cookie("userName","laoli");
res.cookie("age",18,{maxAge:20*1000}); // maxAge 单位是毫秒

// 读取 cookie
req.cookies;

// 删除 cookie
res.clearCookie("userName");
res.clearCookie("age",{path:"/login"});

cookie的缺点:

  1. 各个浏览器对 cookie 的数量和大小都有不同的限制,一般数量不超过 50 个,单个大小不超过 4KB。
  2. cookie 由服务器发给浏览器,再由浏览器发回,如果 cookie 较大会导致发送速度变慢,降低用户体验。
  3. cookie 内容存储在客户端浏览器,客户端可以对 cookie 内容进行修改,安全性低。

    实际影响:不能用 Cookie 存大量数据,比如用户信息、配置、列表数据,这些都得放数据库或 localStorage。Cookie 是存在你自己电脑里的,用户可以直接在开发者工具里修改、伪造 Cookie,甚至通过恶意脚本(XSS)偷取别人的 Cookie,所以也不能在 Cookie 里存敏感数据。

session

Session 是一个对象,存储特定用户会话所需的属性及配置信息。Session是保存在服务器端的数据,保存介质可以是文件、数据库或者内存

session运行流程:

  1. 服务器中为每一次会话创建一个对象,然后每个对象都设置一个唯一的 ID。
  2. 通过设置响应头让浏览器设置 cookie 用于保存该 ID。
  3. 将会话中产生的数据统一保存到 session 对象中,这样我们就可以将用户的数据全都保存到服务器中,而不需要保存到客户端,客户端只需要保存一个 ID 即可。

![[Pasted image 20260511213526.png]]
![[Pasted image 20260511214210.png]]

完整工作流程:

  1. 用户第一次访问网站
    服务器发现你没有登录,会:
    • 在服务器上创建一个 Session 对象。
    • 给这个 Session 分配一个唯一的 Session ID
    • Session ID 通过 Set-Cookie 响应头发给浏览器。
  2. 浏览器保存 Cookie
    浏览器收到响应后,把这个包含 Session ID 的 Cookie 存在本地。
  3. 用户再次访问网站
    浏览器会自动把 Cookie 里的 Session ID 发给服务器。
  4. 服务器根据 ID 找到 Session
    服务器拿到 Session ID,在自己的 Session 池里找到对应的 Session 对象,读取里面的用户数据(比如 “这是已登录的用户March”),然后返回对应的页面。

豆包讲session与cookie的关系 https://www.doubao.com/thread/w28f1678377a5dfeb

在 express 中,通过配置 express-session 中间件,可以将 cookie 解析为一个对象,并为 request 对象添加了一些操作 cookie 属性方法。
安装:

1
npm install express-session

引入:

1
var session = require("express-session");

挂载中间件:

1
2
3
4
5
6
7
8
9
10
app.use(session({
name: 'sess', // 设置cookie的name,默认值是:connect.sid
secret: 'atguigu', // 参与加密的字符串(又称签名)
saveUninitialized: false, //是否为每次请求都设置一个 cookie 用来存储 session 的 id
resave: false ,// 强制保存 session 即使它并没有变化, 默认为 true,建议设置成 false。
cookie: {
httpOnly: true, // 开启后前端无法通过 JS 操作
maxAge: 1000*30 // 这一条 是控制 sessionID 的过期时间的 默认是浏览器一关就没了
}
}));

(豆包讲httpOnly: https://www.doubao.com/thread/wef0a665fd824bd83
使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
app.get("/setSession",function (req,res){
req.session.userName = "March"; // 设置 session
res.send("设置成功");
})
app.get("/getSession",function (req,res){
console.log(req.session.userName)
res.send("获取成功")
})
app.get("/delSession",function (req,res){
req.session.destroy(function (){
res.send("删除成功");
})
})

修改session存储位置
session对象的store属性可以设置session保存在内存、文件或其他存储介质。(默认session会存储在服务器的内存中,重启服务器 session 就没了)

1.保存在文件中: 需要实现文件的读写操作,包括创建文件、写入session数据、读取时查找对应客户端的session。

安装包: 会话-文件-存储 - NPM

1
npm install session-file-store
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 导入包
const FileStore = require('session-file-store')(session);

app.use(session({
name: 'sess',
secret: 'atguigu',
saveUninitialized: false,
resave: false ,
cookie: {
httpOnly: true,
maxAge: 1000*30
}
store: new FileStore(), // 增加这一行
}));
// 结果:自动添加session文件夹 里面存着不同ip用户的session文件 重启服务器,session不会丢失

2.保存在MongoDB中
安装包: Connect-Mongodb-Session - NPM

1
npm install connect-mongodb-session
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 导入包
const MongoDBStore = require('connect-mongodb-session')(session);

// 设置处理session的中间件 并进行session的初始设置
app.use(session({
  name: 'sess',      
  secret: 'atguigu',
  saveUninitialized: false,
  resave: true ,
  cookie: {
    httpOnly: true,
      maxAge: 1000*3600*24*7
  },
  // 增加以下几行
  store: new MongoDBStore({
    uri: 'mongodb://127.0.0.1:27017/account-project-auth',
    collection: 'mySessions'
  })
}));

登录与注册

记账本项目:

  1. 用户路由
    ① 注册页面
    GET /users/reg
    ② 执行注册
    POST /users/reg
    ③ 登录页面
    GET /users/login
    ③ 执行登录
    POST /users/login
    ④ 退出登录
    GET /users/logout

  2. 注册用户
    数据库创建 users 集合,创建对应的 schema、model,将用户信息添加到集合中。用户名是唯一的,否则无法注册成功。密码经过md5加密之后存储在数据库

  3. 登录
    根据提交的用户名和密码从数据库查找,如果找到说明存在用户,登录成功,如果找不到,登录失败
    登录成功之后,将用户信息存储在session中

  4. 全局登录验证
    有些页面只有登录之后才能访问,如果没有登录,跳转到登录页。

    哪些必须登录之后才允许访问?
    GET /account
    GET /account/create
    POST /account/create 在account路由前面加中间件如果有session信息执行next()
    GET /account/delete/:id 没有的话重定向。
    哪些不需登录就可以访问?
    GET /users/reg
    POST /users/reg
    GET /users/login
    POST /users/login
    GET /users/logout

    如何验证是否登录?
    看能否获取到session 如果没有就重定向到登录页
    将用户信息显示在账单首页上

  5. 退出登录
    删除session

  6. 账单
    账单集合,每个文档添加一个属性,记录用户id
    添加账单的时候,将用户id添加进去
    这样查询账单只会查询该用户的账单

Ajax

Ajax 全称为 Asynchronous Javascript And XML,就是异步的 JavaScrript 和 XML。

Ajax 是浏览器自带的技术,不用刷新整个网页,就能在后台异步偷偷和服务器收发数据,实现网页局部更新。

静态网页与动态网页
静态网页:内容提前写死在 HTML 里,所有人打开看到的都一样,不连数据库、不跟服务器实时运算
动态网页:内容是服务器实时临时生成的,可以连数据库、判断登录身份,不同人看到的内容可以不一样

原生Ajax四步流程

◆原生Ajax四步流程:

  • new XMLHttpRequest()
    创建浏览器自带的请求工具对象,相当于造一个通信小助手。
  • 绑定 onload 事件
    提前说好:等后端数据返回来之后,要做什么操作(拿数据、改页面)。
  • xhr.open (请求方式,接口地址)
    给请求设置规则:用 GET/POST、访问哪个后端接口,只是配置,还没发请求
  • xhr.send()
    真正发送请求,去找后端要数据。
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
<h1>ajax 基本流程</h1>
<hr>
<button id="btn">加载</button>
<div id="box"></div>
-------------------------------------------------------------
<script>
const btn = document.querySelector('#btn');
const box = document.querySelector('#box');
btn.onclick = () => {
// 第一步 创建 XMLHttpRequest 对象 浏览器给你内置的专门用来发网络请求的工具
const xhr = new XMLHttpRequest();

// 第二步 监听接响应成功的事件 responseText 就是服务器给的数据
xhr.onload = () => {
console.log('成功接收到后端的响应!');
box.innerHTML += xhr.responseText + '<br>';
};

// 第三步 请求初始化
/*
第一个参数 请求方式
第二个参数 请求URL
第三个参数 是否异步,默认异步
*/
xhr.open('GET', 'http://127.0.0.1:8080/getData'); // open() 告诉xhr以什么方式去哪里拿数据

// 第四步 发送请求
/*
参数可以设置请求体,没有请求可以不设置参数
*/
xhr.send(); // 出发去后端拿数据了
}
</script>

后端路由匹配,只看 ? 前面的路径,不看 ? 后面的参数。

  • ? 前面:路由路径 → 用来匹配后端 app.get('/addData')
  • ? 后面:参数 → 不算路由路径,只是附带的数据
    req.query 干一件事:自动解析 ? 后面的 键=值&键=值,变成一个 JS 对象。

前端->后端 后端接收使用
GET 数据拼在 URL 上 =====> req.query
POST 数据放请求体里 =====> req.body
后端返回统一 res.send → 前端 responseText 接收。

1
2
3
// 设置请求头  请求头一定要在 open() 之后,send() 之前设置
xhr.setRequestHeader('Content-type','application/x-www-form-urlencoded'); // 表单数据格式
xhr.setRequestHeader('Content-type','application/json'); // 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
// 点击按钮向后端发送 GET 请求
const btn1 = document.querySelector('#btn1');
btn1.onclick = () => {
// 定义查询字符串
const qs = `username=${nameInp.value}&userpwd=${pwdInp.value}`;
// 请求初始化 数据拼在URL上
xhr.open('GET', '/addData?'+qs)
// 发送请求
xhr.send();
};

// 点击按钮向后端发送 POST 请求
const btn2 = document.querySelector('#btn2');
btn2.onclick = () => {
// 定义查询字符串
    const qs = `username=${nameInp.value}&userpwd=${pwdInp.value}`;
    // 初始化
    xhr.open('POST', '/addData?type=10');
    // 发送请求  请求体是字符串 使用querystring 格式
    xhr.send(qs)
    }
   
// 点击按钮向后端发送 POST 请求, 设置请求头 指定请求体的 Content-type json
const btn4 = document.querySelector('#btn4');
btn4.onclick = () => {
// 将表单数据放在对象中
const data = {username: nameInp.value, userpwd: pwdInp.value};
// 初始化
xhr.open('POST', '/addData?type=20');
// 设置请求体 JSON数据格式
xhr.setRequestHeader('Content-type', 'application/json');
// 发送请求
xhr.send(JSON.stringify(data))
}

data 是 JS 对象,网络传输只能发字符串,不能直接发对象。JSON.stringify(对象) 就是把 JS 对象转成 JSON 格式字符串。

1
2
// 配置中间件
app.use(bodyParser.json());

后端路由代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 接收 get 请求
app.get('/addData', (req, res) => {
// 控制台输出
console.log('接收到 GET 请求:')
console.log('url:', req.url);
console.log('从url中提取数据:', req.query);
console.log('');
// 给前端的内容
res.send('GET 方式提交成功!');
});

// 接收 post /addData
app.post('/addData', (req, res) => {
// 控制台输出
console.log('接收到 POST 请求:')
console.log('URL中获取的信息:', req.query);
console.log('请求体内容类型:', req.headers['content-type']);
console.log('请求体中获取的信息:', req.body, typeof req.body);
console.log('');
// 给前端的内容
res.send('POST 方式提交成功!');
});

FormData

请求体(POST/PUT)send()的方法的参数)除了是字符串,也可以是 formData 对象。
如果请求体是 FormData 对象,浏览器会自动设置请求头字段 Content-typemultipart/form-data

FormData是浏览器原生自带的表单数据对象,不用自己拼接key=value、也不用手动转JSON。它可以快速组装表单键值对,发AJAX不用手动设置请求头,浏览器自动处理。最大用处是支持文件/图片上传,日常提交表单、传文件都用它,比手写参数简单太多。

使用 FormData 对象作为请求体

1
2
3
4
5
6
7
8
9
// 方式一 创建 空的 FormData,再添加数据,可以添加文件数据或者字符串数据
var fd = new FormData();
fd.append('message', msgInput.value);
fd.append('userpwd', pwdInp.value);
fd.append('avator', avatorInp.files[0]);

// 方拾二 根据表单元素创建 FormData 会包含表单中所有的信息
const formData = document.querySelector('form');
const fd = new FormData(formData);

FormData 对象的方法

append()
set()
delete()
get()
getAll()

FormData 实现文件上传

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// server.js
// 上传文件
app.get('/page03', (req, res) => {
res.sendFile(path.join(__dirname, '03-FormData.html'));
});

const upload = multer({ dest: 'uploads/' })

// upload.single(avatar)是 multer 提供的中间件,意思是接收单个文件。
// avatar 必须和前端 FormData 里 fd.append('avatar', 文件对象) 的键名完全一致!
app.post('/upload', upload.single('avator'),(req, res) => {
// 后端控制台打印
console.log('文件上传成功:');
console.log('文件信息:', req.file);
console.log('表单数据:', req.body);
console.log('');
res.send('文件上传成功!');
})

multer 是 Express 里专门用来处理 multipart/form-data 类型(也就是文件上传)的中间件。{ dest: 'uploads/' } 意思是:上传的文件,先临时存到 uploads/ 文件夹里,这里的这个文件夹要自己手动创建,不然会报错!

✅ 非 Ajax:
HTML 写 form + enctype=”multipart/form-data”
浏览器自动封装文件,不用 JS,但是会刷新页面
✅ Ajax:
自己 new FormData () + append 添加文件
不用写 enctype,无刷新上传

读取响应报文

1
2
3
4
5
6
7
8
9
10
11
12
// 响应行
xhr.status; // 响应状态码
xhr.statusText; // 响应状态描述

// 响应头
xhr.getResponseHeader('Content-type');// 获取指定字段的响应头的信息
xhr.getResponseHeader('Date')
xhr.getAllResponseHeaders(); // 获取所有的响应头信息

// 响应体
xhr.responseText; // 获取响应体体符串
xhr.response; // 获取响应体字符串,如果响应体是特殊格式的字符串,会进行处理

响应JSON数据

服务端设置
设置响应头,告知浏览器响应体的内容类型是 json 格式。

1
Content-type: application/json;charset=utf-8

客户端处理接收到的 json 数据
方式一 使用 JSON.parse。

1
2
3
4
5
// 监听响应结束的回调函数、
xhr.onload = function() {
// 将响应体中 json 格式的字符串处理成对象
var resData = JSON.parse(xhr.responseText);
}

方式二 设置 xhr.responseType 属性 ,通过 xhr.response获取。

1
2
3
4
xhr.responseType = 'json';
xhr.onload = function() {
xhr.response; // 直接得到处理好的对象
}

响应超时

1
2
3
4
5
6
7
8
9
10
11
12
13
 // 监听响应超时的事件
xhr.ontimeout = function(){
alert('响应超时!');
}

// 请求初始话
xhr.open('GET', '/getInfo');

// 发送请求之前设置超时时间
xhr.timeout = 5000;

// 发送
xhr.send();

异步与同步请求

  1. 异步:发请求后代码继续往下跑,响应回来再执行回调,页面不卡死,日常开发全用它
  2. 同步:发请求后代码暂停卡住,必须等响应完毕才继续执行,页面冻结卡顿,极少使用
  3. 开关写法
1
2
xhr.open('GET','地址',true) // 异步(默认)
xhr.open('GET','地址',false)// 同步

XMLHttpRequest 对象总结

① XHR 对象概述

1)XMLHttpRequest 对象简称 XHR 对象。

2)XMLHttpRequest 对象提供了对 HTTP 协议的完全的访问,包括做出 POST 和 HEAD 请求以及普通的 GET 请求的能力。

3)XMLHttpRequest 可以同步或异步地返回 Web 服务器的响应,并且能够以文本或者一个 DOM 文档的形式返回内容。

4)尽管名为 XMLHttpRequest,它并不限于和 XML 文档一起使用,它可以接收任何形式的文本文档。

② 创建 XHR 对象

使用构造函数 XMLHttpRequest 就可以创建一个 XHR 对象。

1
const xhr = new XMLHttpRequest();

注意:在古老的 IE 浏览器中(如:IE6),需要使用其他方式来创建 XHR 对象。

1
2
// IE5、IE6
let xhr = new ActiveXObject("Microsoft.XMLHTTP");

③ XHR 对象的属性

属性名 含义
readyState 返回一个数字,表示请求的状态:
0 – UNSET – XHR对象已创建或已被 abort() 方法重置。
1 – OPENDED – open() 方法已经被调用。
2 – HEADERS_RECEIVED – send() 方法已经被调用,并且响应头和响应状态已经可获得。
3 – LOADING – 下载中, responseText 属性已经包含部分数据。
4 – DONE – 所有响应数据接收完毕。
status 响应状态码,如 404、200 等。
statusText 响应状态码的文本描述,如 200 对应的是 “OK”。
responseXML 接收格式为 XML 的响应数据,返回一个 document 对象。
responseText 获取响应文本,返回一个字符串。
responseType 用于设置响应内容的类型 xhr2
response 返回的类型取决于 responseType 的设置。 xhr2
timeout 设置超时时间。xhr2

④ XHR 对象的方法

方法名 含义
open() 初始化 HTTP 请求,用来指定请求方式和 URL。 xhr.open(method, url, [async], [user], [password])
send() 发送 HTTP 请求,参数可以设置请求体,没有请求体无需设置参数。
setRequestHeader() 设置 HTTP 请求头的值。必须在 open() 之后、send() 之前调用。
abort() 如果请求已被发出,则立刻中止请求。
getAllResponseHeaders() 以字符串形式返回所有的响应头。
getResponseHeader() 返回指定的响应头。

⑤ XHR 对象的事件

事件名 含义
readystatechange readyState 属性值发生变化触发该事件。
abort 请求终止时触发。
error 请求遇到错误时触发。注意,只有发生了网络层级别的异常才会触发此事件。
loadstart 接收到响应数据时触发。 xhr2
load 请求成功完成时触发。xhr2
loadend 当请求结束时触发, 无论请求成功 ( load) 还是失败 (aborerror)。xhr2
progress 当请求接收到更多数据时,周期性地触发。xhr2
timeout 在预设时间内没有接收到响应时触发。xhr2

跨域

浏览器同源策略规定:域名、端口、协议三者任一不同,就是跨域。它是浏览器的安全防护规则。这个策略要求ajax代码所在的页面URL中的协议、域名、端口号与ajax请求的URL中协议、域名、端口号保持一致。

同源三要素:协议 + 域名 + 端口

✅ 完全一致,不跨域
页面:http://127.0.0.1:5500/index.html
请求:http://127.0.0.1:5500/api/data

❌ 端口不同,跨域
页面:http://127.0.0.1:5500
请求:http://127.0.0.1:3000

❌ 域名不同,跨域
页面:http://localhost
请求:http://192.168.1.100

❌ 协议不同,跨域
页面:http://xxx
请求:https://xxx

请求发过去了,服务端不在乎同源还是不同源。数据返程抵达浏览器后,浏览器才会执行同源校验,不符规则就拦截数据,前端代码拿不到结果,会报错。

那么如何进行跨域资源访问?

CORS跨域资源共享

一句话,让后端服务器在响应头里加一句 “允许跨域”,浏览器就放行!

在服务端进行设置,添加一个响应头,设置允许的域名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
res.set("Access-Control-Allow-Origin", "http://localhost:8080")

// 允许所有的域名都可以跨域
res.setHeader("Access-Control-Allow-Origin", "*")

// 只对白名单内域名开放跨域权限
const allowOrigin = [
'http://127.0.0.1:5500',
'http://localhost:8080',
'https://xxx.com'
];

app.use((req, res, next) => {
const origin = req.headers.origin;
// 匹配名单内地址才放行
if (allowOrigin.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}

res.setHeader('Access-Control-Allow-Methods','GET,POST,PUT,DELETE');
res.setHeader('Access-Control-Allow-Headers','Content-Type');
next();
});

JSONP

JSONP(JSON with Padding),是一个非官方的跨域解决方案,纯粹凭借程序员的聪明才智开发出来,只支持 GET 请求方式

在网页有一些标签天生具有跨域能力,比如:img link iframe script,JSONP就是利用 script 标签的跨域能力来发送请求的。

浏览器不拦截 <script> 跨域。只创建 script 不插入页面 = 不请求,插入 body 瞬间浏览器才请求 src。
后端不能返回普通 JSON,必须返回 函数名(数据) 格式。

JSONP 使用步骤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 1.动态的创建一个script标签
var script = document.createElement("script");

// 2.设置script的src
script.src = "http://localhost:3000/testAJAX?callback=abc";

// 3. 定义函数
function abc(data) {
alert(data.name);
};

// 4.将script添加到 body 中,会发送请求
document.body.appendChild(script);

// 5. 将 script 从 body 中删除
document.body.removeChild(script);

如果请求回来后,函数还没定义 → 报错

  • 你插入 script → 请求瞬间飞出去
  • 服务器超快返回:getData(数据)
  • 浏览器立刻执行:getData(...)
  • 但此时 getData 函数还没定义的话!
  • 浏览器懵逼:getData is not defined报错!

服务端的处理
服务端需要将 js 代码作为响应体:

1
2
3
4
5
6
7
8
9
// 1. 获取前端传过来的回调函数名称
var callback = req.query.callback;
// 2. 要返回的数据
var obj = {
name:"孙悟空",
age:18
}
// 3.返回一段js代码
res.send(callback+"("+JSON.stringify(obj)+")");

案例:搜索框实时提示

Promise

Promise基础

  1. 实例化 Promise 类,需要传一个回调函数作为参数。
  2. Promise 类的回调函数(参数),在实例化 Promise 的时候会自动调用,是同步任务。
  3. Promise 类的回调函数(参数),在被调用的时候,会接收两个参数,两个参数都是函数。

第一点什么意思??
① 使用 Promise 构造函数创建 promise 对象

1
2
3
4
5
 new Promise((res, rej) => {
console.log('第一个参数:', res);
console.log('第二个参数:', rej);
});
// 第一个参数:[Function (anonymous)] // 第二个参数:[Function (anonymous)]

第三点怎么理解??
② 修改 promise 对象的状态
JS 会自动给你传进来两个函数

  • 第一个叫 resolve
  • 第二个叫 reject
    你可以随便改名,但作用固定
  • resolve() → 代表成功
  • reject() → 代表失败
1
2
3
4
5
6
7
8
9
10
11
new Promise((resolve, reject) => {
// 调用第一个参数 该promise对象的状态改为 resolved(fulfilled)
// 可以传个参数作为 PromiseResult
// resolve('hello');
// resolve({status:'OK', msg: 'success'});

// 调用第二个参数, 该promise对象的状态改为 rejected
// 可以传个参数作为 PromiseResult
// reject();
reject([10,20,30,40]);
});

③ 为 promise 对象设置回调函数

  1. promise对象的then方法第第一个参数(回调函数),状态变为成功会执行,可以通过形参得到 PromiseResult。
  2. promise对象的then方法第第二个参数(回调函数),状态变为失败会执行,可以通过形参得到 PromiseResult。
  3. then 方法的两个回调函数都是异步执行!

你在 Promise 里写:

1
2
3
4
new Promise((resolve, reject) => {
resolve("成功数据"); // 成功
// reject("失败原因"); // 失败
})

就会在 then 里收到:

1
2
3
4
5
.then(
res => { console.log(res); }, // 输出 "成功数据"
// res 就是 resolve (这里面的值)
err => { console.log(err); }
)

Promise核心的两个方法

  • .then() → 成功时执行
  • .catch() → 失败时执行

写法超级干净:

1
2
3
请求() 
.then(结果 => { 成功 })
.catch(错误 => { 失败 })

then 方法

参数

  1. 第一个参数,是一个回调函数,当promise对象的状态改为成功的时候,会被调用,并接收到参数 PromiseResult。
  2. 第一个参数,是一个回调函数,当promise对象的状态改为失败的时候,会被调用,并接收到参数 PromiseResult。

返回值
then() 方法的返回值是一个 Promise 对象,该 Promise 对象的状态取决于 then() 方法回调函数的返回值(then 可以设置两个回调函数,哪个回调函数执行就取决于谁)。

  1. 情况一:回调函数没有返回值, then()返回的Promise对象改为成功状态,PromiseResult是undefined
  2. 情况二:返回非Promise类型的对象或原始类型数据then()返回的Promise对象改为成功状态,PromiseResult是该回调的返回值
  3. 情况三:返回Promise对象then()返回的Promise对象与该回调返回的Promise对象,状态和PromiseResult保持一致
  4. 情况四:出现代码运行错误, then()返回的Promise对象,状态改为失败,PromiseResult是错误对象

链式调用
以前的回调地狱:

1
2
3
4
5
6
请求1(function(){ 
请求2(function(){
请求3(function(){
})
})
})

嵌套太深,像金字塔,难看难维护。

由于then()方法返回的仍然是一个 promise 对象,所以支持链式调用,then() 的链式调用可以解决回调地狱的问题。

  • 每一个 .then () 都会返回一个新的 Promise
  • 在 .then () 里 return 一个值 → 下一个 .then () 直接收到
  • 任何一步报错,都会直接跳到最近的 .catch (),后面的 then 都不执行!(异常穿透)

catch 方法

参数
需要一个回调函数作为参数,Promise对象的状态改为失败的的时候,执行该回调函数。

finally 方法

1
2
3
.finally( () => {
console.log('finally');
});

Promise构造函数本身的方法

先分清两类东西:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 1. 构造函数:Promise
// 它身上的方法叫「静态方法」
Promise.all()
Promise.race()
Promise.resolve()
Promise.reject()
Promise.allSettled()
Promise.any()

// 2. 实例对象:new Promise(...)
// 它身上的方法叫「实例方法」
const p = new Promise(...);
p.then()
p.catch()
p.finally()

Promise 构造函数本身的方法就是静态方法

async 与 await

asyncawait 关键字让我们可以用一种更简洁的方式写出基于 Promise 的异步行为,而无需刻意地链式调用。

① 定义一个 async 函数
任何形式的函数,声明的时候,添加 async 关键字就可以变为 async 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 // 1. function 关键字方式
async function fn01() {}

// 2. 表示式方式
const fn02 = async function() {};

// 3. 箭头函数
const fn03 = async () => {};

// 4. 立即执行的函数
(async () => {

})();

// 5. 对象中的方法
const user = {
async say() {}
}

Axios