AJAX函数与调用
AJAX函数与调用
以下为学习过程中的极简提炼笔记,以供重温巩固学习
学习准备
准备工作
html+css+JavaScript 3剑客都懂一点,已看完前面3节,特别是第3节,熟悉AJAX原理
学习目的
作为前端懂得如何处理接口
同步代码与异步代码
同步代码:
我们应该注意的是,实际上浏览器是按照我们书写代码的顺序一行一行地执行程序的。浏览器会等待代码的解析和工作,在上一行完成后才会执行下一行。这样做是很有必要的,因为每一行新的代码都是建立在前面代码的基础之上的。 这也使得它成为一个同步程序。
异步代码:
异步编程技术使你的程序可以在执行一个可能长期运行的任务的同时继续对其他事件做出反应而不必等待任务完成。与此同时,你的程序也将在任务完成后显示结果。
同步代码:逐行执行,需原地等待结果后,才继续向下执行
异步代码:调用后耗时,不阻塞代码继续执行(不必原地等待),在将来完成后触发一个回调函数
同步代码与异步代码区分案例:
以下案例打印的数字结果顺序是?
const result = 0 +1
console.1og(result)
setTimeout(() => {
console.1og(2)
},2000)
document.queryselector( '.btn ' ).addEventListener( 'click',()=>{
console.log (3)
})
document.body.style.backgroundcolor = 'pink '
console.log(4)
- 结果是:142
- 3视乎按钮触发情况,若在超过2秒的情况下再触发,则为1423
- 每点击一次打印一次3
- 14为同步代码
- 23为异步代码,使用回调函数接收结果
总结:
- 什么是同步代码?
- 逐行执行,原地等待结果后,才继续向下执行
- 什么是异步代码?
- 调用后耗时,不阻塞代码执行,将来完成后触发回调函数
- JS中有哪些异步代码?
- setTimeout / setlnterval
- 事件
- AJAX
- 异步代码如何接收结果?
- 依靠回调函数来接收
回调函数地狱
回调函数地狱
概念含义:在回调函数中嵌套回调函数,一直嵌套下去就形成了回调函数地狱
缺点:可读性差,异常无法捕获,耦合性严重,牵一发动全身
典型案例: 需求:在下拉菜单中,默认展示:第一个省&第一个市&第一个区
axios({ ur1: 'http: //hmajax.itheima.net/api/province' }).then(result => {
const pname = result.data.list[0]
document.queryselector('.province').innerHTML = pname
//获取第一个省份默认下属的第一个城市名字
axios({ ur1: 'http://hmajax.itheima.net/api/city', params: { pname } }).then(result => {
const cname = result.data.list[0]
document. queryselector('.city').innerHTML = cname
//获取第一个城市默认下属第一个地区名字
axios ({ ur1: 'http://hmajax.itheima.net/api/area',params: { pname,cname } }).then(result => {
document.queryselector('.area ').innerHTML = resu1t.data.list[0]
})
})
})
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>回调地狱</title>
</head>
<body>
<form>
<span>省份:</span>
<select>
<option class="province"></option>
</select>
<span>城市:</span>
<select>
<option class="city"></option>
</select>
<span>地区:</span>
<select>
<option class="area"></option>
</select>
</form>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
/**
* 目标:演示回调函数地狱
* 需求:获取默认第一个省,第一个市,第一个地区并展示在下拉菜单中
* 概念:在回调函数中嵌套回调函数,一直嵌套下去就形成了回调函数地狱
* 缺点:可读性差,异常无法获取,耦合性严重,牵一发动全身
*/
// 1. 获取默认第一个省份的名字
axios({url: 'http://hmajax.itheima.net/api/province'}).then(result => {
const pname = result.data.list[0]
document.querySelector('.province').innerHTML = pname
// 2. 获取默认第一个城市的名字
axios({url: 'http://hmajax.itheima.net/api/city', params: { pname }}).then(result => {
const cname = result.data.list[0]
document.querySelector('.city').innerHTML = cname
// 3. 获取默认第一个地区的名字
axios({url: 'http://hmajax.itheima.net/api/area', params: { pname, cname }}).then(result => {
console.log(result)
const areaName = result.data.list[0]
document.querySelector('.area').innerHTML = areaName
})
})
}).catch(error => {
console.dir(error)
})
</script>
</body>
</html>
- 制造里层回调错误,却在最外层接收错误→无法捕获实际错误
- axios源码抛出异常(未捕获)
总结:
什么是回调函数地狱?
在回调函数中,一直向下嵌套回调函数,形成回调函数地狱回调函数地狱问题?
可读性差 + 异常捕获困难 + 耦合性严重
Promise链式调用
- 目的:利用Promise链式调用特性,解决回调函数地狱问题
- 在创建 new promise() 对象时,里面就会管理一个异步任务
- 通过 .then(回调函数) 拿到该异步任务成功的结果
(即通过promise()对象内.then 的方法中传入回调函数,以接收成功的结果)- 由于.then 本身也是一个方法的调用,在原地也有返回值
- 这个返回值又是一个新的promise对象,里面可以继续嵌套管理异步任务
- 异步任务依赖于上一个 .then(回调函数) 中,return的结果
(.then() 回调函数中return的结果,会影响新生成的promise对象最终状态和结果)
概念定义:
- 依靠 .then() 方法,会返回一个新生成的Promise对象的特性(即 xx.then 的return是一个新对象),继续串联下一环任务,直到所需要的异步任务嵌套结束
细节:
- .then() 回调函数中的返回值,会影响新生成的 Promise对象最终状态和结果
(即,处理后的结果,无论是返回的成功还是失败的结果,都是返回一个新的对象)
- .then() 回调函数中的返回值,会影响新生成的 Promise对象最终状态和结果
Promise链式嵌套调用典型案例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Promise_链式调用</title>
</head>
<body>
<script>
/**
* 目标:掌握Promise的链式调用
* 需求:把省市的嵌套结构,改成链式调用的线性结构
*/
// 1. 创建Promise对象-模拟请求省份名字
const p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('北京市')
}, 2000)
})
// 2. 获取省份名字
// p.then即,前一个p=Promise()对象的回调函数,是一个新对象
const p2 = p.then(result => {
console.log(result)
// 3. 创建Promise对象-模拟请求城市名字
// return Promise对象最终状态和结果,影响到新的Promise对象(P2)
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(result + '--- 北京')
}, 2000)
})
})
// 4. 获取城市名字(p2结果)
p2.then(result => {
console.log(result)
})
// then()原地的结果是一个新的Promise对象
console.log(p2 === p)
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Promise_链式调用</title>
</head>
<body>
<script>
/**
* 目标:掌握Promise的链式调用
* 需求:把省市的嵌套结构,改成链式调用的线性结构
*/
// 1. 创建Promise对象-模拟请求省份名字
const p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('北京市')
}, 2000)
})
// 2. 获取省份名字
// p.then即,前一个p=Promise()对象的回调函数,是一个新对象
const p2 = p.then(result => {
console.log(result)
// 3. 创建Promise对象-模拟请求城市名字
// return Promise对象最终状态和结果,影响到新的Promise对象(P2)
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(result + '--- 北京')
}, 2000)
})
})
// 获取区名字(p3结果)
const p3 = p2.then(result => {
console.log(result)
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(result + p2 + '--- 北京')
}, 2000)
})
})
p3.then(result => {
console.log(result)
})
// then()原地的结果是一个新的Promise对象
console.log(p2 === p)
</script>
</body>
</html>
总结:
- 什么是Promise的链式调用?
- 使用then函数返回新Promise对象特性,一直串联下去
- then回调函数中,return的值会传给哪里?
- 传给then函数生成的新Promise对象
- Promise链式调用有什么用?
- 解决回调函数嵌套问题(由多层嵌套结构,变成环环相扣、线性串联的链式结构)
- 什么是Promise的链式调用?
省-市-区 三级链式调用案例
做法:每个 promise对象中,管理一个异步任务,用then返回promise对象,串联起来
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>三级链式调用案例</title>
</head>
<body>
<form>
<span>省份:</span>
<select>
<option class="province"></option>
</select>
<span>城市:</span>
<select>
<option class="city"></option>
</select>
<span>地区:</span>
<select>
<option class="area"></option>
</select>
</form>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
/**
*目标:把回调函数嵌套代码,改成Promise链式调用结构
*需求:获取默认第一个省,第一个市,第一个地区并展示在下拉菜单中
*/
//
// 设一个全局的变量,存放省份
let pname = ''
// 1.得到-获取省份Promise对象
axios({url: 'http://hmajax.itheima.net/api/province' })
// 第一级回调函数,接收省份结果
.then(result => {
pname = result.data.list[0]
document.querySelector( '.province' ).innerHTML = pname
// 2.得到-获取城市Promise对象
return axios({url:'http://hmajax.itheima.net/api/city', params: { pname }})
})
// 第二级回调函数,是在前一个.then的基础上再套.then回调函数
.then(result =>{
const cname = result.data.list[0]
document.querySelector( '.city' ).innerHTML = cname
// 3.得到-获取地区Promise对象
return axios({url: 'http://hmajax.itheima.net/api/area' , params: { pname,cname } })
})
.then(result =>{
console.log(result)
const areaName = result.data.list[0]
document.querySelector( '.area' ).innerHTML = areaName
})
</script>
</body>
</html>
async函数和await
async函数和await
意义:
- 被称作Javascript中异步编程的终极解决方案
定义:
- async函数是使用async关键字声明的函数。
- async函数是AsyncFunction构造函数的实例,并且其中允许使用await关键字。
- async和await关键字让我们可以用一种更简洁的方式写出基于Promise的异步行为,而无需刻意地链式调用promise。
概念:
- 在async函数内,使用 await 关键字取代 then 函数,等待获取Promise对象成功状态的结果值
语法示例
<body>
<script>
//获取默认省市区
async function getDefau1tArea(){
const pObj = await axios({ ur1: 'http://hmajax.itheima.net/api/province' })
const pname = pObj.data.list[0]
const cObj = await axios({ ur1: 'http://hmajax.itheima.net/api/city',params:{ pname }})
const cname = cObj.data.list[0]
const aObj = await axios({ ur1: 'http://hmajax.itheima.net/api/area',params: { pname,cname } })
const aname = aObj.data.list[0]
//赋子到页面上
document.queryselector('.province').innerHTML = pname
document.queryselector('.city').innerHTML = cname
document.queryselector('.area').innerHTML = aname
}
getDefau1tArea()
</script>
</body>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>async函数和await_解决回调函数地狱</title>
</head>
<body>
<form>
<span>省份:</span>
<select>
<option class="province"></option>
</select>
<span>城市:</span>
<select>
<option class="city"></option>
</select>
<span>地区:</span>
<select>
<option class="area"></option>
</select>
</form>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
/**
* 目标:掌握async和await语法,解决回调函数地狱
* 概念:在async函数内,使用await关键字,获取Promise对象"成功状态"结果值
* 注意:await必须用在async修饰的函数内(await会阻止"异步函数内"代码继续执行,原地等待结果)
*/
// 1. 定义async修饰函数
async function getData() {
// 2. await等待Promise对象成功的结果
const pObj = await axios({url: 'http://hmajax.itheima.net/api/province'})
const pname = pObj.data.list[0]
const cObj = await axios({url: 'http://hmajax.itheima.net/api/city', params: { pname }})
const cname = cObj.data.list[0]
const aObj = await axios({url: 'http://hmajax.itheima.net/api/area', params: { pname, cname }})
const areaName = aObj.data.list[0]
document.querySelector('.province').innerHTML = pname
document.querySelector('.city').innerHTML = cname
document.querySelector('.area').innerHTML = areaName
}
getData()
</script>
</body>
</html>
try...catch捕获错误
async函数和await中,通过try...catch捕获错误
- 使用:
- 通过try...catch语句,标记要尝试的语句块,并指定一个出现异常时抛出的响应。
语法示例:
<body>
<script>
try {
//要执行的代码
}catch (error) {
// error接收的是,错误信息
// try里代码,如果有错误,直接进入这里执行
}
</script>
</body>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>async函数和await_错误捕获</title>
</head>
<body>
<form>
<span>省份:</span>
<select>
<option class="province"></option>
</select>
<span>城市:</span>
<select>
<option class="city"></option>
</select>
<span>地区:</span>
<select>
<option class="area"></option>
</select>
</form>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
/**
* 目标:async和await_错误捕获
*/
async function getData() {
// 1. try包裹可能产生错误的代码
try {
const pObj = await axios({ url: 'http://hmajax.itheima.net/api/province' })
const pname = pObj.data.list[0]
const cObj = await axios({ url: 'http://hmajax.itheima.net/api/city', params: { pname } })
const cname = cObj.data.list[0]
const aObj = await axios({ url: 'http://hmajax.itheima.net/api/area', params: { pname, cname } })
// 如,制造出错案例,在area后面加了“1”
// const aObj = await axios({ url: 'http://hmajax.itheima.net/api/area1', params: { pname, cname } })
const areaName = aObj.data.list[0]
document.querySelector('.province').innerHTML = pname
document.querySelector('.city').innerHTML = cname
document.querySelector('.area').innerHTML = areaName
} catch (error) {
// 2. 接着调用catch块,接收错误信息
// 如果try里某行代码报错后,try中剩余的代码不会执行了
// 通过console.dir(error)打印错误信息,发现接口返回的,真正的响应数据在错误对象(该对象中有着axios处理过的提供的一些其他信息)的response属性中
console.dir(error)
// 即 response.data.message,也可在 浏览器调试>网络 里面查看到
}
}
getData()
// 备注:在try语句中,如果某句代码发生错误,后面的不会再执行
// 因此,若加1后,地区获取不到,那么下面的标签赋予innerHTML的语句全都不执行
</script>
</body>
</html>
事件循环 EventLoop
意义:
- 通过学习事件循环 EventLoop,掌握JavaScript是如何 安排和运行代码
通过两个案例认识
<body>
<script>
console.log(1)
setTimeout(()=>{
console.log(2)
},2000)
console.log(3)
</script>
</body>
<body>
<script>
console.log(1)
setTimeout(()=>{
console.log(2)
},0)
console.log(3)
</script>
</body>
两个案例的运行结果:打印的顺序都是 1 3 2
案例分析:console.log是同步任务,顺序执行,先执行,因此打印 1 3 ,而setTimeout是异步任务,无论延迟多少,都是在同步任务之后
事件循环 EventLoop
概念:
- JavaScript 有一个基于事件循环的并发模型,事件循环负责执行代码、收集和处理事件,以及执行队列中的子任务。这个模型与其它语言中的模型截然不同,比如C和Java。
原因:
- JavaScript 的单线程特性(某一刻只能执行一行代码),为了让耗时代码不阻塞其他代码运行,设计了事件循环模型
备注:
调用栈:可以理解为 JS代码在运行时形成的调用环境
宿主环境:浏览器(注意:浏览器是多线程的,JS是单线程的)
任务队列:内存开辟的一块空间
初步理解事件循环 EventLoop的执行过程
什么是事件循环?
- 执行代码和收集异步任务,在调用栈空闲时,反复调用任务队列里回调函数执行机制
为什么有事件循环?
- JavaScript是单线程的,为了不阻塞JS引擎,设计执行代码的模型
JavaScript 内代码如何执行?
- 执行同步代码,遇到异步代码交给宿主浏览器环境执行
- 异步有了结果后,把回调函数放入任务队列排队
- 当调用栈空闲后,反复调用任务队列里的回调函数
通过典型案例/面试高频案例,认识事件循环,分析代码执行过程
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>事件循环_练习</title>
</head>
<body>
<script>
/**
* 目标:阅读并回答执行的顺序结果
*/
console.log(1)
setTimeout(() => {
console.log(2)
}, 0)
function myFn() {
console.log(3)
}
function ajaxFn() {
const xhr = new XMLHttpRequest()
xhr.open('GET', 'http://hmajax.itheima.net/api/province')
xhr.addEventListener('loadend', () => {
console.log(4)
})
xhr.send()
}
for (let i = 0; i < 1; i++) {
console.log(5)
}
ajaxFn()
document.addEventListener('click', () => {
console.log(6)
})
myFn()
// 1 5 3 2 4 点击一次document就会执行一次打印6
</script>
</body>
</html>
备注:
深入理解宏任务与微任务(深入理解事件循环)
宏任务与微任务
ES6之后引入了Promise对象,让JS引擎也可以发起异步任务
其中异步任务又分为:
宏任务:由浏览器环境执行的异步代码 微任务:由JS 引擎环境执行的异步代码
宏任务(代码) | 执行所在环境 |
---|---|
JS脚本执行事件(script) | 浏览器 |
setTimeout/setInterval | 浏览器 |
AJAX请求完成事件 | 浏览器 |
用户交互事件等 | 浏览器 |
微任务(代码) | 执行所在环境 |
---|---|
Promise对象中的.then()方法 | js引擎 |
注:Promise本身是同步的,而then和catch回调函数是异步的
认识宏任务与微任务的执行顺序
什么是宏任务
- 交给浏览器管理和执行的异步代码
什么是微任务(在JS引擎中执行,先与宏任务执行)
- JS引擎发起和管理执行的异步代码
宏任务与微任务执行顺序典型案例
<body>
<script>
console.log(1)
setTimeout(() => {
console.log(2)
},0)
const p = new Promise((resolve,reject) => {
console.log(3)
resolve(4)
})
p.then( result => {
console.log(result)
})
console.log(5)
</script>
</body>
什么是宏任务?
- 浏览器执行的异步代码
- 例如:
- JS执行脚本事件,
- setTimeout/setInterval,
- AJAX请求完成事件,
- 用户交互事件等(鼠标滚轮,点击等等)
什么是微任务?
- JS 引擎执行的异步代码
- 例如:
- Promise对象.then()的回调
JavaScript内代码如何执行?
- 执行第一个script脚本事件宏任务,里面同步代码
- 遇到宏任务/微任务交给宿主环境,有结果回调函数进入对应队列
- 当执行栈空闲时,清空微任务队列,再执行下一个宏任务,从1再来
宏任务与微任务经典面试题案例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>事件循环经典_经典面试题</title>
</head>
<body>
<script>
// 目标:回答代码执行顺序
console.log(1)
setTimeout(() => {
console.log(2)
const p = new Promise(resolve => resolve(3))
p.then(result => console.log(result))
}, 0)
const p = new Promise(resolve => {
setTimeout(() => {
console.log(4)
}, 0)
resolve(5)
})
p.then(result => console.log(result))
const p2 = new Promise(resolve => resolve(6))
p2.then(result => console.log(result))
console.log(7)
// 1 7 5 6 2 3 4
</script>
</body>
</html>
promise.all
概念:合并多个promise对象,等待所有同时成功完成(或某一个失败),再做后续逻辑
promise.all典型语法案例
<body>
<script>
const p = Promise.all([Promise对象,Promise对象,...])
p.then(result => {
// result结果:[Promise对象成功结果,Promise对象成功结果,...]
})
.catch(error =>{
// 第一个失败的Promise对象,抛出的异常
})
</script>
</body>
promise.all应用案例
需求:同时请求“北京”,“上海”,“广州”,“深圳”的天气并在网页尽可能同时显示
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Promise的all方法</title>
</head>
<body>
<ul class="my-ul"></ul>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
/**
* 目标:了解Promise的all方法作用,和使用场景
* 业务:当我需要同一时间显示多个请求的结果时,就要把多请求合并
* 例如:默认显示"北京","上海","广州","深圳"的天气在首页滚动查看
* code:
* 北京-110100
* 上海-310100
* 广州-440100
* 深圳-440300
* */
// 1.请求城市天气,得到Promise对象
const bjPromise = axios({ url: 'http://hmajax.itheima.net/api/weather' , params: { city: '110100'} })
const shPromise = axios({ url: 'http://hmajax.itheima.net/api/weather' , params: { city: '310100'} })
const gzPromise = axios({ url: 'http://hmajax.itheima.net/api/weather' , params: { city: '440100'} })
const szPromise = axios({ url: 'http://hmajax.itheima.net/api/weather' , params: { city: '440300'} })
// 2.使用Promise.all,合并多个Promise对象
const p = Promise.all([bjPromise,shPromise,gzPromise,szPromise])
p.then(result =>{
//注意:结果数组顺序和合并时顺序是一致
console.log(result)
const htmlstr = result.map( item => {
return `<li>${item.data.data.area} --- ${item.data.data.weather}</li>`
})
.join('')
document.querySelector('.my-ul').innerHTML =
})
.catch(error => {
console.dir(error)
})
</script>
</body>
</html>
综合大案例
商品分类
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>案例_分类导航</title>
<link rel="stylesheet" href="./css/index.css">
</head>
<body>
<!-- 大容器 -->
<div class="container">
<div class="sub-list">
<div class="item">
<h3>分类名字</h3>
<ul>
<li>
<a href="javascript:;">
<img src="http://zhoushugang.gitee.io/erabbit-client-pc-static/uploads/img/category%20(9).png" />
<p>巧克力</p>
</a>
</li>
<li>
<a href="javascript:;">
<img src="http://zhoushugang.gitee.io/erabbit-client-pc-static/uploads/img/category%20(9).png" />
<p>巧克力</p>
</a>
</li>
<li>
<a href="javascript:;">
<img src="http://zhoushugang.gitee.io/erabbit-client-pc-static/uploads/img/category%20(9).png" />
<p>巧克力</p>
</a>
</li>
</ul>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
/**
* 目标:把所有商品分类“同时”渲染到页面上
* 1. 获取所有一级分类数据
* 2. 遍历id,创建获取二级分类请求
* 3. 合并所有二级分类Promise对象
* 4. 等待同时成功后,渲染页面
*/
// 1. 获取所有一级分类数据
axios({
url: 'http://hmajax.itheima.net/api/category/top'
}).then(result => {
console.log(result)
// 2. 遍历id,创建获取二级分类请求
const secPromiseList = result.data.data.map(item => {
return axios({
url: 'http://hmajax.itheima.net/api/category/sub',
params: {
id: item.id // 一级分类id
}
})
})
console.log(secPromiseList) // [二级分类请求Promise对象,二级分类请求Promise对象,...]
// 3. 合并所有二级分类Promise对象
const p = Promise.all(secPromiseList)
p.then(result => {
console.log(result)
// 4. 等待同时成功后,渲染页面
const htmlStr = result.map(item => {
const dataObj = item.data.data // 取出关键数据对象
return `<div class="item">
<h3>${dataObj.name}</h3>
<ul>
${dataObj.children.map(item => {
return `<li>
<a href="javascript:;">
<img src="${item.picture}">
<p>${item.name}</p>
</a>
</li>`
}).join('')}
</ul>
</div>`
}).join('')
console.log(htmlStr)
document.querySelector('.sub-list').innerHTML = htmlStr
})
})
</script>
</body>
</html>
学习反馈-省、市、区菜单/切换/数据提交
样式结构:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- 初始化样式 -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/reset.css@2.0.2/reset.min.css">
<!-- 引入bootstrap.css -->
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.2.3/css/bootstrap.min.css" rel="stylesheet">
<!-- 核心样式 -->
<link rel="stylesheet" href="./css/index.css">
<title>学习反馈</title>
</head>
<body>
<div class="container">
<h4 class="stu-title">学习反馈</h4>
<img class="bg" src="./img/head.png" alt="">
<div class="item-wrap">
<div class="hot-area">
<span class="hot">热门校区</span>
<ul class="nav">
<li><a target="_blank" href="http://bjcp.itheima.com/">北京</a> </li>
<li><a target="_blank" href="http://sh.itheima.com/">上海</a> </li>
<li><a target="_blank" href="http://gz.itheima.com/">广州</a> </li>
<li><a target="_blank" href="http://sz.itheima.com/">深圳</a> </li>
</ul>
</div>
<form class="info-form">
<div class="area-box">
<span class="title">地区选择</span>
<select name="province" class="province">
<option value="">省份</option>
</select>
<select name="city" class="city">
<option value="">城市</option>
</select>
<select name="area" class="area">
<option value="">地区</option>
</select>
</div>
<div class="area-box">
<span class="title">您的称呼</span>
<input type="text" name="nickname" class="nickname" value="播仔">
</div>
<div class="area-box">
<span class="title">宝贵建议</span>
<textarea type="text" name="feedback" class="feedback" placeholder="您对AJAX阶段课程宝贵的建议"></textarea>
</div>
<div class="area-box">
<button type="button" class="btn btn-secondary submit">
确定提交
</button>
</div>
</form>
</div>
</div>
<script src="https://cdn.bootcdn.net/ajax/libs/axios/1.2.0/axios.min.js"></script>
<script src="./js/form-serialize.js"></script>
<!-- 核心代码 -->
<script src="./js/index.js"></script>
</body>
</html>
功能核心JS代码(./js/index.js):
<body>
<script>
/**
* 目标1:完成省市区下拉列表切换
* 1.1 设置省份下拉菜单数据
* 1.2 切换省份,设置城市下拉菜单数据,清空地区下拉菜单
* 1.3 切换城市,设置地区下拉菜单数据
*/
// 1.1 设置省份下拉菜单数据
axios({
url: 'http://hmajax.itheima.net/api/province'
}).then(result => {
const optionStr = result.data.list.map(pname => `<option value="${pname}">${pname}</option>`).join('')
document.querySelector('.province').innerHTML = `<option value="">省份</option>` + optionStr
})
// 1.2 切换省份,设置城市下拉菜单数据,清空地区下拉菜单
document.querySelector('.province').addEventListener('change', async e => {
// 获取用户选择省份名字
// console.log(e.target.value)
const result = await axios({ url: 'http://hmajax.itheima.net/api/city', params: { pname: e.target.value } })
const optionStr = result.data.list.map(cname => `<option value="${cname}">${cname}</option>`).join('')
// 把默认城市选项+下属城市数据插入select中
document.querySelector('.city').innerHTML = `<option value="">城市</option>` + optionStr
// 清空地区数据
document.querySelector('.area').innerHTML = `<option value="">地区</option>`
})
// 1.3 切换城市,设置地区下拉菜单数据
document.querySelector('.city').addEventListener('change', async e => {
console.log(e.target.value)
const result = await axios({url: 'http://hmajax.itheima.net/api/area', params: {
pname: document.querySelector('.province').value,
cname: e.target.value
}})
console.log(result)
const optionStr = result.data.list.map(aname => `<option value="${aname}">${aname}</option>`).join('')
console.log(optionStr)
document.querySelector('.area').innerHTML = `<option value="">地区</option>` + optionStr
})
/**
* 目标2:收集数据提交保存
* 2.1 监听提交的点击事件
* 2.2 依靠插件收集表单数据
* 2.3 基于axios提交保存,显示结果
*/
// 2.1 监听提交的点击事件
document.querySelector('.submit').addEventListener('click', async () => {
// 2.2 依靠插件收集表单数据
const form = document.querySelector('.info-form')
const data = serialize(form, { hash: true, empty: true })
console.log(data)
// 2.3 基于axios提交保存,显示结果
try {
const result = await axios({
url: 'http://hmajax.itheima.net/api/feedback',
method: 'POST',
data
})
console.log(result)
alert(result.data.message)
} catch (error) {
console.dir(error)
alert(error.response.data.message)
}
})
</script>
</body>
练手项目(token、接口)
项目介绍
功能:
- 登录和权限判断
- 查看文章内容列表(筛选,分页)
- 编辑文章(数据回显)
- 删除文章
- 发布文章(图片上传,富文本编辑器)
详见视频
项目准备
技术:
- 基于 Bootstrap 搭建网站标签和样式
- 集成 wangEditor 插件实现富文本编辑器
- 使用原生 JS 完成增删改查等业务
- 基于 axios 与黑马头条线上接口交互
- 使用 axios 拦截器进行权限判断
项目准备: 准备配套的素材代码 包含: html,css,js,静态图片,第三方插件等等
目录管理: 建议这样管理,方便查找
- assets:资源文件夹(图片,字体等)
- lib:资料文件夹(第三方插件,例如:form-serialize)
- page:页面文件夹
- utils:实用程序文件夹(工具插件)****
备注:
- 实际生产中,看个人使用习惯
- 实际工作中,看团队规定
验证码模块
验证码登录
目标:完成验证码登录,后端设置验证码默认为 246810
原因:因为短信接口不是免费的,防止攻击者恶意盗刷
步骤: 1.在 utils/request.js 配置 axios 请求基地址
- 作用:提取公共前缀地址,配置后 axios 请求时都会 baseURL+url 2.收集手机号和验证码数据 3.基于 axios 调用验证码登录接口 4.使用 Bootstrap 的 Alert 警告框反馈结果给用户
- 登录成功
- 手机号或验证码不正确
<!--引入样式、标签结构 axios.min.js bootstrap.min.js form-serialize.js插件 封装的公共使用的请求插件request.js 弹窗插件alert.js 当前页面的js逻辑index.js-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.2.3/css/bootstrap.min.css">
<link rel="stylesheet" href="./index.css">
<title>黑马头条-数据管理平台</title>
</head>
<body>
<!-- 警告框 -->
<div class="alert info-box">
操作结果
</div>
<!-- 登录页面 -->
<div class="login-wrap">
<div class="title">黑马头条</div>
<div>
<form class="login-form">
<div class="item">
<input type="text" class="form-control" name="mobile" placeholder="请输入手机号" value="13888888888">
</div>
<div class="item">
<input type="text" class="form-control" name="code" placeholder="默认验证码246810" value="246810">
</div>
<div class="item">
<button type="button" class="btn btn-primary btn">登 录</button>
</div>
</form>
</div>
</div>
<script src="https://cdn.bootcdn.net/ajax/libs/axios/1.3.4/axios.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.2.3/js/bootstrap.min.js"></script>
<script src="../../lib/form-serialize.js"></script>
<script src="../../utils/request.js"></script>
<script src="../../utils/alert.js"></script>
<script src="./index.js"></script>
</body>
</html>
// axios 公共配置
// 基地址
axios.defaults.baseURL = "http://geek.itheima.net";
/**
* 目标1:验证码登录
* 1.1 在 utils/request.js 配置 axios 请求基地址
* 1.2 收集手机号和验证码数据
* 1.3 基于 axios 调用验证码登录接口
* 1.4 使用 Bootstrap 的 Alert 警告框反馈结果给用户
*/
document.querySelector(".btn").addEventListener("click", () => {
const form = document.querySelector(".login-form");
// 使用form-serialize插件
const data = serialize(form, { hash: true, empty: true });
// console.log(data);
axios({
url: "/v1_0/authorizations",
method: "post",
data,
})
.then((result) => {
myAlert(true, "登录成功");
localStorage.setItem("token", result.data.token);
setTimeout(() => {
location.href = "../content/index.html";
}, 1500);
console.log(result);
})
.catch((error) => {
// 注意 myAlert 里面的内容,不要加引号了,加了引号就是变成和登录成功一样的字符串,无法返回动态的信息了
myAlert(false, error.response.data.message);
console.dir(error.response.data.message);
});
});
验证码流程原理
见视频见图,后补
Token概念
- 概念:访问权限的令牌,本质上是一串字符串
- 创建:正确登录后,由后端签发并返回
- 作用:判断是否有登录状态等,控制访问权限
- 注意:前端只能判断 token 有无,而后端才能判断 token 的有效性
Token的使用
目标:只有登录状态,才可以访问内容页面
步骤:
- 在 utils/auth.is 中判断无 token 令牌字符串,则强制跳转到登录页(手动修改地址栏测试)
- 在登录成功后,保存 token 令牌字符串到本地,再跳转到首页(手动修改地址栏测试)
/**
* 目标1:验证码登录
* 1.1 在 utils/request.js 配置 axios 请求基地址
* 1.2 收集手机号和验证码数据
* 1.3 基于 axios 调用验证码登录接口
* 1.4 使用 Bootstrap 的 Alert 警告框反馈结果给用户
*/
document.querySelector(".btn").addEventListener("click", () => {
const form = document.querySelector(".login-form");
// 使用form-serialize插件
const data = serialize(form, { hash: true, empty: true });
console.log(data);
axios({
url: "/v1_0/authorizations",
method: "post",
data,
})
.then((result) => {
myAlert(true, "登录成功");
// 保存登录成功后服务器返回的token到本地,以后续打开其他页面时验证
localStorage.setItem("token", result.data.token);
setTimeout(() => {
// 延迟跳转,让警告框弹显示一下
location.href = "../content/index.html";
}, 1500);
console.log(result);
})
.catch((error) => {
myAlert(false, error.response.data.message);
console.dir(error.response.data.message);
});
});
// 权限插件(引入到了除登录页面,以外的其他所有页面)
/**
* 目标1:访问权限控制
* 1.1 判断无 token 令牌字符串,则强制跳转到登录页
* 1.2 登录成功后,保存 token 令牌字符串到本地,并跳转到内容列表页面
*/
const token = localStorage.getItem("token");
if (!token) {
location.href = "../login/index.html";
}
/**
* 目标2:设置个人信息
* 2.1 在 utils/request.js 设置请求拦截器,统一携带 token
* 2.2 请求个人信息并设置到页面
*/
axios({
url: "/v1_0/user/profile",
}).then((result) => {
console.log(result);
const username = result.data.name;
document.querySelector(".nick-name").innerHTML = username;
});
/**
* 目标3:退出登录
* 3.1 绑定点击事件
* 3.2 清空本地缓存,跳转到登录页面
*/
document.querySelector(".quit").addEventListener("click", (e) => {
localStorage.clear();
myAlert(true, "退出登录成功");
setTimeout(() => {
location.href = "../login/index.html";
}, 1500);
});
Token的作用总结
- Token 的作用?
- 判断用户是否有登录状态等
- Token 的注意点:
- 前端只能判断 token 的有无
- 后端通过解密可以提取 token 字符串的原始信息,判断有效性
Axios请求拦截器(个人信息设置的需求案例)
需求:设置用户昵称(登陆后,从服务器获取后,设置在内容管理/发布文章页面右上角导航) 语法:axios 可以在 headers 选项 传递请求头参数
格式:位置 headers 类型 string 格式(后端要求) Bearer token 携带方法:
// token请求头语法
axios({
ur1:'目标资源地址',
headers:{
Authorization:`Bearer ${localstorage.getItem('token')}`
}
})
问题:很多接口,都需要携带 token 令牌字符串 解决:在请求拦截器统一设置公共 headers 选项
axios 请求拦截器:发起请求之前,触发的配置函数,对请求参数进行额外配置
// axios 请求拦截器
axios.interceptors.request.use(
// 第一个函数体,请求拦截器
function(config){
// 往请求参数的配置对象当中的请求头,添加参数名和值,即Authorization:`Bearer ${localstorage.getItem('token')
const token = location.getItem('token')
// 将请求参数的配置对象,即将config,return到axios源码内
// 统一携带token令牌字符串到请求头上
// 此处通过 && 逻辑与,将获取到的 token 与 请求头 一并return到新的token中
token && (config.headers.Authorization = `Bearer ${token}`)
// 在发送请求之前做些什么
return config
},
// 第二个函数体
function(error){
// 在请求发起时,对请求错误做些什么
return Promise.reject(error)
}
)
// 在请求发送之前对请求进行拦截,添加Authorization头,将token添加到请求头中
axios.interceptors.request.use(
function (config) {
// 从localStorage中获取token
const token = localStorage.getItem("token");
// 如果token存在,则将token添加到请求头中
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
// 返回配置对象
return config;
},
function (error) {
// 如果请求发送失败,则返回错误信息
return Promise.reject(error);
}
);
axios属性axios.interceptors.request.use
use方法调用,传递2个函数体:
- 请求拦截器:
- 往请求参数的配置对象当中的请求头,添加参数名和值
- 将请求参数的配置对象,即将config相关参数的配置对象作设置,并return到axios源码内
- 请求发起时,作相关的处理 :
各接口使用的公共的请求参数,合并放到请求拦截器统一携带,即引入的request.js封装的请求插件,以放入请求相关的公共设置
- 拦截器详见 https://axios.js.cn/guide/%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E.html#%E6%8B%A6%E6%88%AA%E5%99%A8
总结:
- 什么是 axios 请求拦截器?
- 发起请求之前,调用的一个函数,对请求参数进行设置
- axios 请求拦截器,什么时候使用?
- 有公共配置和设置时,统一设置在请求拦截器中
Axios响应拦截器(身份验证失败案例)
定义:服务器响应回的结果,到达具体逻辑页面 then/catch 之前,触发axios的拦截函数,对响应结果统一处理
// 添加响应拦截器
axios.interceptors.response.use(
function (response) {
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么
// 这里的 response 函数的结果,会返回到axios中的then中的result
const result=response.data;
return result;
},
function (error) {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
console.dir(error);
// 可选链式操作符
// 例如对401身份验证失败的情况做处理
// if 有报错>有回应>有状态===401
if(error?.response?.status==401){
alert('身份验证失败,请重新登录');
localStorage.clear();
location.href='../login/index.html';
}
return Promise.reject(error);
}
);
PS:
可选链式操作符 (?.) 是 JavaScript 中的一个运算符,它允许你安全地访问嵌套对象的属性,而无需担心对象或其属性是否为 undefined。
可选链式操作符的工作原理是:它从左到右依次检查每个属性。如果某个属性不存在或为 undefined,则整个表达式返回 undefined。否则,它返回最后一个属性的值。
const user = {
name: 'John Doe',
address: {
street: '123 Main Street',
city: 'Anytown',
state: 'CA',
zip: '12345',
},
};
const streetAddress = user.address?.street;
在这个例子中,user.address 存在,因此可选链式操作符会继续检查 street 属性。
由于 user.address.street 也存在,因此 streetAddress 的值为 "123 Main Street"。
如果 user.address 为 undefined,则 user.address?.street 将返回 undefined,因为可选链式操作符会在遇到第一个 undefined 值时停止求值。
可选链式操作符对于处理可能为 undefined 的嵌套对象非常有用。它可以帮助避免编写冗长的 if 语句或使用默认值来处理不存在的属性。
- 什么是 axios 响应拦截器?
- 响应回到 then/catch 之前,触发的拦截函数,对响应结果统一处理
- axios 响应拦截器,什么时候触发成功/失败的回调函数?
- 状态为 2xx 触发成功回调,其他则触发失败的回调函数
优化axios响应结果
服务器返回的数据对象,在浏览器调试中可以看到,是axios封装的对象,服务器返回的有效数据部分,是挂载到了axios封装的对象中的response.data.data中
目的,简化获取有效数据,减少嵌套,直接返回服务器的接口对象,而不是经过axios封装的接口对象
即,将axios封装中的result.data.data,转为真正的服务器result.data
原理,通过响应拦截器的返回的response.data值来取出
富文本编辑器(文章发布需求)
目标:发布文章页,富文本编辑器的集成
- 富文本:带样式,多格式的文本,在前端一般使用标签配合内联样式实现,innerHTML
- 富文本编辑器:用于编写富文本内容的容器
使用: wangEditor插件
步骤:参考文档
引入 CSS 定义样式
(在对应的index.html在引入style,一般会将对应页面的css全部抽离到独立的如index.css中)定义 HTML 结构
(先确定在哪里哪个内容位置插入,再在对应的html位置中引入)引入JS 创建编辑器
(在对应的index.html在引入,抽离独立的配置js)监听内容改变,保存在隐藏文本域(便于后期收集)
实际上是一个P标签,监听内容改变并保存在一个隐藏的textarea文本输入域,后续通过form-serialize.js提交
同理,其他插件的使用步骤雷同,参考官方文档
在通过复制粘贴引入后,需要理解代码的含义,确认每一句配置的意思,以确认效果,与项目产品按需对应修改 1.
// 富文本编辑器
// 创建编辑器函数,创建工具栏函数
// 声明全局window.wangEditor属性,对象赋值createEditor, createToolbar两个函数
const { createEditor, createToolbar } = window.wangEditor;
// 编辑器配置
const editorConfig = {
placeholder: "发布文章内容...",
// 编辑器变化时的回调函数
onChange(editor) {
// 获取富文本内容标签字符串
const html = editor.getHtml();
// 可以到官方文档中找,也能通过打印确认语句功能
console.log("editor content", html);
// 也可以同步到 <textarea>
// 为了后续快速收集表单内容
document.querySelector('.publish-content').value = html;
},
};
// 创建编辑器
const editor = createEditor({
// 创建位置
selector: "#editor-container",
// 默认内容
html: "<p><br></p>",
// 配置项
config: editorConfig,
// 配置集成模式
mode: "default", // or 'simple'
});
// 工具栏
// 工具栏配置对象默认为空
const toolbarConfig = {};
const toolbar = createToolbar({
// 为指定编辑器创建工具栏
editor,
// 工具栏的创建位置
selector: "#toolbar-container",
// 工具栏配置对象
config: toolbarConfig,
// 配置集成模式
mode: "default", // or 'simple'
});
接口获取、展示、与设置(发布文章所属频道列表展示与选定)
- 目标:展示频道列表,供用户选择
- 步骤:
- 获取频道列表数据
- 展示到下拉菜单中
- 思考:
- 编写代码之前要想一想,这段代码要不要复用呢?
- 要,因为在发表文章页,和内容管理页都有地方要获取频道列表需要展示,因此封装函数
- 服务器返回的数组数据,该如何展示呢?
- 打开页面,默认调用一次,即,当用户打开发布文章的页面时,就需要从服务器获取一次列表内容
- 通过打印的log确认,服务器返回的数据对象结构
- 数据对象结构:data.channels,数组中是具体一个个的频道数据对象,id值是频道的唯一标识,name是频道的需要展示的名称,即innerHTML
- 将数据对象结构截图记住,方便思路
- 回到代码中/或继续页面检查,确认展示位置的具体的html标签结构
- 标签结构1:发现是原生select标签下嵌套options,第一项默认是空值,即默认选中项
<option value="" selected>
,此项需要以innerHTML形式保留并作默认展示 - 标签结构2:下面接续的下拉数据,通过逐项循环映射生成 res.data.channels.map(item => 默认值+数组映射)
- 编写代码时,确认数据需要逐项映射成什么样的结构?见下表对应
- 当用户选中下拉某项时,代表已确认后续需要收集并返回服务器的项
- 也就是map(item =>中的数据),需要后续提交返回给服务器(即用户选中文章频道并提交发布),因此需要通过接口文档确认是返回什么数据给服务器
- 需要展示选中项的什么信息? 答
${item.name}
,填入option标签对中<option value="">${item.name}</option>
给人看的 - 需要收集/返回给服务器选中项的什么信息? 答
${item.id}
,填入<option value="${item.id}">
- 通过res.data.channels.map(item => 默认值+数组映射)完成标签字符串映射,收集成数组后,需要
.join(``)
拼接成字符串,通过htmlStr接收 - 确认最终需要插入页面的html字符串,通过调试找到要插入的位置的标签类名
.form-select
,通过document.querySelector(".form-select").innerHTML = htmlStr
插回去
- 编写代码之前要想一想,这段代码要不要复用呢?
HTML结构 (来源:页面index.html或者调试中找) | 服务器返回数据 (来源:调试中抓服务器返回的log) |
---|---|
总页面 <select class="form-select" id="channel_id" name="channel_id"> | data.channels:Array |
默认展示项<option value="" selected>请选择文章频道</option> | 0:{id:0,name:'推荐'} |
下拉展示第1项<option value="1">频道1</option> | 1:{id:1,name:'html'} |
下拉展示第2项<option value="2">频道2</option> | 2:{id:2,name:'开发者资讯'} |
下拉展示第3项<option value="3">频道3</option> | 3:{id:3,name:'c++'} |
/**
* 目标1:设置频道下拉菜单
* 1.1 获取频道列表数据
* 1.2 展示到下拉菜单中
*/
// 1.1 获取频道列表数据,封装函数
function setChannelList() {
// 通过axios从后端服务器获取频道接口信息
axios({
url: "/v1_0/channels",
})
// .then接收请求返回的响应结果
.then((result) => {
// 默认选中项+result.data.channels
const htmlStr =`<option value="" selected>请选择文章频道</option>` + result.data.channels
.map((item) => {
// 在用户选中下拉菜单中的某一个时,选中项的id值会被收集,名称会被展示
return `<option value="${item.id}">${item.name}</option>`;
})
// 拼接字符串
.join("<br>");
// 将htmlStr赋予给选中框
document.querySelector(".form-select").innerHTML = htmlStr;
});
}
// 网页运行后,默认调用一次
setChannelList();
// 除了使用.then接收请求返回的响应结果,还可以直接async和await
async function setChannelList() {
const res = await axios({
url: "/v1_0/channels",
})
console.log(res)
// 默认选中项 + result.data.channels
const htmlStr = `<option value="" selected>请选择文章频道</option>` + res.data.channels
.map(item => `<option value="${item.id}">${item.name}</option>`)
// 拼接字符串
.join('')
consloe.log(htmlStr)
// 将htmlStr赋予给选中框
document.querySelector(".form-select").innerHTML = htmlStr;
}
setChannelList();
// .map() 方法用于创建一个新数组,其元素是通过对输入数组的每个元素应用一个回调函数而产生的。
// 语法:
array.map((element, index, array) => {
// 回调函数
// ... 返回新的元素
})
// 参数:
// **element:**数组中的当前元素。
// **index(可选):**当前元素的索引。
// **array(可选):**原始数组。
// **返回值:**一个包含新元素的新数组。
// 用法:
// .map() 方法广泛用于对数组元素进行转换、过滤或映射。例如:
// 转换元素:
const numbers = [1, 2, 3, 4, 5];
const doubledNumbers = numbers.map((number) => number * 2);
console.log(doubledNumbers); // [2, 4, 6, 8, 10]
// 过滤元素:
const ages = [18, 25, 16, 30, 22];
const adultAges = ages.map((age) => {
if (age >= 18) {
return age;
}
});
console.log(adultAges); // [18, 25, 30, 22]
// 映射元素:
const users = [
{ id: 1, name: "John" },
{ id: 2, name: "Mary" },
{ id: 3, name: "Bob" },
];
const userNames = users.map((user) => user.name);
console.log(userNames); // ["John", "Mary", "Bob"]
// 优点:
// .map() 方法创建了一个新数组,不会修改原始数组。
// 它可以轻松地对数组元素进行转换、过滤或映射。
// 它是一个高阶函数,可以接受其他函数作为参数。
- 总结:
- 思考用到的接口是否需要复用,如果复用,就封装函数,基于axios,配合async和await拿到频道列表数据
- 分析数据结构和标签的对应关系,循环生成标签,并插入到指定位置
接口文件上传提交(发布文章封面设置)
- 目标:点击加号框(或“封面”文字)上传图片作为封面
- 步骤:
- 熟悉准备好的标签结构和对应显示和隐藏的样式类名
- 先确认input表单元素标签,实际上无法改成+加号盒子,因此实际上:+加号盒子,img标签,input表单元素,是3个标签
- input type属性值为file 的表单元素很难改成加号框,所以,加号盒子(label,所以监听的是input元素的change事件),img封面盒子(img)和input表单元素(input)为三个标签
- 封面回显的时候,需要使加号盒子隐藏,img显示(rounded类选择器使其隐藏)
- 获取选择的图片文件,并保存在 FormData表单数据对象中,因为后端图片上传接口要求传表单数据
- 单独上传图片并得到图片 URL 地址(图片传到服务器后,服务器会返回URL),后续需要将URL放到表单收集中提交
- 回显并切换 img 标签展示(并隐藏+号上传标签) 注意:图片地址临时存储在 img 标签上,并未和文章关联保存,也就是还没点击发布文章按钮,因此还没完成表单收集,还没提交到发布文章的接口
- 熟悉准备好的标签结构和对应显示和隐藏的样式类名
先确认标签结构,浏览器右键调试检查,或者找到对应页面的html
<div class="cover">
<!-- 标题 -->
<label for="img">封面:</label>
<!-- 右侧盒子 -->
<label for="img" class="place">+</label>
<!-- 备注:label标签for属性作用:通过for属性对应的值,关联input标签表单元素对应的id值,即点击封面标签和点击加号标签,都触发input标签 -->
<input class="img-file" type="file" name="img" id="img" hidden>
<!-- class="rounded"该类名在index.css中是隐藏的,加上css中的对应类名,就会显示或者隐藏 -->
<img class="rounded">
</div>
<!-- -->
通过获取并监测文件选择表单元素的change事件,拿到事件对象
/**
* 目标1:设置频道下拉菜单
* 1.1 获取频道列表数据
* 1.2 展示到下拉菜单中
*/
// 1.1 获取频道列表数据
async function setChannleList() {
const res = await axios({
url: '/v1_0/channels'
})
// 1.2 展示到下拉菜单中
const htmlStr = `<option value="" selected="">请选择文章频道</option>` + res.data.channels.map(item => `<option value="${item.id}">${item.name}</option>`).join('')
document.querySelector('.form-select').innerHTML = htmlStr
}
// 网页运行后,默认调用一次
setChannleList()
/**
* 目标2:文章封面设置
* 2.1 准备标签结构和样式
* 2.2 选择文件并保存在 FormData
* 2.3 单独上传图片并得到图片 URL 网址
* 2.4 回显并切换 img 标签展示(隐藏 + 号上传标签)
*/
// 2.2 选择文件并保存在 FormData
// 通过获取并监测文件选择表单元素的change事件,拿到事件对象
document.querySelector('.img-file').addEventListener('change', async e => {
// 属性中第0个元素就是用户选定的属性对象
const file = e.target.files[0]
// 接口规定,请求体body参数,是FormData表单数据对象,携带的参数名是image,value值是用户选择的文件对象file
const fd = new FormData()
fd.append('image', file)
// 2.3 调用上传图片的接口+拿到返回的响应数据,单独上传图片并得到图片 URL 网址
// 所在的函数是事件处理函数,以就近原则,找离最近的处理函数,加上async修饰,在当前的函数范围内,使用await接收结果
const res = await axios({
url: '/v1_0/upload',
method: 'POST',
data: fd
console.log(res);
})
// 2.4 回显并切换 img 标签展示(隐藏 + 号上传标签)
// 取出图片地址
const imgUrl = res.data.url
// 通过img标签的.rounded类名,赋予图片地址给img标签
document.querySelector('.rounded').src = imgUrl
document.querySelector('.rounded').classList.add('show')
document.querySelector('.place').classList.add('hide')
})
// 优化:点击 img 可以重新切换封面
// 思路:img 点击 => 用 JS 方式触发文件选择元素 click 事件方法
// 监测用户鼠标点击在了图片img标签上,然后获取表单元素,通过js方式调用标签对象的click事件方法
// 通过类名获取img标签,监听点击事件
document.querySelector('.rounded').addEventListener('click', () => {
// 在js代码中,通过调用这个标签的事件方法名字 .click ,来模拟用户触发一个事件
document.querySelector('.img-file').click()
})
// 给文件输入框添加change事件监听器
document.querySelector(".img-file").addEventListener("change", async (e) => {
// 获取选中的文件
const file = e.target.files[0];
// 创建一个FormData对象
const fd = new FormData();
// 将文件添加到FormData对象中
fd.append("image", file);
// 发送POST请求上传文件
const res = await axios({
url: "/v1_0/upload",
method: "post",
data: fd,
});
// 获取上传后返回的图片URL
const imgURL = res.data.url;
// 将图片URL设置到img元素的src属性上
document.querySelector(".rounded").src = imgURL;
// 显示图片
document.querySelector(".rounded").classList.add("show");
// 隐藏占位符
document.querySelector(".place").classList.add("hide");
});
// 给图片元素添加click事件监听器
document.querySelector(".rounded").addEventListener("click", () => {
// 触发文件输入框的click事件
document.querySelector(".img-file").click();
});
// append() 方法用于向 FormData 对象中追加一个键值对。它有以下几个用途:
// 添加文件: append() 方法可以向 FormData 对象中添加文件。例如:
const fd = new FormData();
fd.append('image', file);
// 添加普通数据: append() 方法也可以向 FormData 对象中添加普通数据(字符串、数字等)。例如:
const fd = new FormData();
fd.append('username', 'admin');
fd.append('password', 'password');
// 追加多个值: 如果同一个键名需要追加多个值,可以使用 append() 方法多次追加。例如:
const fd = new FormData();
fd.append('hobby', 'basketball');
fd.append('hobby', 'football');
// 在发送表单数据时,FormData 对象会自动将键值对转换为合适的格式,以便在网络请求中传输。
// 需要注意的是,append() 方法会覆盖之前同名键的值。如果需要追加多个值,请使用多次 append() 方法。
收集表单并上传接口 (发布文章的收集并保存功能)
- 目标:收集文章内容,并提交保存
- 效果:前期已经将频道、封面、富文本编辑器集成到发布文章页面,点击发布按钮,收集整个表单用户填写的信息,然后提交到服务器,完成保存
- 步骤分析:
- 监测点击按钮,基于 form-serialize 插件收集表单数据对象
- 基于 axios 提交到服务器保存
- 调用 Alert 警告框反馈结果给用户
- 重置表单并跳转到列表页
<div class="body">
<form class="art-form">
<input type="text" name="id" hidden>
<div>
<label for="title" class="form-label">标题:</label>
<input type="text" class="form-control" id="title" name="title">
</div>
<div>
<label for="channel_id" class="form-label">频道:</label>
<select class="form-select" id="channel_id" name="channel_id">
<option value="" selected>请选择文章频道</option>
<option value="1">频道1</option>
<option value="2">频道2</option>
<option value="3">频道3</option>
</select>
</div>
<div class="cover">
<label for="img">封面:</label>
<label for="img" class="place">+</label>
<input class="img-file" type="file" name="img" id="img" hidden>
<img class="rounded">
</div>
<div>
<label for="">内容:</label>
<!-- 富文本编辑器位置 -->
<div id="editor—wrapper">
<div id="toolbar-container"><!-- 工具栏 --></div>
<div id="editor-container"><!-- 编辑器 --></div>
</div>
<!-- 记录富文本内容-用于表单收集 -->
<textarea name="content" class="publish-content" hidden></textarea>
</div>
<div>
<button type="button" class="btn btn-primary send">发布</button>
</div>
</form>
</div>
/**
* 目标3:发布文章保存
* 3.1 基于 form-serialize 插件收集表单数据对象
* 3.2 基于 axios 提交到服务器保存
* 3.3 调用 Alert 警告框反馈结果给用户
* 3.4 重置表单并跳转到列表页
*/
// 3.1 基于 form-serialize 插件收集表单数据对象
// 1获取按钮标签,绑定点击事件
document.querySelector('.send').addEventListener('click', async e => {
if (e.target.innerHTML !== '发布') return
// 2收集整个表单数据,4个表单元素,通过插件serialize函数,调用后,传入需要获取的表单对象
// 3先通过类名.art-form获取需要收集的表单标签对象
const form = document.querySelector('.art-form')
// 4定义一个常量data,来收集表单数据对象,借助插件serialize函数+配置对象
const data = serialize(form, { hash: true, empty: true })
// 6发布文章的时候,不需要 id 属性,所以可以删除掉(id 为了后续做编辑使用)
// 7删除一个对象里的属性,用 delete关键字 data对象.id属性
delete data.id
// 5打印确认收集到的数据对象
console.log(data)
// 8自己收集封面图片地址并保存到 data 对象中,先找到封面的url地址,是在img标签中的src属性中所以获取过来,在上一节的 document.querySelector('.rounded').src = imgUrl 中
// 9因为img不是表单元素,但是又需要收集到数据对象中,并上传接口保存,因此得加一个数据,看后端的接口要求,指定data.cover
// 10看 接口文档.md ,看后端的要求,一个cover的属性+嵌套对象
data.cover = {
type: 1, // 封面类型
images: [document.querySelector('.rounded').src] // 封面图片 URL 网址,数组类型的值,数组里面的元素就是文章封面的url
}
// 3.2 基于 axios 提交到服务器保存
// 17 通过try-catch来捕获async和await的错误情况
try {
// 14使用await等待请求返回的成功的接口对象,而且await是只等待成功返回的接口对象,如果是用户没写完整,返回的对象就是错误的
const res = await axios({
// 11基于axios提交到服务器
url: '/v1_0/mp/articles',
method: 'POST',
// 12不再需要写请求头参数,因为请求拦截器已经携带
// 13请求体参数data
data: data
})
// 15打印确认从服务器请求返回的接口对象
console.log(res)
// 16 3.3 调用 Alert 警告框反馈结果给用户,选择以true成功的样式调用,提示‘发布成功’
myAlert(true, '发布成功')
// 21 如果发布成功,需要重置表单 3.4 重置表单并跳转到列表页,找到成功的myAlert(true, '发布成功')这一句,后面添加
// 22 表单重置,先前已获取到表单对象const form = document.querySelector('.art-form'),使用内置的.reset()事件方法
form.reset()
// 24 封面需要手动重置,复制前面的那段改一下
// document.querySelector(".rounded").src = imgURL; src赋予空字符串,去掉url
// document.querySelector(".rounded").classList.add("show"); img标签的.show类名改为remove
// document.querySelector(".place").classList.add("hide"); +号标签的.hide隐藏类名也去掉,就显示回来了
document.querySelector('.rounded').src = ''
document.querySelector('.rounded').classList.remove('show')
document.querySelector('.place').classList.remove('hide')
// 25 富文本编辑器重置,引入的富文本编辑器插件,有全局的对象editor,通过调用内置的setHtml方法,可以参考wangEditer手册,重置
editor.setHtml('')
// 23 让警告框停留1.5秒后,再做跳转,到../content/index.html内容列表页面
setTimeout(() => {
location.href = '../content/index.html'
}, 1500)
// 18 接收失败的错误对象,通过小括号(error)来抓错误对象
} catch (error) {
// 19先打印这个错误对象的属性和值看看,找到需要提示的信息
console.dir(error)
// 20复制服务器返回的、需要提示的错误信息,做提示,调试中选中,右键,复制属性路径,选择以false失败的样式调用,提示失败信息
myAlert(false, error.response.data.message)
}
})
// form-serialize 是一个轻量级的 JavaScript 插件,用于将 HTML 表单序列化为字符串或对象。它提供了以下功能:
// 用法
// 要使用 form-serialize,请遵循以下步骤:
// 安装插件:
npm install form-serialize
// 在你的 HTML 页面中引入插件:
<script src="path/to/form-serialize.js"></script>
// 序列化表单:
const form = document.querySelector("form");
const data = serialize(form, { hash: true, empty: true });
// 选项含义:
// serialize 函数接受一个可选的选项对象,该对象可以自定义序列化行为:
// hash (默认: false):将数据序列化为哈希表,其中键是字段名称,值为字段值。
// array (默认: false):将具有相同名称的多个字段值序列化为数组。
// empty (默认: false):包括空字段值。
// booleans (默认: true):将复选框和单选按钮的值序列化为布尔值(true/false)。
// 示例
// 示例的html部分
{/* <form id="my-form">
<input type="text" name="name" value="John Doe">
<input type="email" name="email" value="john.doe@example.com">
<input type="checkbox" name="remember-me" checked>
</form> */}
// 示例的js部分
const form = document.querySelector("#my-form");
const data = serialize(form, { hash: true, empty: true });
console.log(data); // 输出:{ name: "John Doe", email: "john.doe@example.com", "remember-me": true }
// 返回结果
// serialize 函数返回一个字符串或对象,具体取决于提供的选项:
// 如果 hash 选项为 true,则返回一个哈希表。
// 否则,返回一个查询字符串。
接口查询、分析返回数据、取出返回数据(内容管理-文章列表展示)
- 目标:获取文章列表并展示
- 效果:类似与禅道列表,或者官网后台列表,本节做展示,后续做筛选和分页
- 步骤: 0. 先看接口文档,看看有哪些查询参数,先全面完整囊括这些参数,都写好传给后端
- 准备查询参数对象,定义一个新对象,将向后台查询的参数都放到这个对象
- 获取文章列表数据,传递这个查询参数的集合,即这个新对象
- 展示到指定的标签结构中,打开调试或者对应页面html,看传输的数据与后台的对应关系
- 确认这个查询和展示的逻辑,需要复用,定义一个新函数
- 服务器返回的res.data.results数据对象需要映射成什么结构呢?tbody文章列表 + tr每一篇文章的信息,所以需要遍历数组,把每个对象信息映射成一个tr结构,将数据放进去
/**
* 目标1:获取文章列表并展示
* 1.1 准备查询参数对象
* 1.2 获取文章列表数据
* 1.3 展示到指定的标签结构中
*/
const queryObj = {
status: "",
channel_id: "",
page: 1,
per_page: 2,
};
let totalCount = 0;
async function setArtileList() {
const res = await axios({
url: "/v1_0/mp/articles",
params: queryObj,
});
const htmlStr = res.data.results
.map(
(item) =>
`<tr>
<td>
<img
src=${
// 封面,需要判断有没有封面,没有封面给默认值
// 条件表达式,当item.cover.type === 0时,接口为0是无封面,放默认封面
item.cover.type === 0
? "https://img2.baidu.com/it/u=2640406343,1419332367&fm=253&fmt=auto&app=138&f=JPEG?w=708&h=500"
// type不为0,则有值,取item.cover.images中的第1张,也就是数组[0]
: item.cover.images[0]
};
alt="">
</td>
<td>${item.title}</td>
<td>
${
item.status === 1
? `<span class="badge text-bg-primary">待审核</span>`
: `<span class="badge text-bg-success">审核通过</span>`
}
</td>
<td>
<span>${item.pubdate}</span>
</td>
<td>
<span>${item.read_count}</span>
</td>
<td>
<span>${item.comment_count}</span>
</td>
<td>
<span>${item.like_count}</span>
</td>
<td data-id="${item.id}">
<i class="bi bi-pencil-square edit"></i>
<i class="bi bi-trash3 del"></i>
</td>
</tr>`
)
// 拼接字符串
.join("");
// 插入tbody
document.querySelector(".art-list").innerHTML = htmlStr;
totalCount = res.data.total_count;
document.querySelector(".total-count").innerHTML = `共${totalCount}条`;
}
setArtileList();
//params 参数用于指定要附加到请求 URL 中查询字符串的查询参数对象。这些查询参数是作为键值对发送的,其中键是查询参数的名称,而值是查询参数的值。
// params 对象中的每个键值对都将被 URI 编码并附加到请求 URL 的查询字符串中,使用问号 (?) 将其与 URL 路径分隔开。查询参数之间的多个键值对使用与号 (&) 分隔。
// 示例:使用 params 参数发送查询参数
axios.get('/api/users', {
params: {
limit: 10,
offset: 20
}
});
// 上面示例的请求将发送到以下 URL:
/api/users?limit=10&offset=20
// params 参数通常用于向服务器传递过滤、排序或分页等查询参数。它是一种将查询参数附加到请求 URL 的简单且方便的方法。
// 注意:
// 1 params 参数中的键和值都会被 URI 编码。
// 2 如果 URL 中已经存在查询参数,则 params 参数中的查询参数将附加到现有查询参数的末尾。
// 3 params 参数不能用于发送请求正文或标头。
// 条件表达式(又称三元运算符)是一种在 JavaScript 中用于根据条件计算不同值的简洁方法。它的语法如下:
condition ? value_if_true : value_if_false;
// 其中:
// condition 是一个布尔表达式,它决定了计算哪个值。
// value_if_true 是如果 condition 为 true 时要计算的值。
// value_if_false 是如果 condition 为 false 时要计算的值。
// 示例:
let age = 18;
let message = age >= 18 ? "成年人" : "未成年人";
console.log(message); // 输出:成年人
// 在上面的示例中,如果 age 大于或等于 18(即 condition 为 true),则 message 变量将被分配值为 "成年人"。否则,message 变量将被分配值为 "未成年人"。
// 条件表达式的优点:
// 简洁:条件表达式提供了一种简洁的方法来根据条件计算不同的值,而无需使用 if-else 语句。
// 可读性:条件表达式通常比 if-else 语句更易于阅读和理解。
// 可扩展性:条件表达式可以链接在一起以创建更复杂的分支逻辑。
// 注意:
// 条件表达式只能计算单个值。如果需要计算多个值,则需要使用 if-else 语句或其他控制流结构。
// 条件表达式中的布尔表达式必须求值为 true 或 false。
// 条件表达式不能包含分号 (;)。
接口按单条件或多条件组合查询(内容管理-文章筛选)
目标:根据筛选条件,获取匹配数据展示
目标包括:频道条件、文章状态,或两个条件结合
步骤:
- 设置频道列表数据,供用户选择筛选(复用发布文章中选择频道的函数)
- 监听筛选条件改变(单选框及下拉菜单option选中选项值),保存查询信息到查询参数对象中
- 点击筛选时,传递查询参数对象到服务器
- 获取匹配数据,覆盖到页面展示
要点:
- 带查询条件的查询请求,标准做法是,不但要监听筛选条件改变(单选框及下拉菜单option选中选项值),还需要保存查询信息到独立的、统一的、唯一的查询参数对象中管理
- 当用户选择的条件改变,就会影响查询参数对象中的值同步改变,只需要再调用一次获取设置频道列表的函数,就会带着新的条件对象去查
- 到html页面中,确认绑定好的筛选条件的value值,分别绑定change事件,获取到选中的单选框,作为条件绑定到查询参数对象
/**
* 目标2:筛选文章列表
* 2.1 设置频道列表数据
* 2.2 监听筛选条件改变,保存查询信息到查询参数对象
* 2.3 点击筛选时,传递查询参数对象到服务器
* 2.4 获取匹配数据,覆盖到页面展示
*/
// 2.1 设置频道列表数据
async function setChannleList() {
const res = await axios({
url: '/v1_0/channels'
})
const htmlStr = `<option value="" selected="">请选择文章频道</option>` + res.data.channels.map(item => `<option value="${item.id}">${item.name}</option>`).join('')
document.querySelector('.form-select').innerHTML = htmlStr
}
setChannleList()
// 2.2 监听筛选条件改变,保存查询信息到查询参数对象
// 筛选状态标记数字->change事件->绑定到查询参数对象上
// querySelectorAll获取多个单选框,传入类名.form-check-input,因为获取到的是伪数组,forEach遍历伪数组,遍历每一个单选框
// 给定的代码使用 forEach() 方法遍历所有具有类名 .form-check-input 的元素,这些元素通常是单选按钮输入。
// 对于每个单选按钮,它添加一个 change 事件监听器,该监听器在单选按钮的值更改时触发。
// 当单选按钮的值更改时,事件处理函数将 queryObj.status 属性设置为该目标单选按钮的 value 属性。
// 这意味着当用户选择一个单选按钮时,queryObj.status 属性将更新为所选单选按钮的值。
// 以下是代码的简要说明:
// document.querySelectorAll('.form-check-input'):选择所有具有类名 .form-check-input 的元素,这些元素通常是单选按钮输入。
// forEach(radio => { ... }):使用 forEach() 方法遍历所选元素的集合,其中 radio 是集合中的每个元素。
// radio.addEventListener('change', e => { ... }):为每个单选按钮添加一个 change 事件监听器,该监听器在单选按钮的值更改时触发。
// e.target.value:获取触发事件的单选按钮的 value 属性。
// queryObj.status = e.target.value:将 queryObj.status 属性设置为所选单选按钮的 value 属性。
// 总之,这段代码用于在单选按钮的值更改时更新 queryObj.status 属性,该属性可能是用于过滤或搜索数据的查询对象的一部分。
document.querySelectorAll('.form-check-input').forEach(radio => {
radio.addEventListener('change', e => {
// e.target.value,即事件对象.target.value值属性,拿到后,绑定回去查询条件
console.log(e.target.value)//可以打印确认筛选效果
queryObj.status = e.target.value
})
})
// 筛选频道 id -> change事件 -> 绑定到查询参数对象上
document.querySelector('.form-select').addEventListener('change', e => {
queryObj.channel_id = e.target.value
})
// 2.3 点击筛选时,传递查询参数对象到服务器
document.querySelector('.sel-btn').addEventListener('click', () => {
// 2.4 获取匹配数据,覆盖到页面展示,调用一下函数就可以了
setArtileList()
})
// 伪数组遍历是指使用标准数组方法(如 forEach(), map(), filter(), reduce(), some(), every(), 等)遍历类数组对象的过程。
// 类数组对象是具有长度属性和按索引访问元素的对象,但不是真正的数组。例如,NodeList、HTMLCollection、Arguments 对象都是伪数组。
// 遍历伪数组的方法:
// 1. 使用 Array.from() 创建一个真正的数组:
const pseudoArray = document.querySelectorAll('li');
const realArray = Array.from(pseudoArray);
realArray.forEach((item) => {
// ...
});
// 2. 使用扩展运算符(...)将伪数组转换为真正的数组:
const pseudoArray = document.querySelectorAll('li');
const realArray = [...pseudoArray];
realArray.forEach((item) => {
// ...
});
// 3. 使用 forEach() 方法直接遍历伪数组:
const pseudoArray = document.querySelectorAll('li');
const realArray = [...pseudoArray];
realArray.forEach((item) => {
// ...
});
// 注意:
// 直接使用 forEach() 方法遍历伪数组时,无法使用 break 或 continue 语句。
// 如果需要使用 break 或 continue 语句,请使用上述方法之一将伪数组转换为真正的数组。
// 优点:
// 方便:伪数组遍历使用标准数组方法,非常方便且易于使用。
// 可读性:伪数组遍历的代码通常比使用 for 循环更易于阅读和理解。
// 可扩展性:伪数组遍历可以与其他数组方法(如 filter(), map(), reduce(), 等)结合使用,以创建更复杂的数据转换。
// addEventListener() 方法用于向元素添加事件监听器,以便在发生特定事件时执行指定的函数。它有以下用法:
// 1. 添加单个事件监听器:
element.addEventListener('event', function);
// 例如:
button.addEventListener('click', () => {
console.log('Button clicked');
});
// 2. 添加多个事件监听器:
element.addEventListener('event1', function1);
element.addEventListener('event2', function2);
// 例如:
button.addEventListener('click', () => {
console.log('Button clicked');
});
button.addEventListener('mouseover', () => {
console.log('Button hovered');
});
// 3. 添加事件监听器并指定选项对象:
element.addEventListener('event', function, options);
// options 对象可以包含以下属性:
// capture: 布尔值,指示是否在捕获阶段还是冒泡阶段触发事件处理函数。
// once: 布尔值,指示事件处理函数是否只触发一次。
// passive: 布尔值,指示事件处理函数是否可以取消默认操作(如滚动)。
// 例如:
button.addEventListener('click', () => {
console.log('Button clicked');
}, {
capture: true,
once: true,
passive: true
});
// 4. 移除事件监听器:
element.removeEventListener('event', function);
// 例如:
button.removeEventListener('click', () => {
console.log('Button clicked');
});
// 注意:
// 事件处理函数可以是匿名函数或具名函数。
// 同一个事件可以有多个事件监听器。
// 事件监听器在元素被移除或销毁后将自动移除。
// addEventListener() 方法是向元素添加事件处理程序的最常用方法,因为它提供了在各种事件上处理事件的灵活性和控制权。
// =======================================
// 附:
// JavaScript 中可以被监听的事件有很多,包括以下一些最常见的事件:
// 点击事件: click
// 鼠标悬停事件: mouseover 和 mouseout
// 键盘事件: keydown, keyup 和 keypress
// 表单事件: submit, change 和 input
// 窗口事件: load, unload 和 resize
// 鼠标移动事件: mousemove 和 mousedown
// 滚动事件: scroll
// 拖放事件: drag 和 drop
// 媒体事件: play, pause 和 ended
// 动画事件: animationstart 和 animationend
// 过渡事件: transitionstart 和 transitionend
// 错误事件: error
// 自定义事件: 使用 CustomEvent 构造函数创建的事件
// 此外,不同的浏览器还支持其他特定于浏览器的事件。例如,Chrome 浏览器支持 pointerdown 和 pointerup 事件,而 Firefox 浏览器支持 deviceorientation 和 devicemotion 事件。
// 可以监听的事件类型取决于所使用的元素类型和浏览器。有关特定元素支持的事件的完整列表,请参阅相应元素的文档。
对接口获取到的数据作处理并展示(内容管理-分页功能)
- 目标:完成文章列表,分页管理功能,是一个常见业务
- 步骤:
- 保存并设置文章总条数,对接口获取回来的总条数保存和展示
- 点击下一页,做临界值判断,并切换页码参数请求最新数据(向上取整,末页不够一页也要占一页)
- 点击上一页,做临界值判断,并切换页码参数请求最新数据
- 记得第一步要到上面,定义一个变量,保存文章总条数let totalCount = 0 默认值0
let totalCount = 0
//保存并设置文章总条数
totalCount = res.data.total_count
document.querySelector(".total-count").innerHTML = `共${totalCount}条`;
/**
* 目标3:分页功能
* 3.1 保存并设置文章总条数
* 3.2 点击下一页,做临界值判断,并切换页码参数并请求最新数据
* 3.3 点击上一页,做临界值判断,并切换页码参数并请求最新数据
*/
// 3.2 点击下一页,做临界值判断,并切换页码参数并请求最新数据
document.querySelector('.next').addEventListener('click', e => {
// 当前页码小于最大页码数
// 当当前页码小于(总数除以每页),mathceil向上取整,注意,除法优先括号括起来
if (queryObj.page < Math.ceil(totalCount / queryObj.per_page)) {
// 页码数字在click的时候自增
queryObj.page++
// 并将当前页数值,赋值回去给标签
document.querySelector('.page-now').innerHTML = `第 ${queryObj.page} 页`
// 再次调用
setArtileList()
}
})
// 3.3 点击上一页,做临界值判断,并切换页码参数并请求最新数据
document.querySelector('.last').addEventListener('click', e => {
// 大于 1 的时候,才能翻到上一页
if (queryObj.page > 1) {
queryObj.page--
document.querySelector('.page-now').innerHTML = `第 ${queryObj.page} 页`
setArtileList()
}
})
// Math 对象 是 JavaScript 中一个内置对象,它提供了许多与数学相关的函数和属性。
// 语法:
Math.functionName(arguments);
// functionName 是要调用的函数的名称。
// arguments 是传递给函数的参数。
// 常用函数:
// Math.abs(): 返回一个数的绝对值。
// Math.acos(): 返回一个数的反余弦值。
// Math.asin(): 返回一个数的反正弦值。
// Math.atan(): 返回一个数的反正切值。
// Math.atan2(): 返回给定 x 和 y 坐标的反正切值。
// Math.ceil(): 返回一个数向上取整后的值。
// Math.cos(): 返回一个数的余弦值。
// Math.exp(): 返回 e 的给定幂。
// Math.floor(): 返回一个数向下取整后的值。
// Math.log(): 返回一个数的自然对数。
// Math.max(): 返回一组数字中的最大值。
// Math.min(): 返回一组数字中的最小值。
// Math.pow(): 返回一个数的给定幂。
// Math.random(): 返回一个 0 到 1 之间的随机数。
// Math.round(): 返回一个数四舍五入后的值。
// Math.sin(): 返回一个数的正弦值。
// Math.sqrt(): 返回一个数的平方根。
// Math.tan(): 返回一个数的正切值。
// 常用属性:
// Math.E: 自然对数的底数 e(约为 2.718)。
// Math.LN10: 10 的自然对数(约为 2.303)。
// Math.LN2: 2 的自然对数(约为 0.693)。
// Math.LOG10E: e 的以 10 为底的对数(约为 0.434)。
// Math.LOG2E: e 的以 2 为底的对数(约为 1.443)。
// Math.PI: 圆周率(约为 3.141)。
// Math.SQRT1_2: 平方根 2 的倒数(约为 0.707)。
// Math.SQRT2: 平方根 2(约为 1.414)。
// 示例:
// 计算绝对值
const absoluteValue = Math.abs(-5); // 5
// 计算余弦值
const cosineValue = Math.cos(Math.PI / 2); // 0
// 计算随机数
const randomNumber = Math.random(); // 0 到 1 之间的随机数
操作删除接口(内容管理-删除功能)
- 目标:完成删除文章功能
- 步骤:
- 关联文章 id 到删除图标,先在content内容页下index.js中的获取并展示部分,找到删除按钮,并在td中插入对应的${id}关联,让获取的时候就让按钮带上对应的文章id
- 点击删除时,获取文章 id
- 调用删除接口,传递文章 id 到服务器
- 重新获取文章列表,并覆盖展示
<td data-id="${item.id}">
<i class="bi bi-pencil-square edit"></i>
<i class="bi bi-trash3 del"></i>
</td>
复习addEventListener('click', async e => {})中,e => {}是指事件目标源e.target 在下面这里,e.target是指的触发点击事件的对应具体的标签
/**
* 目标4:删除功能
* 4.1 关联文章 id 到删除图标
* 4.2 点击删除时,获取文章 id
* 4.3 调用删除接口,传递文章 id 到服务器
* 4.4 重新获取文章列表,并覆盖展示
* 4.5 删除最后一页的最后一条,需要自动向前翻页
*/
// 4.2 点击删除时,获取文章 id
document.querySelector('.art-list').addEventListener('click', async e => {
// 判断点击的是删除元素
// 如果触发点击事件的目标标签,类名classList中包含contains('del')
if (e.target.classList.contains('del')) {
// 如果触发事件的parentNode父级,在该html元素上的数据中的id属性,拿出来赋值给delId作为删除id
const delId = e.target.parentNode.dataset.id
// 可以打印确认
console.log(delId);
// 4.3 调用删除接口,传递文章 id 到服务器
// 并通过res接收服务器返回的结果
const res = await axios({
url: `/v1_0/mp/articles/${delId}`,
method: 'DELETE'
})
// 4.5 删除最后一页的最后一条,需要自动向前翻页
const children = document.querySelector('.art-list').children
if (children.length === 1 && queryObj.page !== 1) {
queryObj.page--
document.querySelector('.page-now').innerHTML = `第 ${queryObj.page} 页`
}
// 4.4 重新获取文章列表,并覆盖展示
// setArtileList()
// 可以自己加一个提示
myAlert(true, "删除成功");
setTimeout(() => {
setArtileList();
}, 1500);
}
})
// dataset 是 HTML 元素上一个特殊的数据属性,它允许您以编程方式存储和检索自定义数据,而无需修改元素本身的 HTML。它是一个DOMStringMap 对象,可以存储键值对。
// 用法:
// 要设置一个数据属性,可以使用以下语法:
element.dataset.propertyName = "value";
// 例如:
const myElement = document.getElementById("my-element");
myElement.dataset.name = "John Doe";
// 要获取数据属性,可以使用以下语法:
element.dataset.propertyName;
// 例如:
const name = myElement.dataset.name; // "John Doe"
// 优点:使用 dataset 的优点包括:
// 易于使用: 无需修改元素的 HTML,即可轻松存储和检索数据。
// 自定义数据: 您可以存储任何类型的数据,包括字符串、数字、对象和数组。
// 与 CSS 集成: 您可以使用数据属性值来动态设置 CSS 样式。
// 需要注意的是:
// dataset 仅适用于 HTML 元素,不适用于 SVG 元素。
// dataset 属性是只读的,因此无法直接修改它。您必须使用 element.dataset.propertyName = "value" 语法来设置值。
// dataset 属性不会被序列化为 JSON,因此它们不会保存在服务器端。
前端页面bug fix(内容管理-删除最后一条)
- 目标:在删除最后一页,最后一条时有 Bug
- 解决:
- 删除成功&列表刷新前时,判断 DOM 元素只剩一条,让当前页码 page--
- 注意,当前页码为 1时不能继续向前翻页
- 重新设置页码数,获取最新列表展示
- 重点: 分析代码逻辑,找到问题产生的原因;如此案例的bug,是由于删除接口调用后,就直接调用setArtileList()重新获取文章列表的函数了,此时,由于点击删除按钮前,当前的停留页面是3,点击删除后,queryObj中的当前页面配置还是3,没有处理 遇到问题多想原因,分析代码逻辑,代入执行去思考
// 获取.art-list这个类名所属的tbody标签下的children也就是tr标签
const children = document.querySelector('.art-list').children
// 如果伪数组长度为1,也就是只有一个了,且不为第一页
// 也就是,当你删除的这个tr是页面中仅有的仅剩的一个tr时
if (children.length === 1 && queryObj.page !== 1) {
queryObj.page--
document.querySelector('.page-now').innerHTML = `第 ${queryObj.page} 页`
}
接口跳转 (内容管理-编辑文章-回显)
- 目标:编辑文章时,回显数据到表单,点击编辑按钮,跳转到修改文章页面
- 细分目标:
- 编辑文章与发布文章共用一套表单,方便后续产品修改,不需要维护两套表单
- 实际上,编辑文章=发布文章+获取文章ID并作内容回显
- 获取查询参数的方法:截取查询参数字符串,创建URLSearchParams构造函数,得出查询参数对象,格式化成键值对结构,
- 通过内置foreach方法,遍历每一对的参数值和参数名
- 获取并回显的代码,先不管要不要复用,哪怕不复用,为了避免定义的变量不污染全局作用域,都应该写函数,此处写了一个自调用函数/立即执行函数
- 创建一个对象,看看哪些数据需要回显就写进去,不需要回显的数据就不用了
- 遍历这个新对象,遍历对象属性,作为类名,或者name属性的名字,找到语句关联的表单标签,再回传
- 封面不是表单元素,不能直接赋值;内容得用editor.setHtml,编辑器.sethtml
- 步骤:
- 页面跳转传参(URL 查询参数方式)
- 发布文章页面接收参数判断(共用同一套表单)
- 修改标题和按钮文字
- 获取文章详情数据并回显表单
- 跳转总结:事件委托 + 获取ID + url查询参数 + 跳转传参(跨页面传参的一种解决方案)
- 编辑总结:
- URLSearchParams把查询参数格式化成查询参数对象 + 遍历参数对象中的每一组名字和值并作判断 + 当有ID,需要走编辑逻辑
- 编辑逻辑:修改标题,获取文章数据;筛选需要回显的数据,取出数据组织一个新的对象
- 遍历新的对象的属性,观察属性和标签的关系,用对象里面的属性名,作为name属性,来获取对应的标签快速赋值,如果特殊的情况单独判断赋值
// 在内容管理页下浏览,并点击内容管理页中的编辑按钮
// 先获取该按钮,带上对应文章id,并绑定点击事件,点击时触发跳转
document.querySelector(".art-list").addEventListener("click", (e) => {
// e.target事件对象.target是目标源,若源身上的classList类名集合中包括edit(非css选择器,不要加.)
if (e.target.classList.contains("edit")) {
// 获取文章ID,在父级节点上的自定义属性的id值
const artId = e.target.parentNode.dataset.id;
// 跳转页面,..回到父级,并传递ID
location.href = `../publish/index.html?id=${artId}`;
}
});
总结:事件委托 + 获取ID + url查询参数 + 跳转传参(跨页面传参的一种解决方案)
/**
* 目标4:编辑-回显文章
* 4.1 页面跳转传参(URL 查询参数方式)
* 4.2 发布文章页面接收参数判断(共用同一套表单)
* 4.3 修改标题和按钮文字
* 4.4 获取文章详情数据并回显表单
*/
// 自调用函数/立即执行函数,通过location.search属性,拿到url中的查询参数字符串
; (function () {
// 4.2 发布文章页面接收参数判断(共用同一套表单)
// console.log(location.search);
const paramsStr = location.search
const params = new URLSearchParams(paramsStr)
params.forEach(async (value, key) => {
// 打印转换后的结果
console.log(value, key);
// 当前有要编辑的文章 id 被传入过来
if (key === 'id') {
// 4.3 修改标题和按钮文字
document.querySelector('.title span').innerHTML = '修改文章'
document.querySelector('.send').innerHTML = '修改'
// 4.4 获取文章详情数据并回显表单
const res = await axios({
url: `/v1_0/mp/articles/${value}`
})
console.log(res)
// 组织我仅仅需要的数据对象,为后续遍历回显到页面上做铺垫
const dataObj = {
channel_id: res.data.channel_id,
title: res.data.title,
rounded: res.data.cover.images[0], // 封面图片地址,取出数组里面的地址
content: res.data.content,
id: res.data.id //当前正在编辑的文章ID是隐藏的也需要
}
// 遍历数据对象属性,映射到页面元素上,快速赋值
// Object.keys(dataObj)取出对象里的所有属性,形成一个数组,并foreach遍历这个数组的每个key
Object.keys(dataObj).forEach(key => {
// 当遍历到'rounded'时
if (key === 'rounded') {
// 封面设置
if (dataObj[key]) {
// 有封面
document.querySelector('.rounded').src = dataObj[key]
document.querySelector('.rounded').classList.add('show')
document.querySelector('.place').classList.add('hide')
}
} else if (key === 'content') {
// 富文本内容
editor.setHtml(dataObj[key])
} else {
// 用数据对象属性名,作为标签 name 属性选择器值来找到匹配的标签
document.querySelector(`[name=${key}]`).value = dataObj[key]
}
})
}
})
})();
总结:
- URLSearchParams把查询参数格式化成查询参数对象 + 遍历参数对象中的每一组名字和值并作判断 + 当有ID,需要走编辑逻辑
- 编辑逻辑:修改标题,获取文章数据;筛选需要回显的数据,取出数据组织一个新的对象
- 遍历新的对象的属性,观察属性和标签的关系,用对象里面的属性名,作为name属性,来获取对应的标签快速赋值,如果特殊的情况单独判断赋值
根据业务的改变选择不同的接口(内容管理-编辑文章-保存)
- 目标:确认修改,保存文章到服务器;
- 要点:
- 注意,表单是同一套,但接口不一样;编辑与发布不一样,实际上编辑和发布是两个不同的接口
- 修改页和发布页是同一套表单,因此都写到同一个js中
- 在同一个js中,点击一个按钮,触发两个事件函数,因此需要在不同的函数前加判断
- 即在发布函数中加if (e.target.innerHTML != "发布") return;如果发现按钮不是发布按钮就返回不要往下走发布逻辑;
- 以及在编辑函数中加if (e.target.innerHTML != "修改") return;不是修改就返回不要走下面修改逻辑
- 步骤:
- 同一个按钮,在点击事件上,通过判断按钮文字,区分业务(因为共用一套表单);或者通过ID的有无来判断也是可以的
- 调用编辑文章接口,保存信息到服务器
- 基于 Alert 反馈结果消息给用户
/**
* 目标5:编辑-保存文章
* 5.1 判断按钮文字,区分业务(因为共用一套表单)
* 5.2 调用编辑文章接口,保存信息到服务器
* 5.3 基于 Alert 反馈结果消息给用户
*/
document.querySelector('.send').addEventListener('click', async e => {
// 5.1 判断按钮文字,区分业务(因为共用一套表单)
if (e.target.innerHTML !== '修改') return
// 修改文章逻辑
const form = document.querySelector('.art-form')
const data = serialize(form, { hash: true, empty: true })
// 可以打印一下修改后的数据
console.log(data);
// 5.2 调用编辑文章接口,保存信息到服务器
try {
const res = await axios({
url: `/v1_0/mp/articles/${data.id}`,
method: 'PUT',
data: {
// 组织一个新对象,并解构出先前获取的对象,这样此前的4对key value就融入到新对象
...data,
// 加一个cover属性,对应是一个对象
cover: {
// 前端判断有没有封面图片,有值就是有封面走1,通过条件表达式,如果undefined就走0
type: document.querySelector('.rounded').src ? 1 : 0,
images: [document.querySelector('.rounded').src]
}
}
})
console.log(res)
myAlert(true, '修改文章成功')
} catch (error) {
myAlert(false, error.response.data.message)
}
})
token注销(退出登录)
- 目标:点击退出按钮时,完成退出登录效果
- 要点:清空缓存+页面跳转
- 步骤:
- 绑定点击事件
- 清空本地缓存,跳转到登录页面
/**
* 目标3:退出登录
* 3.1 绑定点击事件
* 3.2 清空本地缓存,跳转到登录页面
*/
// 按钮绑定点击事件,监测用户点击动作
document.querySelector(".quit").addEventListener("click", (e) => {
// 清空本地缓存(清空token)
localStorage.clear();
myAlert(true, "退出登录成功");
// 跳转
setTimeout(() => {
location.href = "../login/index.html";
}, 1500);
});
// localStorage 是 JavaScript 中一个全局对象,它允许你在浏览器中存储键值对数据。数据会一直保留在浏览器中,即使关闭浏览器窗口或选项卡后也不会丢失。
// localStorage 包括以下特性:
// 存储类型: 只能存储字符串。
// 存储大小: 大小限制因浏览器而异,但通常为几兆字节。
// 作用域: 仅限于当前域和协议。
// 过期时间: 数据不会过期,除非手动删除或浏览器清除数据。
// 使用方法:
// 设置值:
localStorage.setItem("key", "value")
// 获取值:
localStorage.getItem("key")
// 删除值:
localStorage.removeItem("key")
// 清除所有值:
localStorage.clear()
示例:
// 设置一个键值对
localStorage.setItem("name", "John Doe");
// 获取一个值
const name = localStorage.getItem("name");
// 删除一个键值对
localStorage.removeItem("name");
// 清除所有键值对
localStorage.clear();
// 注意:
// localStorage 中的数据是字符串类型的,因此在存储其他类型的数据(如数字或对象)之前需要将其转换为字符串。
// localStorage 不支持事件监听器,因此无法检测到数据何时被更改。