Node.js
回顾
数据库 (database) 是用来组织、存储和管理数据的仓库
需要安装PHPStudy里面内置了mysql环境软件一起安装到电脑中
MYSQL: 数据库管理软件
Navicat: 是图形化界面, 可以操作MySQL数据库的
node+mysql模块: 可以用代码来操作MYSQL数据库
SQL语句, 本质上都用这个来操作数据库
新增:
INSERT INTO 表名 SET ?
查询:
SELECT 字段 FROM 表名 WHERE 条件 ORDER BY 排序字段 LIMIT 开始下标, 个数
- 带 and or 等查询 > < 等条件
- 模糊查询 like % 占0到多位, _ 占1位
- 统计
- count() - 数量
- sum() 求和
- max() - 求最大
- avg() - 求平均值
更新
UPDATE 表名 SET ?
项目思路:
前端JS+axios - nodejs+mySQL+express, 数据库保存数据
22. 前后端身份验证
22.0 项目开发模式
-
服务器端渲染
服务器端渲染完数据形成的html页面, 直接把html页面响应给客户端浏览器直接显示
前端无需Ajax再请求数据了
-
优点
-
前端耗时少
-
更容易被爬虫(SEO)
-
-
缺点
- 占用服务器端资源
- 前端复杂度高的开发效率过低
-
-
前后端分离项目
服务器端只负责提供API数据接口, 前端用Ajax调用使用即可
- 优点
- 开发体验好, 前端专注UI效果开发
- 后端只专注数据业务逻辑开发.减轻服务器端压力
- 缺点
- 不利于seo, 因为是动态渲染的页面, 不利于爬虫
- 优点
具体使用哪种, 要根据实际业务场景来定的, 而且同一个项目中, 不同网页可以使用以上的2种方式
- 首页是静态宣传网页, 无任何交互, 可以使用后端渲染的方式
- 其他页面用户交互比较多, 可以选择前后端分离方式的开发模式
22.1 身份认证
身份认证, 又称"身份验证", “鉴权”, 是指通过一定的手段, 完成对用户身份的确认
目的: 确认当前所声称为某种身份的用户, 确实是所声称的用户, 例如你去取快递, 你需要先声明你手机号后4位
不同开发模式下的身份认证
服务器端渲染的项目: 推荐使用session认证机制
前后端分离的项目: 推荐使用JWT认证机制
HTTP 无状态性
服务器接收到每次请求时, 和响应后, 服务器是不会保留过上一次HTTP请求的任何信息和状态的(在协议中)
突破HTTP无状态性
每次请求过来以后, 服务器都对浏览器设置一个cookie保存一个值, 这样下次这个浏览器在发送请求过来, 会携带cookie过来, 服务器就知道你是谁了
22.2 Cookie
Cookie跟localStorage和sessionStorage都是在浏览器上存储的一种方式由名称值格式存储
特点:
- 自动发送未过期的cookie给后台
- 不同域名下cookie是各自独立
- 可以设置过期时限
- 4KB大小限制
- 跨域后端无法设置前端的cookie, 所以一般我们都用jwt(token)
查看Cookie
检查 -> Application -> Cookie
22.3 Cookie认证机制
客户端第一次请求-> 服务器, 服务器在响应头给客户端设置cookie保存在浏览器中
以后客户端每次请求服务器都会把未过期的cookie携带给后台, 表明自己的身份 (在请求头里自动携带)
去运行下提前准备好的代码, 无需编写, 明白流程, 因为一会儿我们有插件使用
前台登录.html
$("#btn").on("click", () => {
var username = $("#userInp").val();
var password = $("#passInp").val();
axios({
url: "http://localhost:3000/api/login",
params: {
username,
password
}
}).then(res => {
if (res.data.status == 1) {
window.location.href = "/22.3_首页.html";
}
})
})
22.3_cookie验证.js
const express = require("express");
const app = express();
app.listen(3000, () => {
console.log("服务器启动了");
})
app.use(express.static(__dirname + "/../1_前端/"));
let arr = [
{
username: "nana",
password: 111111,
money: 10000
},
{
username: "xiaomi",
password: 222222,
money: 500
}
]
// 重要: 跨域后端无法设置前端cookie - 直接在浏览器打开后端的静态资源
// 服务器端, 利用响应头给前端本次请求的位置设置cookie的方式
app.get("/api/login", (req, res) => {
// 假装登录成功了, 设置一个cookie给前端, 比如用户名字
// set-cookie是固定的, 后面的格式固定名称=值 - 去浏览器请求这个接口-观察前端浏览器上被设置了cookie
let { username, password } = req.query;
// 查找用户和密码是否有同时匹配的
var have = arr.some(obj => obj.username == username && obj.password == password);
if (have) {
res.setHeader("set-cookie", `user=${username}; path=/`); // path代表此cookie在前端生效的区域(发送请求时url只要是在/下, 就都能使用cookie)
res.send({
status: 1,
msg: "登录成功"
})
} else {
res.send({
status: 0,
msg: "账号或密码错误"
})
}
})
app.get("/api/getMoney", (req, res) => {
// 提取cookie, 找到user保存的用户名
var argStr = [];
if (req.headers.cookie) {
argStr = req.headers.cookie.split("; ");
}
var username = ""; // 保存找到的用户名
argStr.forEach(str => {
let smallArr = str.split("=");
if (smallArr[0] == "user") {
username = smallArr[1];
}
})
// 再用用户名, 查询这个用户的金额
var money = 0;
arr.forEach(obj => {
if (obj.username == username) {
money = obj.money;
}
})
res.send({
status: 1,
msg: "获取成功",
data: money
})
})
前台_首页.html
用户查询此用户余额
axios({
url: "http://localhost:3000/api/getMoney"
}).then(res => {
$("#myDiv").html(res.data.data);
})
cookie不具有安全性
后端只判断有cookie, 而且有那个名字user(类似于会员卡), 就直接返回了数据
那前端浏览器伪造(直接在浏览器cookie写个名字user, 值随便写), 就可以随意拿到数据了, 很危险
后端需要验证值是否也合法
22.4 Session认证机制
底层需要cookie配合, cookie端保存一个代号, 代号对应此用户在后台保存的身份信息名字
express-session
前台不变
这是一个第三方模块, 用于快速让express服务器集成session功能, 而封装的模块, npm下载后可以直接使用
npm install express-session
const express = require("express");
const app = express();
app.listen(3000, () => {
console.log("服务器启动了");
})
app.use(express.static(__dirname + "/../1_前端/"));
let arr = [
{
username: "nana",
password: 111111,
money: 10000
},
{
username: "xiaomi",
password: 222222,
money: 500
}
]
const session = require("express-session");
app.use(session({
secret: 'keyboard cat', // 用于加密目标数据用的字符串
resave: false, // resave是指每次请求都重新设置session cookie
saveUninitialized: true // saveUninitialized是指无论有没有session cookie,每次请求都设置个session cookie ,默认给个标示为 connect.sid
}))
app.get("/api/login", (req, res) => {
let {username, password} = req.query;
// 查找用户和密码是否有同时匹配的
var have = arr.some(obj => obj.username == username && obj.password == password);
if (have) {
req.session.usern = username;
res.send({
status: 1,
msg: "登录成功"
})
} else {
res.send({
status: 0,
msg: "账号或密码错误"
})
}
})
// 重要 - 多个前端登录验证, 需要用不同的浏览器, 因为一个浏览器的同一个域名下的cookie会被覆盖
app.get("/api/getMoney", (req, res) => {
// 提取cookie的值, 从express-session内存中提取对应的值
var username = req.session.usern;
// 查找余额
var money = 0;
arr.forEach(obj => {
if (obj.username == username) {
money = obj.money;
}
})
res.send({
status: 1,
msg: "获取成功",
data: money
})
})
注意每个浏览器使用连接的cookie相同, 如果想要测试不同登录人的cookie需要不同浏览器
22.5 删除Session
注册, 只会删除这个用户的
app.put("/logout", (req, res) => {
req.session.destroy();
res.send({
status: 1,
msg: "退出成功"
})
})
23. JWT认证
jwt全称叫 JSON Web Token - 是一种代替session的验权技术
Session如果在跨域的项目中, 后端需要开启Https + SameSite 才能给跨域的前端设置cookie使用 (否则浏览器无法被跨域的服务器设置cookie)
而且Session只保存在当前的服务器上, 如果是分布式服务器集群, Session还需要同步给各个服务器, 太麻烦
JWT就是颁发给前端一串字符串保存在客户端(加密后), 后端解密JWT, 提取用户身份信息来认真
jwt组成
23.0 jwt使用
前端登录以后, 后端生成签名token, 前端得到后保存到浏览器本地
以后请求接口都携带token, 后端解析token提取你的用户信息, 然后返回你的对应的信息
后端下载2个包
jsonwebtoken 用于生成jwt字符串
express-jwt 用于将jwt字符串解析还原成JS对象
npm i jsonwebtoken express-jwt
// 1. 引入模块
const jsonwebtoken = require('jsonwebtoken');
const expressJwt = require("express-jwt");
// 2. 设置秘钥
const secretKey = "itheima No.1 ^.^";
// 4. 解析 jwt
// 用哪个秘钥解密, jwt的算法, unless哪些接口不需要权限验证
app.use(expressJwt({secret: secretKey, algorithms: ["HS256"]}).unless({path: [/^\/api\//]}))
app.get("/api/login", (req, res) => {
let {username, password} = req.query;
var have = arr.some(obj => obj.username == username && obj.password == password);
if (have) {
// 3. 登录成功 - 生成token
res.send({
status: 1,
msg: "登录成功",
// 注意这个前缀是expressJwt规定的, 必须带这个固定的头加空格, 里面要拆分使用(跟真正jwt数据毫无关系)
token: "Bearer " + jsonwebtoken.sign({username: username}, secretKey, {expiresIn: "30s"})
})
// h是小时, s是秒
} else {
res.send({
status: 0,
msg: "账号或密码错误"
})
}
})
app.get("/my/getMoney", (req, res) => {
// 5. 使用req.user.username
var username = req.user.username;
var money = 0;
arr.forEach(obj => {
if (obj.username == username) {
money = obj.money;
}
})
res.send({
status: 1,
msg: "获取成功",
data: money
})
})
23.1 jwt过期 - 兜底错误验证
// 6. 兜底错误处理 - 自定义错误处理中间件
app.use((err, req, res, next) => {
// token 解析失败导致的错误
if (err.name == "UnauthorizedError") {
return res.send({status: 401, msg: "无效的token"});
}
res.send({status: 500, msg: err.message});
})
24. Promise
- 如何获取异步的结果?必须通过回调函数获取,不能通过返回值获取
- 如果要保证多个异步结果获取的顺序那么需要进行回调的嵌套,如果嵌套层次比较多,那么就会出现一个问题:回调地狱
- 为了解决回调地狱的问题就诞生了一种新的技术:Promise
Promise:Promise对象用于表示一个异步操作的最终完成 (或失败)及其结果值。它是异步任务的管家
JS异步有哪些
- 定时任务
- Ajax
- 事件处理函数
24.0 Promise基本用法
- 异步任务需要在Promise回调函数中进行处理 当Promise对象创建时, 马上执行回调函数里异步任务
- 正常的异步结果交给resolve函数
- 异常的错误信息交给reject函数
- 获取正常或异常的数据分别使用then或者catch
- resolve调用 -> then里回调函数执行
- reject调用 -> catch里回调函数执行
// 1. 新建Promise对象
let p = new Promise((resolve, reject) => {
// 2. 异步任务在这里
fs.readFile(__dirname + "/24/0.json", "UTF-8", (err, data) => {
if (err) reject(err)
else resolve(data);
})
})
// 3. 监测p任务管家的结果
p.then(res => {
console.log(res);
}).catch(err => {
console.log(err);
})
24.1 then里return值
then函数本身会返回一个Promise
这个Promise的值就是then回调函数里return出来的结果
如果return的又是一个Promise则会覆盖then函数默认返回的Promise
- 如果then回调函数的返回值返回Promise实例对象,那么下一个then会得到该异步任务的结果
- 如果then回调函数的返回值返回普通数据,那么下一个then会直接获取该数据
// 1. then()方法默认返回一个Promise, 而then里的return值作为这个Promise成功的结果触发这个Promise的then函数执行并传值
// let p = new Promise((res, rej) => {
// setTimeout(() => {
// res("异步返回结果");
// }, 2000);
// })
// var newP = p.then(res => {
// console.log(res);
// return 123;
// })
// newP.then(res => {
// console.log("第二个then执行");
// console.log(res);
// })
// 2. then()方法里return一个新的Promise使用
let p = new Promise((res, rej) => {
setTimeout(() => {
res("异步返回结果");
}, 2000);
})
let p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("5秒后返回结果");
}, 5000);
})
var newP = p.then(res => {
console.log("第一个then执行");
console.log(res);
return p2;
})
newP.then(res => {
console.log("第二个then执行");
console.log(res);
})
// 代码执行顺序, 创建p变量的Promise管家, 然后马上执行里面的异步任务(开始计时)
// 主线程继续往下, 创建p2变量的Promise管家, 然后也马上执行Promise里面的异步任务(开始计时)
// 然后给p绑定then函数, 返回一个newP, 也马上绑定then函数 - 主线程执行完毕
// 待时间到了, 触发p的Promise里的resolve调用p的then函数体执行,
// 然后return p2这个Promise对象-等待时间到达后, 触发p2的then函数执行 - 如果要是值直接就传给下一个处理函数
24.2 解决回调地狱问题
// 需求读取3个文件异步任务 - 要从0和1和2的顺序执行
const fs = require("fs");
// 之前回调地狱写法
// fs.readFile(__dirname + "/24/0.json", "UTF-8", (err, data) => {
// if (err) console.log(err)
// else {
// console.log(data);
// fs.readFile(__dirname + "/24/1.json", "UTF-8", (err, data) => {
// if (err) console.log(err)
// else {
// console.log(data);
// fs.readFile(__dirname + "/24/2.json", "UTF-8", (err, data) => {
// if (err) console.log(err)
// else {
// console.log(data);
// }
// })
// }
// })
// }
// })
// Promise处理回调地狱
let p0 = new Promise((resolve, reject) => {
fs.readFile(__dirname + "/24/0.json", "UTF-8", (err, data) => {
if (err) reject(err)
else resolve(data);
})
});
let p1 = new Promise((resolve, reject) => {
fs.readFile(__dirname + "/24/1.json", "UTF-8", (err, data) => {
if (err) reject(err)
else resolve(data);
})
});
let p2 = new Promise((resolve, reject) => {
fs.readFile(__dirname + "/24/2.json", "UTF-8", (err, data) => {
if (err) reject(err)
else resolve(data);
})
});
// 重点在这里
p0.then(res => {
console.log(res);
return p1;
}).then(res => {
console.log(res);
return p2;
}).then(res => {
console.log(res);
})
24.3 重构数据库操作
function theMySQL(sql, params) {
return new Promise((resolve, reject) => {
const mysql = require('mysql');
const conn = mysql.createConnection({
host: 'localhost',
user: 'root',
password: 'root',
database: '121_db'
});
conn.connect();
// 完成增删改查
conn.query(sql, params, (err, data) => {
if (err) reject(err);
else resolve(data);
});
conn.end();
})
}
// 查询
// theMySQL("SELECT * FROM student", []).then(res => {
// console.log(res);
// })
// 新增
// let userObj = {
// name: "小米",
// age: 20,
// sex: "男",
// tel: 13900203123
// }
// let sql = `INSERT INTO student SET ?`;
// theMySQL(sql, userObj).then(res => {
// console.log(res);
// })
24.4 Promise的对象方法
// 案例: 想要执行多个异步任务, 都成功了, 同时按照顺序返回, 活着呢有一个失败了直接返回失败的提示, 建议使用Promise.all()
// 需求读取3个文件异步任务 - 要从0和1和2的顺序执行
const fs = require("fs");
let p0 = new Promise((resolve, reject) => {
fs.readFile(__dirname + "/24/0.json", "UTF-8", (err, data) => {
if (err) reject(err)
else resolve(JSON.parse(data));
})
});
let p1 = new Promise((resolve, reject) => {
fs.readFile(__dirname + "/24/1.json", "UTF-8", (err, data) => {
if (err) reject(err)
else resolve(JSON.parse(data));
})
});
let p2 = new Promise((resolve, reject) => {
fs.readFile(__dirname + "/24/2.json", "UTF-8", (err, data) => {
if (err) reject(err)
else resolve(JSON.parse(data));
})
});
// 重点在这里
// Promise.all() 可以把多个Promise对象合并成一个大的
// Promise.all([p0, p1, p2]).then(res => {
// console.log(res);
// }).catch(err => {
// console.log(err.message);
// })
// 案例: 哪个Promise异步执行完了(无论成功或者失败) - 直接触发 - 不关心其他人
Promise.race([p0, p1, p2]).then(res => {
console.log(res);
}).catch(err => {
console.log(err.message);
})
25. async函数和await修饰符
ES2017(ES8) 新出的2个关键字, async+await以更简洁的方式写出基于Promise的异步行为,而无需刻意地链式调用promise
async函数: 使用async
关键字声明的函数。 其中允许使用await
关键字。
await关键字: await
操作符用于等待一个Promise
对象。它只能在异步函数 async function
中使用。无需刻意地链式调用promise
。
- 如果等待的不是 Promise 对象,则返回该值本身
- 如果等待的是Promise对象, 则等待Promise对象的resolve调用把值返回到原地
25.0 async+await干掉then函数
const fs = require("fs");
// Promise处理回调地狱
let p0 = new Promise((resolve, reject) => {
fs.readFile(__dirname + "/24/0.json", "UTF-8", (err, data) => {
if (err) reject(err)
else resolve(data);
})
});
let p1 = new Promise((resolve, reject) => {
fs.readFile(__dirname + "/24/1.json", "UTF-8", (err, data) => {
if (err) reject(err)
else resolve(data);
})
});
let p2 = new Promise((resolve, reject) => {
fs.readFile(__dirname + "/24/2.json", "UTF-8", (err, data) => {
if (err) reject(err)
else resolve(data);
})
});
// 重点在这里
console.log("开始执行");
async function getData(){ // 这是一个异步块的代码, 不会阻塞主线程执行
const res0 = await p0; // await会暂停代码, 在这里等待p0这个Promise的结果回来才会继续往下执行
const res1 = await p1;
const res2 = await p2;
console.log(res0);
console.log(res1);
console.log(res2);
}
getData();
console.log("主线程结束");
25.1 优化上面
const fs = require("fs");
// 代码有点重复, 封装一个异步函数, 专门用于读取文件的Promise管家
const readFileFn = (pathUrl) => {
return new Promise((resolve, reject) => {
// 这里的异步任务马上执行
fs.readFile(__dirname + pathUrl, "UTF-8", (err, data) => {
if (err) reject(err)
else resolve(data);
})
});
}
async function getData(){
const res0 = await readFileFn("/24/0.json");
const res1 = await readFileFn("/24/1.json");
const res2 = await readFileFn("/24/2.json");
console.log(res0);
console.log(res1);
console.log(res2);
}
getData();
Promise和await和async都是ES6/ES8标准制定的, Node和前端都是JS语法
25.2 try和catch的使用
// try, catch是 JS当中2个关键字
// 专门来捕获同步代码的错误
const fs = require("fs");
// 异步代码捕获错误: -> 回调函数里的形参 (JS源码会给你返回错误对象)
// fs.readFile(__dirname + "/hah.json", "UTF-8", (err, data) => {
// if (err) console.log("自定义错误提示消息: 文件可能不在");
// else console.log(data);
// })
// 同步代码(等待结果) - 捕获错误
// (1): 用try{} 把可能报错的代码 包起来 (如果有错误, 会直接进入到catch不会继续执行下面的代码)
try {
const data = fs.readFileSync(__dirname + "/haha.json", "UTF-8");
console.log(data);
} catch (err) { // (2): 等待上面{}里代码如果报错了, 就会进入到这个catch块, 传递给err值
if (err) console.log("自定义错误提示消息: 文件可能不在");
}
25.3 try和catch解决await错误
function readFileFn(filePath) {
const fs = require("fs");
return new Promise((resolve, reject) => {
fs.readFile(filePath, "UTF-8", (err, data) => {
if (err) reject(err)
else resolve(data);
})
})
}
// 同步代码的错误, 如果程序员不捕获处理, js就会直接抛出到控制台里去
// await 等待Promise的resolve的结果返回在原地
// await 等待的Promise报错了, 执行了reject - 同步流程报错- 阻止代码继续向下执行
async function myFn() {
try { // 如果同步代码有try+catch, js就会把错误对象抛到catch里去, 让程序处理这个异常
const res0 = await readFileFn(__dirname + "/24/1.json");
const res1 = await readFileFn(__dirname + "/24/1.json");
const res2 = await readFileFn(__dirname + "/24/2.json");
console.log(res0);
console.log(res1);
console.log(res2);
} catch (err) {
if (err) console.log("可能文件的路径不对");
}
}
myFn();
// await 错误 - 后面还会学await-to.js(功能) (优化掉try+catch以及大括号)
如有不足,请多指教,
未完待续,持续更新!
大家一起进步!