异步请求
-
我们之前编写的服务器都是传统的服务器,服务器的结构是基于MVC模式
- MVC模式:
- Model----数据模型 (data)
- View----视图,用来呈现 (views)
- Controller----控制器,加载数据并选择视图来呈现数据 (router)
- 传统的服务器是直接为客户端返回一个页面,但是传统的服务器并不能适用于现在的应用场景
- MVC模式:
-
现在的应用场景,一个应用通常都会有多个客户端(client)存在
web端 移动端(app) pc端 -
如果服务器直接返回html页面,那么服务器就只能为web端提供服务,其他类型的客户端还需要单独开发服务器,提高了开发和维护的成本
-
传统的服务器需要做两件事情:
- 加载数据
- 要将模型渲染进视图
-
如何解决这个问题?
-
将渲染视图的功能从服务器中剥离出来,服务器只负责向客户端返回数据,渲染视图的工作由客户端自行完成
-
分离以后,服务器只提供数据,一个服务器可以同时为多种客户端提供服务,同时将视图渲染的工作交给客户端以后,简化了服务器代码的编写
-
-
-
实现异步请求的方案:
- ajx:XMLHTTPRequest(xhr)
- Fetch
- Axios
Rest
-
REpresentational State Transfer,是一种针对网络应用程序设计的架构风格,它采用统一的接口原则进行资源的访问和操作。
-
主要特点:服务器只返回数据。服务器和客户端传输数据时通常会使用JSON作为数据格式
-
请求的方法 GET 加载数据(查询统一用get) POST 新建或添加数据 PUT 添加或修改数据 PATCH 修改数据 DELETE 删除数据 OPTION 由浏览器自动发送,检查请求的一些权限 -
RESTful API(接口)定义了一组用于与Web服务进行通信的规则和约定。 (Endpoint端点)
GET /user
POST /user
DELETE /user/:id ...
-
-
统一的api:路由路径统一、发送数据格式统一
CORS:跨域资源共享
-
跨域检查:1. 协议 2. 域名 3. 端口号 三个只要有一个不同,就算跨域
-
当通过AJAX去发送跨域请求时,浏览器为了服务器的安全,会阻止JS读取到服务器的数据
-
解决方案:在服务器中设置一个允许跨域的头,
Access-Control-Allow-Origin,允许那些客户端访问我们的服务器-
Access-Control-Allow-Origin 设置指定值时只能设置一个
res.setHeader("Access-Control-Allow-Origin", "http://127.0.0.1:5500")
-
服务器端:
// 解析json格式请求体的中间件
app.use(express.json())
app.use((req, res, next) => {
// 设置响应头
res.setHeader("Access-Control-Allow-Origin", "*")
// Access-Control-Allow-Methods 允许的请求的方式
res.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,PATCH")
// Access-Control-Allow-Headers 允许传递的请求头
res.setHeader("Access-Control-Allow-Headers", "Content-type")
next()
})
Ajax
-
Ajax(Asynchronous JavaScript and XML):在js中向服务器发送请求和加载数据的技术叫AJAX。通过 AJAX 可以在浏览器中向服务器发送异步请求,最大的优势:无刷新获取数据。
-
XML可扩展标记语言:早期AJAX使用的数据格式,目前数据格式都使用json
-
使用方法:
btn.onclick = () => {
// 创建一个xhr对象
const xhr = new XMLHttpRequest()
// 设置响应体的类型,设置后会自动对数据进行类型转换
xhr.responseType = "json"
// 可以为xhr对象绑定一个load事件,当xhr加载完毕后才执行函数(异步)
xhr.onload = function () {
// xhr.status 表示响应状态码 / 服务器中设置的status是指数据传递的状态
if (xhr.status === 200) {
//手动转换响应的类型 等价于 xhr.responseType = "json"
// const result = JSON.parse(xhr.response)
// console.log(result.status, result.data)
// xhr.response 表示响应信息 前面设置响应为json,此时会自动转换成对象类型
const result = xhr.response
if (result.status === "ok") // 判断数据是否正确
{
const ul = document.createElement("ul")
root.appendChild(ul)
for (let stu of result.data) {
ul.insertAdjacentHTML(
"beforeend",
`<li>${stu.id} - ${stu.name} - ${stu.age} - ${stu.gender} - ${stu.address}</li>`
)
}
}
}
}
// 设置请求的信息
xhr.open("get", "http://localhost:3000/students")
// 发送请求
xhr.send()
}
fetch
-
fetch是xhr的升级版,采用的是Promise API,作用和AJAX一样,是原生js就支持的一种异步请求的方式
-
用fetch获取到的数据是服务器端返回的数据
-
fetch的配置对象:method......
fetch("http://localhost:3000/students", { method: "post", //使用不同请求时,需载服务器端允许接收不同请求 headers:{ "Content-type":"application/json" }, // 通过post的请求体body去发送数据时,必须通过请求头来指定数据的类型 body: JSON.stringify({ //content }) }) -
fetch的请求中止:使用
AbortControllerlet controller btn01.onclick = () => { // 1. 创建一个AbortController controller = new AbortController() //2. 设置fetch的配置对象 fetch("http://localhost:3000/test", { signal: controller.signal }) .then((res) => console.log(res)) .catch((err) => console.log("出错了", err)) } btn02.onclick = () => { //3. 调用请求中止 controller && controller.abort() } -
fetch的await使用:将promise改写为await时,一定要写try-catch
btn03.onclick = async () => { try { const res = await fetch("http://localhost:3000/students") const data = await res.json() } catch (e) { console.log("出错了", e) } }fetch("http://localhost:3000/students") .then((res) => { //这一步获取到服务器端响应的对象 if(res.status === 200){ return res.json() //res.json() 可以用来读取json格式的数据,此时res.json()是一个promise }else { throw new Error("加载失败!") } }) .then(res => { //这一步获取到服务器端传回的数据,此时直接是对象形式 if(res.status === "ok"){ // 对数据进行操作 } }) .catch((err) => { console.log("出错了!", err) })
本地存储
- 问题:在创建登录页面后,登陆成功后只要一刷新就会跳转到重新登录页面,需要解决这个问题,需要将用户信息存储到本地中。cookie和session不能用因为跨域实现。
-
本地存储就是指浏览器自身的存储空间,可以将用户的数据存储到浏览器内部
-
类型:
sessionStorage中存储的数据,页面一关闭就会丢失localStorage存储的时间比较长,只有被显式地删除或者浏览器清除缓存时才会丢失
-
方法:localStorage.setItem("name", "孙悟空")
setItem() 存储数据 getItem() 获取数据 removeItem() 删除数据 clear() 清空数据
token
- 问题:登录以后直接将用户信息存储到了localStorage。 1. 数据安全问题 2. 服务器没有验证登录
-
使用token解决:
- 对数据加密(token)
- 告诉服务器客户端的登录状态
- rest风格的服务器是无状态的服务器,服务器中不能存储用户信息,但可以将用户信息发送给客户端保存
- 客户端每次访问服务器时,直接将用户信息发回,服务器就可以根据用户信息来识别用户的身份(解密成功则有权限)
-
jsonwebtoken:jwt --> 通过对json加密后,生成一个web中使用的令牌
jwt.sign():加密jwt.verify():解密
// 安装后引入jwt const jwt = require("jsonwebtoken") // 创建一个对象 const obj = {name: "max"} // 使用jwt来对json数据进行加密 const token = jwt.sign(obj, "hihihihihi", { expiresIn: "1" //有效时长 }) try { //服务器收到客户端的token后 const decodeData = jwt.verify(token, "hihihihihi") } catch (e) { // 说明token解码失败,说明token console.log("无效的token") }
axios
- 可以理解为对xhr的封装。和fetch一样,都可以在浏览器和nodejs中运行
- 网页端直接在html中引入:
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
配置对象
-
baseURL指定服务器的根目录(路径的前缀) -
url请求地址 -
method请求方法,默认是get -
data请求体{a : "xxx"}/"a=xxx&b=xxx" -
params用来指定路径中的查询字符串 -
timeout过期时间 -
transformRequest可以用来处理请求数据(data)- 它需要一个数组作为参数,数组可以接收多个函数,请求发送时多个函数会按照顺序执行,函数在执行时,会接收到两个参数data和headers
// axios(config) axios({ method: "post", url: "http://localhost:3000/students", data:{} params:{ id:1, name:"swk" }, transformRequest:[function(data, headers){ // 可以在函数中对data和headers进行修改 data.name = "xxx" headers["Content-Type"] = "application/json" return data }, function(data, headers){ return JSON.stringify(data) // 最后一个函数必须返回一个字符串,才能使得数据有效 }] // 指定请求头 axios会根据内容自动选择类型 // headers:{"Content-type":"application/json"} }) .then((result) => { //axios默认只会在响应状态为2xx时才会调用then // result是axios封装过的对象 }) .catch((err) => { console.log("出错了!", err) }) }
默认配置
设置默认配置后,后面所有请求的该配置都遵循默认配置
axios.defaults.baseURL = "http://localhost:3000"
axios.defaults.headers.common["Authorization"] = `Bearer ${localStorage.getItem("token")}`
axios实例
-
axios实例相当于是axios的一个副本,它的功能和axios一样
-
axios的默认配置在实例也同样会生效,但可以单独对实例的默认配置进行修改
-
创建实例:
const instance = axios.creat()axios.defaults.baseURL = "http://localhost:3000" axios.defaults.headers.common["Authorization"] = `Bearer ${localStorage.getItem("token")}` const instance = axios.create({ baseURL:"http://localhost:4000" }) //创建实例监听另一个端口 const instance = axios.create() instance.defaults.baseURL = "xxx"
拦截器
-
axios的拦截器有请求拦截器和响应拦截器,在请求发送前和响应读取前处理数据
-
拦截器只对当前的实例有效(给哪个开启拦截器,就对哪个生效)
axios.interceptors.request.use( function (config) { // config 表示axios中的配置对象 // 可以修改对象 config.headers["Authorization"] = `Bearer ${localStorage.getItem("token")}` // 在发送请求之前做些什么 return config }, function (error) { // 对请求错误做些什么 return Promise.reject(error) }) //添加响应拦截器 axios.interceptors.response.use( function (response) { // 2xx 范围内的状态码都会触发该函数。 // 对响应数据做点什么 return response; }, function (error) { // 超出 2xx 范围的状态码都会触发该函数。 // 对响应错误做点什么 return Promise.reject(error); });
fetch例子
登录界面,登陆成功后可访问数据
客户端
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" /> ......
<style>
table {
border-collapse: collapse;
width: 50%;
}
td,
th {
font-size: 20px;
text-align: center;
border: 1px solid #000;
}
caption {
font-size: 30px;
font-weight: bold;
}
</style>
</head>
<body>
<div id="root">
<h1>请登录以后再做操作</h1>
<h2 id="info"></h2>
<form>
<div><input id="username" type="text" /></div>
<div><input id="password" type="password" /></div>
<div><button id="login-btn" type="button">登录</button></div>
</form>
</div>
<script>
// 点击login-btn后实现登录功能
const loginBtn = document.getElementById("login-btn")
const root = document.getElementById("root")
function loadData()
{
// 当我们访问的是需要权限的api时,必须在请求中附加权限的信息
// token一般都是通过请求头来发送
const token = localStorage.getItem("token")
fetch("http://localhost:3000/students", {
headers:{
// "Bearer xxxxxx"
"Authorization":`Bearer ${token}` //服务器端的setheader中需要允许
}
})
.then((res) => {
if (res.status === 200) {
return res.json() // res.json() 可以用来读取json格式的数据
} else { throw new Error("加载失败!") }
})
.then((res) => {
// 获取到数据后,将数据渲染到页面中
if (res.status === "ok") {
// 创建一个table
const dataDiv = document.getElementById("data")
const table = document.createElement("table")
dataDiv.appendChild(table)
table.insertAdjacentHTML("beforeend","<caption>学生列表</caption>")
table.insertAdjacentHTML(
"beforeend",
`
<thead>
<tr>
<th>学号</th>
<th>姓名</th>
<th>年龄</th>
<th>性别</th>
<th>地址</th>
</tr>
</thead>
`)
const tbody = document.createElement("tbody")
table.appendChild(tbody)
// 遍历数据
for (let stu of res.data) {
tbody.insertAdjacentHTML(
"beforeend",
`<tr>
<td>${stu.id}</td>
<td>${stu.name}</td>
<td>${stu.age}</td>
<td>${stu.gender}</td>
<td>${stu.address}</td>
</tr>`
)}
}
})
.catch((err) => {console.log("出错了!", err) }
)}
// 判断用户是否登录
if (localStorage.getItem("token")) {
// 用户已经登录
root.innerHTML = `
<h1>欢迎 ${localStorage.getItem("nickname")} 回来!</h1>
<hr>
<button id="load-btn" onclick="loadData()">加载数据</button>
<button onclick="localStorage.clear()">注销</button>
<hr>
<div id="data"></div>`
} else {
loginBtn.onclick = () => {
// 获取用户输入的用户名和密码
const username = document.getElementById("username").value.trim()
const password = document.getElementById("password").value.trim()
// 调用fetch发送请求来完成登录
fetch("http://localhost:3000/login", {
method: "POST",
headers: {
"Content-type": "application/json"
},
body: JSON.stringify({ username, password })
})
.then((res) => res.json())
.then((res) => {
if (res.status !== "ok") { throw new Error("用户名或密码错误") }
// 登录成功以后,需要保持用户的登录的状态,需要将用户信息存储到本地存储
// 登录成功,向本地存储中插入用户的信息
localStorage.setItem("token", res.data.token)
localStorage.setItem("nickname", res.data.nickname)
// 登录成功
root.innerHTML = `
<h1>欢迎 ${res.data.nickname} 回来!</h1>
<hr>
<button id="load-btn" onclick="loadData()">加载数据</button>
<button onclick="localStorage.clear()">注销</button>
<hr>
<div id="data"></div>`
})
.catch((err) => { // 这里登录失败
document.getElementById("info").innerText ="用户名或密码错误"
})
}}
</script>
</body>
</html>
服务器端
const express = require("express")
const jwt = require("jsonwebtoken") // 引入jwt
const app = express()
const STU_ARR = [
{ id: "1", name: "孙悟空", age: 18, gender: "男", address: "花果山" },
{ id: "2", name: "猪八戒", age: 28, gender: "男", address: "高老庄" },
{ id: "3", name: "沙和尚", age: 38, gender: "男", address: "流沙河" }
]
app.use(express.urlencoded({ extended: true }))
// 解析json格式请求体的中间件
app.use(express.json())
app.use((req, res, next) => {
// 设置响应头
res.setHeader("Access-Control-Allow-Origin", "*")
res.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,PATCH")
res.setHeader("Access-Control-Allow-Headers", "Content-type,Authorization")
next()
})
// 定义一个登录的路由
app.post("/login", (req, res) =>
{
const { username, password } = req.body
if (username === "admin" && password === "123123") {
// 登录成功,生成token
const token = jwt.sign(
{
id: "12345",
username: "admin",
nickname: "超级管理员"
},
"chaojianquanmima",
{ expiresIn: "1d" }
)
res.send({
status: "ok",
data: {
token,
nickname: "超级管理员"
}
})
} else { // 登录失败
res.status(403).send({
status: "error",
data: "用户名或密码错误"
})
}
})
// 定义学生信息的路由
app.get("/students", (req, res) => {
try {
// 这个路由必须在用户登录后才能访问
// req.get:读取请求头
const token = req.get("Authorization").split(" ")[1]
// 对token进行解码
const decodeToken = jwt.verify(token, "chaojianquanmima")
// 解码成功,token有效 返回学生信息
res.send({
status: "ok",
data: STU_ARR
})
} catch (e) { // 解码错误,用户token无效
res.status(403).send({
status: "error",
data: "token无效"
})
}
})
app.listen(3000, () => {
console.log("服务器已经启动!")
})