Node.js
Node简介
/*
Node.js
- 运行在服务器的js
- 用来编写服务器
- 特点:
- 单线程、异步、非阻塞
- 统一API
nvm
- 命令
nvm list - 显示已安装的node版本
nvm install 版本 - 安装指定版本的node
配置nvm的镜像服务器
nvm node_mirror https://npmmirror.com/mirrors/node/
nvm use 版本 - 指定要使用的node版本
node.js和JavaScript有什么区别
ECMAScript(node有) DOM(node没有) BOM(node没有)
*/
console.log("hello node");
异步
/*
进程和线程
- 进程(厂房)
- 程序的运行的环境
- 线程(工人)
- 线程是实际进行运算的东西
同步
- 通常情况下代码都是自上向下一行一行执行的
- 前边的代码不执行后边的代码也不会执行
- 同步的代码执行会出现阻塞的情况
- 一行代码执行满会影响到整个程序的执行
解决同步问题:
- java puthon
- 通过多线程来解决
- node.js
- 通过异步方式来解决
异步
- 一段代码的执行不会影响到其他的程序
- 异步的问题:
异步的代码无法通过return来设置返回值
- 特点:
1.不会阻塞其他代码的执行
2.需要通过回调函数来返回结果
- 基于回调函数的异步带来的问题
1.代码的可读性差
2.课调试性差
- 解决问题:
- 需要一个东西,可以代替回调函数来给我们返回结果
- Promise横空出世
- Promise是一个可以用来存储数据的对象
Promise存储数据的方式比较特殊,这种特殊方式使得Promise可以用来存储异步调用的数据
*/
Promise
/*
异步调用必须通过回调函数来返回数据,当我们进行一些复杂的调用时,会出现“回调地狱”
问题:
异步必须通过回调函数来返回结果,回调函数一多就很痛苦
Promise
- Promise可以帮助我们解决异步中的回调函数的问题
- Promise就是一个用来存储数据的容器
它拥有着一套特殊的存取数据的方式
这个方式使得它里面可以存储异步调用的结果
*/
// 创建Promise时,构造函数中需要一个函数作为参数
// Promise构造函数的回调函数,它会在创建Promise时调用,调用时会有两个参数传递进去
const promise = new Promise((resolve, reject) => {
/*
resolve 和 reject 是两个函数,通过这两个函数可以向Promise中存储数据
resolve在执行正常时存储数据,reject在执行错误时存储数据
通过函数来向Promise中添加数据,好处就是可以用来添加异步调用的数据
*/
// throw new Error("hhh");
resolve("resolve");
// reject("reject");
});
/*
从Promise中读取数据
- 可以通过Promise的实例方法then来去读Promise中存储的数据
- then需要两个回调函数作为参数,回调函数用来获取Promise中的数据
通过resolve存储的数据,会调用第一个函数返回
可以在第一个函数中编写处理数据的代码
通过reject存储的数据或者出现异常时,会调用第二个函数返回数据
可以在第二个函数中编写处理异常的代码
*/
promise.then((result) => {
console.log(result);
}, (reason) => {
console.log(reason);
});
/*
Promise中维护了两个隐藏属性:
PromiseResult
- 用来存储数据
PromiseState
- 记录Promise的状态(三种状态)
pending(进行中)
fulfilled(完成) 通过resolve存储数据时
rejected(拒绝,出错了) 出错了或通过reject存储数据时
- state只能修改一次,修改以后永远不会再变
流程:
当Promise创建时,PromiseState初始值为pending,
当通过resolve存储数据时 PromiseState 变为fulfilled(完成)
PromiseResult变为存储的数据
当通过reject存储数据或出错时 PromiseState rejected(拒绝,出错了)
PromiseResult变为存储的数据 或 异常对象
当我们通过then读取数据时,相当于为Promise设置了回调函数,
如果PromiseState变为fulfilled,则调用then的第一个回调函数来返回
如果PromiseState变为rejected,则调用then的第二个回调函数来返回
*/
const promise2 = new Promise((resolve, reject) => {
resolve("哈哈");
});
/*
catch() 用法和then类似,但是只需要一个回调函数作为参数
catch()中的回调函数只会在Promise被拒绝时才调用
catch() 相当于 then(null, reason => {})
catch() 就是一个专门处理Promise异常的方法
finally()
- 无论是正常存储数据还是出现异常了,finally总会执行
- 但是finally的回调函数中不会接收到数据
- finally()通常用来编写一些无论成功与否都要执行的代码
*/
promise2.finally(() => {
console.log(222);
});
/*
Promise就是一个用来存储数据对象
但是由于Promise存取的方式的特殊,所以可以直接将异步调用的结果存储到Promise中
*/
const promise = new Promise((resolve, reject) => {
reject("1111");
});
/*
对Promise进行链式调用时
后边的方法(then和catch)读取的上一步的执行结果
如果上一步的执行结果不是当前想要的结果,则跳过当前的方法
*/
/*
当Promise出现异常时,而整个调用链中没有出现catch,则异常会向外抛出
*/
promise
.then(r => console.log("第一个then", r))
.catch(r => {
console.log("异常处理", r); // 异常处理 1111
return "异常返回数据";
})
.then(r => console.log("第二个then", r)); // 第二个then 异常返回数据
/*
promise中的
then (return new Promise())
catch
- 这三个方法都会返回一个新的Promise,Promise中会存储回调函数的返回值
finally
- finally的返回值,不会存储到新的Promise中
*/
function sum(a, b) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(a + b);
}, 1000);
});
}
sum(123, 456)
.then(result => result + 7)
.then(result => result + 8)
.then(result => console.log(result)); // 594
/*
静态方法
Promise.resolve() 创建一个立即完成的Promise
Promise.reject() 创建一个立即拒绝的Promise
Promise.all([...]) 同时返回多个Promise的执行结果
其中有一个报错,就返回错误
Promise.allSettled() 同时返回多个Promise的执行结果(无论成功或失败)
{status: 'fulfilled', value: 912}
{status: 'rejected', reason: '哈哈'}
Promise.race([...]) 返回执行最快的Promise(不考虑对错)
Promise.any([...]) 返回执行最快完成的Promise,如果全失败,则返回错误
*/
function sum(a, b) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(a + b);
}, 1000);
});
}
// Promise.all([...])
Promise.all([sum(123, 456), sum(456, 456), sum(789, 456)]).then(r => {
console.log(r); // [579, 912, 1245]
});
Promise.allSettled([sum(123, 456), sum(456, 456), sum(789, 456), Promise.reject("哈哈")]).then(r => {
console.log(r);
});
/*
0:
{status: 'fulfilled', value: 579}
1:
{status: 'fulfilled', value: 912}
2:
{status: 'fulfilled', value: 1245}
3:
{status: 'rejected', reason: '哈哈'}
*/
/*
JS是单线程的,它的运行时基于基于事件循环机制(event loop)
- 调用栈
- 栈
- 栈是一种数据结构,后进先出
- 任务队列
- 队列
- 队列也是一种数据结构,先进先出
- 任务队列的是将要执行的代码
- 当调用栈中的代码执行完毕后,队列中的代码才会安装顺序依次进入到栈中执行
- 在JS中任务队列有两种
- 宏任务队列(大部分代码都去宏任务队列中排队)
- 微任务队列(Promise的回调函数(then、catch、finally))
- 整个流程
1.执行调用栈中的代码
2.执行微任务队列中的所有任务
3.执行宏任务队列中的所有任务
*/
// 定时器的作用是间隔一段时间后,将函数放入到宏任务队列中
/*
Promise的执行原理
- Promise在执行时,then就相当于给Promise绑定了回调函数
当Promise的状态从pending 变为 fulfilled时,then的回调函数会被放入到微任务队列中
*/
/*
queueMicrotask() 用来向微任务队列中添加一个任务
*/
setTimeout(() => {
console.log(3);
});
queueMicrotask(() => {
console.log(1);
});
console.log(2); // 2 1 3
async和await
// 通过async可以快速的创建异步函数
/*
通过async可以来创建一个异步函数
异步函数的返回值会自动封装到一个Promise中返回
在async声明的异步函数中可以使用await关键字来调用异步函数
*/
async function fn2() {
return 10;
}
console.log(fn2()); // Prmoise
/*
Promise解决了异步调用中回调函数问题
虽然通过链式调用解决了回调地狱,但是链式调用太多以后还是不好看
我多想以同步的方式去调用异步的代码
*/
function sum(a, b) {
return new Promise(resolve => {
setTimeout(() => {
resolve(a + b);
}, 1000);
})
}
async function fn3() {
/*
当我们通过await去调用异步函数时,它会暂停代码的运行
直到异步代码执行有结果时,才会将结果返回
注意 await 只能更用于 async声明的异步函数中,或es模块的顶级作用域中
await阻塞的只是异步函数内部的代码,不会影响外部代码
通过await调用异步代码时,需要通过try-catch来处理异常
*/
try {
let result = await sum(123, 456); // 579
console.log(result);
} catch (e) {
console.log("出错了");
}
}
fn3();
// 如果async声明的函数中没有写await,那么它里面就会依次执行
async function fn4() {
console.log(1);
console.log(2);
console.log(3);
}
// fn4和fn5等价
function fn5() {
return new Promise(resolve => {
console.log(1);
console.log(2);
console.log(3);
resolve();
});
}
// fn4();
// console.log(4); // 1 2 3 4
async function fn6() {
console.log(1);
/*
当我们使用await调用函数后,当前函数后边的所有代码
会在当前函数执行完毕后,被放入到微任务队列中
*/
await console.log(2);
console.log(3);
}
function fn7() {
return new Promise(resolve => {
console.log(1);
console.log(2);
resolve();
}).then(r => {
console.log(3);
});
}
// fn6和fn7 等价
fn6();
console.log(4); // 1 2 4 3
模块化
CommonJS引用模块
/*
早期的网页中,是没有一个实质的模块规范的
我们实现模块化的方式,就是最原始的通过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导出模块
/*
在定义模块时,模块中的内容默认是不能被外部看到的
可以通过exports来设置要向外部暴露的内容
访问exports的方式有两种:
exports
module.exports
- 当我们在其他模块中引入当前模块时,require函数返回的就是exports
- 可以将希望暴露给外部模块的内容设置为exports的属性
*/
/*
可以通过exports 一个一个的导出值
也可以直接通过module.exports同时导出多个值
*/
// exports.a = "孙悟空";
module.exports = {
a: "haha"
}
module.exports = {
name: "孙悟空",
age: 18,
gender: "男"
}
CommonJS原理
/*
所有的CommonJS的模块都会被包装到一个函数中
(function(exports, require, module, __filename, __dirname){
});
*/
console.log(arguments);
console.log(__filename); // __filename表示当前模块的绝对路径
console.log(__dirname); // 当前模块所在目录的路径
ES模块
/*
默认情况下,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模块化,在浏览器中同样支持,但是通常我们不会直接使用
通常都会结合打包工具使用
*/
/*
ES 模块化
*/
// 向外部导出内容
export let a = 10;
export let b = "哈哈";
export let c = true;
// 设置默认导出
export default function sum(a, b) {
return a + b;
}
核心模块
path
/*
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");
fs
/*
fs(File System)
- fs用来帮助node来操作磁盘中的文件
- 文件操作也是所谓的I/O,input output
- 使用fs模块,同样需要引入
readFileSync() 同步的读取文件的方法,会阻塞后边代码的执行
当我们通过fs模块读取磁盘中的数据时,读取到的数据总会以Buffer对象的形式返回
Buffer是一个临时用来存储数据的缓冲区
readFile() 异步的读取文件的方法
*/
const fs = require("node:fs");
const buf = fs.readFileSync(path.resolve(__dirname, "./hello.txt"));
console.log(buf.toString()); // hello world今天天气真不错
fs.readFile(path.resolve(__dirname, "./hello.txt"), (err, buffer) => {
if (err) {
console.log("出错了");
} else {
console.log(buffer.toString()); // hello world今天天气真不错
}
});
/*
Promise版本的fs的方法
*/
const fs = require("node:fs/promises");
fs.readFile(path.resolve(__dirname, "./hello.txt"))
.then(buffer => console.log(buffer.toString()))
.catch(e => console.log("出错了"));
(async () => {
try {
const buffer = await fs.readFile(path.resolve(__dirname, "./hello.txt"));
console.log(buffer.toString());
} catch (e) {
console.log("出错了");
}
})();
const fs = require("node:fs/promises");
const path = require("node:path");
/*
fs.readFile() 读取文件
fs.appendFile() 创建新文件,或将数据添加到已有文件中
fs.mkdir() 创建目录
fs.rmdir() 删除目录
fs.rm() 删除文件
fs.rename() 重命名 (剪切)
fs.copyFile() 复制文件 (复制)
*/
fs.appendFile(path.resolve(__dirname, "./hello.txt"), "今天天气真不错")
.then(r => {
console.log("添加成功");
});
// 复制一个文件
fs.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.mkdir(path.resolve(__dirname, "./hello/abc"), { recursive: true })
.then(r => {
console.log("操作成功");
})
.catch(err => {
console.log("创建失败");
});
// 删除目录
fs.rmdir(path.resolve(__dirname, "./hello"), { recursive: true })
.then(r => {
console.log("删除成功");
})
// 重命名
fs.rename(path.resolve(__dirname, "../iu.jpg"), path.resolve(__dirname, "./iu.jpg"))
.then(r => console.log("成功"));
包管理器
/*
package.json
- package.json是包的描述文件
- node中通过该文件对项目进行描述
- 每一个node项目必须有package.json
name(必备) 包的名称,可以包含小写字母、_ 和 -
version (必备) 版本信息
description 对包的介绍
main 入口文件 一般为index.js
author 作者
license 版权协议
scripts:
- 可以自定义一些命令
- 定义以后可以直接通过npm来执行这些命令
- start 和 test 可以直接通过 npm start / npm test执行
- 其他命令需要通过npm run xxx 执行
命令
npm init 初始化项目,创建package.json文件(需要回到问题)
yarn init
npm init -y 初始化项目,创建package.json文件(所有值都采用默认值)
yarn init -y
npm install 包名 将指定包下载到当前项目中
yarn add 包名
yarn add 包名 -D 开发依赖
install时发生了什么?
1.将包下载到当前项目的node_modules目录下
2.会在package.json的dependencies属性中添加一个新属性
"lodash": "^4.17.21" 匹配4的最新版本
"lodash": "~4.17.21" 匹配4.17的最新版本
3.会自动添加package-lock.json文件
帮助加速npm下载的,不用动它
npm install 自动下载
npm install 包名 -g 全局安装
yarn global add 包名
- 全局安装是将安装到计算机中
- 全局安装的通常都是一些工具
npm uninstall 包名 卸载
yarn remove 包名
npm uninstall 包名 -g 全局卸载
yarn global remove 包名 全局
npm镜像
- npm的仓库的服务器位于国外,有时候并不是那么的好使
- 为了解决这个问题,可以在npm中配置一个镜像服务器
- 镜像的配置:
1.在系统中安装cnpm
2.彻底修改npm仓库地址
*/
/*
引入从npm下载的包时,不需要书写路径,直接写包名即可
*/
const _ = require("lodash");
console.log(_);
http协议
/*
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.可以向浏览器返回响应报文
*/
express
安装
/*
express 是node中的服务器软件
通过express可以快速地在node中搭建一个web服务器
- 使用步骤:
1.创建并初始化项目
yarn init -y
2.安装express
yarn add express
3.创建index.js并编写代码
*/
// 引入express
const express = require("express");
// 获取服务器地实例(对象)
const app = express();
// 启动服务器
/*
app.listen(端口号) 用来启动服务器
服务器启动后,我们便可以通过3000端口来访问了
协议名://ip地址:端口号/路径
http://localhost:3000
http://127.0.0.1:3000
*/
app.listen(3000, () => {
console.log("服务器已经启动");
});
路由
/*
如果希望服务器可以正常访问,则需要为服务器设置路由
路由可以根据不同地请求方式和请求地址来处理用户地请求
app.METHOD(...)
METHOD可以是 get 或 post ...
*/
app.get("/hello", (req, res) => {
console.log("有人访问我了");
/*
在路由中,应该做两件事
读取用户地请求(request)
根据用户地请求返回响应(response)
路由的回调函数执行时,会收到三个参数
第一个request 第二个respond
req 表示的是用户的请求信息,通过req可以获取用户传递的数据
res 表示的是服务器发送给客户端的响应信息,可以通过res来向客户端返回数据
sendStatus() 向客户端发送响应状态码
status() 用来设置响应状态码,但是并不发送 默认200
send() 设置并发送响应体
*/
console.log(req.url);
res.status(200);
res.send("<h1>hello world</h1>");
});
中间件
/*
中间件
- 在express我们使用app.use来定义一个中间件
中间件作用和路由很像,用法很像
但是路由不区分请求的方式,只看路径
- 和路由的区别
1.会匹配所有的请求
2.路径设置父目录,省略目录就是更目录,相当于 "/"
- next() 是回调函数的第三个参数,它是一个函数,调用函数后,可以触发后续的中间件
next() 不能在响应处理完毕后调用
*/
app.use("/nihao", (req, res, next) => {
console.log("收到请求1");
// res.send("这是通过中间件1返回的响应");
next(); // 放行
});
app.use("/nihao", (req, res) => {
console.log("收到请求2");
res.send("这是通过中间件2返回的响应");
});
// 可以在所有路由的后边配置错误路由
app.use((req, res) => {
// 只要这个中间件一执行,说明上面的地址都没有匹配
res.status(404);
res.send("没这玩意");
});
静态资源
/*
给用户返回一个网页
服务器中的代码,对于外部来说都是不可看见的,
所以我们写的html页面,浏览器无法直接访问
如果希望浏览器可以访问,则需要将页面所在的目录设置为静态资源目录
设置app.use(express.static("public")) 中间件后,会自动去public目录寻找是否有匹配的静态资源
*/
app.use(express.static(path.resolve(__dirname, "./public")));
// http://localhost:3000/ 就找public目录下的index.html文件,所以下面这个根路由不会执行
app.get("/", (req, res) => {
res.send("hello");
});
查询字符串
// http://localhost:3000/login?username=sunwukong&password=123123
app.get("/login", (req, res) => {
console.log("请求收到");
/*
获取到用户输入的用户名和密码
req.query 表示字符串中的请求参数
*/
if (req.query.username === "sunwukong" && req.query.password === "123123") {
res.send("登录成功");
} else {
res.send("用户名或密码错误");
}
});
nodemon
/*
nodemon
1.全局安装
npm i nodemon -g
yarn global add nodemon
- 通过yarn进行全局安装时,默认yarn的目录并不在环境变量中
- 需要手动将路径添加到环境变量中
- 启动:
nodemon // 运行index.js
nodemon xxx.js // 运行指定的js
2.在项目中安装
npm i nodemon -D
yarn add nodemon -D 开发依赖
- 启动:
npx nodemon // 运行index.js
npx nodemon xxx.js // 运行指定的js
*/
param
/*
get 请求发送参数的第二种方式
/hello/:id 表示当用户访问/hello/xxx 时就会触发
在路径中以冒号命名的部分我们称为param,在get请求中可以被解析为参数
可以通过req.params书写来获取这些参数
param传参一般不会传递特别复杂的参数
*/
// http://localhost:3000/hello/1
app.get("/hello/:id", (req, res) => {
console.log(req.params); // {id: '1'}
res.send("hello");
});
post请求
// 引入解析请求体的中间件
// app.use(express.urlencoded());
app.use(express.urlencoded({ extended: true })); // true和false都行
// post请求
app.post("/login", (req, res) => {
/*
通过req.body来获取post请求的参数(请求体中的参数)
默认情况下,express不会自动解析请求体,需要通过中间件为其增加功能
*/
const username = req.body.username;
const password = req.body.password;
if (username === "admin" && password === "123123") {
res.send("登录成功");
} else {
res.send("登录失败");
}
});
ejs
/*
ejs是node中的一款模板引擎,使用步骤
1.安装ejs
yarn add ejs
2.配置express的模板引擎为ejs
app.set("view engine","ejs")
3.配置模板路径
app.set("views", path.resolve(__dirname, "views"))
注意,模板引擎需要被express渲染后才能使用
*/
// 将ejs设置为默认的模板引擎
app.set("view engine", "ejs");
// 配置模板的路径
app.set("views", path.resolve(__dirname, "views"));
app.get("/students", (req, res) => {
/*
用来渲染一个模板引擎,并将其返回给浏览器
可以将一个对象作为render的第二个参数传递,这样在模板中可以访问到对象中的数据
<%= %>在ejs中输出内容时,它会自动对字符串中的特殊符号进行转义 <
<%- %>不会转义,直接将内容输出
<% %> 可以在其中直接编写js代码,js代码会在服务器中执行
<%# %> 注释
这个设计主要为了避免 xss 攻击
*/
res.render("students", { name: "孙悟空", age: 18 });
});
redirect
app.post("/add-student", (req, res) => {
...
// 将新的数据写入到json文件中
fs.writeFile(path.resolve(__dirname, "./data/student.json"), JSON.stringify(STUDENT_ARR))
.then(() => res.redirect("/students"))
.catch(() => console.log("存入失败"));
/*
res.redirect() 用来发起请求重定向
重定向的作用是告诉浏览器你向另外一个地址再发起一次请求
*/
});
Router
// user.js
const express = require("express");
/*
Router是express中创建的一个对象
router实际上是一个中间件,可以在该中间件上去绑定各种路由以及其他的中间件
*/
// 创建一个router对象
const router = express.Router();
router.get("/list", (req, res) => {
res.send("hello 我是list");
});
// 将router暴露到模块外
module.exports = router;
// index.js
const express = require("express");
const app = express();
const path = require("path");
// 引入路由
const userRouter = require("./routes/user");
app.use(express.static(path.resolve(__dirname, "./public")));
app.use(express.urlencoded({ extended: true }));
// 使路由生效
// app.use("/user", require("./routes/student"));
app.use("/user", userRouter);
app.use((req, res) => {
res.status(404).send("没这玩意");
});
app.listen(3000, () => {
console.log("服务器启动");
});
cookie
/*
需要安装中间件来使得express可以解析cookie
1.安装cookie-parser
yarn add cookie-parser
2.引入
const cookieParser = require("cookie-parser")
3.设置为中间件
app.use(cookieParser())
*/
/*
HTTP协议是一个无状态的协议,服务器无法区分请求是否发送自同一客户端
cookie
- cookie是HTTP协议中用来解决无状态问题的技术
- cookie的本质就是一个头
- 服务器以响应头的形式将cookie发送给客户端
客户端收到以后会将其存储,并在下次向服务器发送请求时将其传回,浏览器自动发
这样服务器就可以根据cookie来识别出客户端了
*/
const express = require("express");
const app = express();
const path = require("path");
const cookieParser = require("cookie-parser");
app.use(express.static(path.resolve(__dirname, "./public")));
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser());
app.get("/get-cookie", (req, res) => {
// 给客户端发送一个cookie
res.cookie("username", "admin");
res.send("cookie已经发送");
});
app.get("/hello", (req, res) => {
// req.cookies 用来读取客户端发回的cookie
console.log(req.cookies); // {username : "admin"}
res.send("hello路由");
});
app.post("/login", (req, res) => {
// 获取用户的用户名和密码
const { username, password } = req.body;
if (username === "admin" && password === "123123") {
// 登录成功
// 将用户名放入cookie
res.cookie("username", username)
res.redirect("/students/list");
} else {
res.send("用户名或密码错误");
}
});
app.get("/set", (req, res) => {
/*
cookie是有有效期的
- 默认情况下cookie的有效期就是一次会话(session)
会话就是一次从打开到关闭浏览器的过程
- maxAge 用来设置cookie有效时间,但闻是毫秒
*/
res.cookie("name", "sunwukong", {
// expires:new Date(2023,11,7)
maxAge: 1000 * 60 * 60 * 24 * 30
});
res.send("设置cookie");
});
app.get("/delete-cookie", (req, res) => {
/*
cookie一旦发送给浏览器我们就不能再修改了
但是我们可以通过发送新的同名cookie来替换旧cookie,从而达到修改的目的
*/
res.cookie("name", "", { maxAge: 0 });
res.send("删除cookie");
});
session
/*
cookie的不足
- cookie是由服务器创建,浏览器保存
每次浏览器访问服务器时都需要将cookie发回
这就导致我们不能再在cookie存放较多的数据
并且cookie是直接存储在客户端,容易被篡改盗用
- 注意:
我们在使用cookie时一定不会在cookie存储敏感数据
- 所以为了cookie的不足,我们希望可以这样
将用户的数据统一存储在服务器中,
每一个用户的数据都有一个对应的id
我们只需通过cookie将id发送给浏览器
浏览器只需每次访问时将id发回,即可读取到服务器中存储的数据
这个技术我们称之为session(会话)
session
- session是服务器中的一个对象,这个对象用来存储用户的数据
- 每一个session对象都有一个唯一的id,id会通过cookie的形式发送给客户端
- 客户端每次访问时自需将存储有id的cookie发回即可获取它在服务器中存储的数据
- 在express 可以通过 express-session,组件来实现session功能
- 使用步骤:
1.安装
yarn add express-session
2.引入
const session = require("...")
3.设置为中间件
app.use(session({...}))
*/
const express = require("express");
const app = express();
const path = require("path");
// 引入session
const session = require("express-session");
app.use(express.static(path.resolve(__dirname, "./public")));
app.use(express.urlencoded({ extended: true }));
// 设置session中间件
app.use(session({
// 加密
secret: "hello"
}));
app.get("/set", (req, res) => {
// 通过req设置session
req.session.username = "sunwukong";
res.send("查看session");
});
app.get("/get", (req, res) => {
const username = req.session.username;
console.log(username);
res.send("读取session");
});
/*
session是服务器中的一个对象,这个对象用来存储用户的信息
每一个session都会有一个唯一的id,session创建后,id会以cookie的形式发送给浏览器
浏览器收到以后,每次访问都会将id发回,服务器中就可以根据id找到对应的session
id(cookie) --> session对象
session什么时候会失效?
1.浏览器的cookie没了
2.服务器中的session对象没了
express-session默认是将session存储到内存中的,所以服务器一旦重启session会自动重置,
所以我们使用session通常会对session进行一个持久化的操作(写到文件或数据库)
如何将session存储到文件中:
- 需要引入一个中间件
- 使用步骤:
1.安装
yarn add session-file-store
2.引入
const FileStore = require("session-file-store")(session)
3.设置为中间件
app.use(session({
store:new FileStore({}),
secret: "hello"
}));
*/
const express = require("express");
const app = express();
const path = require("path");
const session = require("express-session");
// 引入file-store
const FileStore = require("session-file-store")(session);
app.use(session({
store: new FileStore({
// path用来指定session本地文件的路径
path: path.resolve(__dirname, "./session"),
// 加密
secret: "haha",
// session的有效时间 秒 默认1个小时
// ttl: 10,
// 默认情况下,fileStore会每间隔一小时,清除一次session对象
// repaInterval 用来指定清除session的间隔,单位秒,默认 1小时
// reapInterval: 10
}),
secret: "hello"
}));
app.get("/logout", (req, res) => {
// 使session失效
req.session.destroy(() => {
res.redirect("/");
});
});
app.post("/login", (req, res) => {
const { username, password } = req.body;
if (username === "admin" && password === "123123") {
// 登录成功后,将用户信息放入session
req.session.loginUser = username;
res.redirect("/students/list");
} else {
res.send("用户名或密码错误");
}
});
csrf
/*
csrf攻击
- 跨站请求伪造
- 现在大部分的浏览器都不会在跨域的情况下自动发送cookie
这个设计就是为了避免csrf的攻击
- 如何解决?
1.使用referer头来检查请求的来源
2.使用验证码
3.尽量使用post请求(结合token)
- token(令牌)
- 可以在创建表单时随机生成一个令牌
然后将令牌存储到session中,并通过模板发送给用户
用户提交表单时,必须将token发回,才可以进行后续操作
(可以使用uuid来生成token)
*/