黑马程序员前端AJAX入门到实战全套教程,包含学前端框架必会的(ajax+node.js+webpack+git),一套全覆盖
一、AJAX
什么是ajax?
AJAX 是异步的 JavaScript 和 XML (Asynchronous JavaScript And XML) 。简单点说,就是使用 XMLHttpRequest 对象与服务器通信。它可以使用 JSON,XML,HTML 和 text 文本等格式发送和接收数据。AJAX 最吸引人的就是它的“异步”特性,也就是说它可以在不重新刷新页面的情况下与服务器通信,交换数据,或更新页面。
概念: AJAX是浏览器与服务器进行数据通信,实现动态数据交互的技术
ajax的使用
-
先使用 axios [eek'sious] 库,与服务器进行数据通信
基于 XMLHttpRequest 封装、代码简单、月下载量在14 亿次
Vue、React 项目中都会用到 axios
-
再学习 XMLHttpRequest 对象的使用,了解 AJAX 底层原理
语法:
- 引入 axios.js 库: cdn.jsdelivr.net/npm/axios/d…
- 使用 axios 函数
axios({
// 传入配置对象
url: '目标资源地址'
// 再用 .then 回调函数接收结果,并做后续处理
}).then((result) => {
// 对服务器返回的数据做后续处理
})
例:
请求目标资源地址,拿到省份列表数据。显示到页面目标资源地址: hmajax.itheima.net/api/provinc…
<body>
<p class="p"></p>
<!-- 1. 引入 axios 库-->
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
// 2. 使用 axios 函数
axios({
url: 'http://hmajax.itheima.net/api/province'
}).then(result => {
// console.log(result)
document.querySelector('.p').innerHTML = result.data.list.join('<br>')
})
</script>
</body>
url
定义:统一资源定位符(英语: Uniform Resource Locator,缩写: URL,或称统一资源定位器、定位地址、URL地址[1) 俗称网页地址,简称网址,是因特网上标准的资源的地址 (Address),如同在网络上的门牌。它最初是由蒂姆·伯纳斯-李发明用来作为万维网的地址,现在它已经被万维网联盟编制为因特网标准RFC 1738。
例: www.baidu.com/index.html --> 网页资源
www.itheima.com/images/logo… --> 图片资源
hmaiax.itheima.net/api/provinc… --> 数据资源
概念:url就是统一资源定位符,简称网址,用于访问网络上的资源
协议:超文本传输协议,规定浏览器和服务器之间传输数据的格式
域名:标记服务器在互联网中方位
资源路径:标记资源在服务器下的具体位置
url 查询参数
作用:浏览器提供给服务器的额外信息,让服务器返回浏览器想要的数据
如何携带查询参数?使用 params 选项,携带参数名和值
<body>
<p class="p"></p>
<!-- 1. 引入 axios 库-->
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
axios({
url: 'http://hmajax.itheima.net/api/city',
params: {
pname: '福建省'
}
}).then(result => {
// console.log(result)
document.querySelector('.p').innerHTML = result.data.list.join('<br>')
})
</script>
</body>
网络请求时接口访问地址:hmajax.itheima.net/api/city?pn…
axios 请求配置
axios 是一个基于 promise 的 HTTP 库,简单的讲就是可以发送 get、post 等请求。
请求方法:对服务器资源,要执行的操作,常用的请求方法如下:
axios 请求配置
url:请求的URL网址
method:请求的方法,GET可以省略 (不区分大小写)
data:提交数据
axios({
url: '目标资源地址',
method: '请求方法',
data: {
参数名:值
}
]).then((result) => {
// 对服务器返回的数据做后续处理
});
例:点击按钮,通过axios提交用户和密码,完成注册
<body>
<button class="btn">注册用户</button>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
document.querySelector('.btn').addEventListener('click', () => {
axios({
url: 'http://hmajax.itheima.net/api/register',
method: 'POST',
data: {
username: 'itheima007',
password: '7654321'
}
})
})
</script>
</body>
axios 错误处理
语法:在 then 方法的后面,通过点语法调用 catch 方法,传入 回调函数 并定义 形参
axios({
// 请求选项
]).then(result => {
// 处理数据
}).catch(error => {
// 处理错误
})
例:注册案例,重复注册时通过弹框提示用户错误信息
<body>
<button class="btn">注册用户</button>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
/*
需求: 使用axios错误处理语法, 拿到报错信息, 弹框反馈给用户
*/
document.querySelector('.btn').addEventListener('click', () => {
axios({
url: 'http://hmajax.itheima.net/api/register',
method: 'post',
data: {
username: 'itheima007',
password: '7654321'
}
}).then(result => {
// 成功
console.log(result)
}).catch(error => {
// 失败
// 处理错误信息
console.log(error)
console.log(error.response.data.message)
alert(error.response.data.message)
})
})
</script>
</body>
HTTP 协议
请求报文
HTTP 协议:规定了浏览器发送及服务器返回内容的格式
请求报文:浏览器按照 HTTP 协议要求的格式,发送给服务器的内容(可以在控制台的网络面板查看报文)
响应报文
响应状态码:用来表明请求是否成功完成
1xx:信息、2xx:成功、3xx:重定向消息、4xx:客户端错误、5xx:服务器错误
接口文档
接口文档:由后端提供的描述接口的文章
接口:使用 AJAX 和服务器通讯时,使用的 URL,请求方法,以及参数
<body>
<button class="btn">注册用户</button>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
/* 根据接口文档来写网络请求 */
document.querySelector('.btn').addEventListener('click', () => {
axios({
url: 'http://hmajax.itheima.net/api/login',
method: 'post',
data: {
username: 'itheima007',
password: '7654321'
}
});
</script>
</body>
快速搜集表单元素
使用 form-serialize 插件,快速收集表单元素值,解决多个表单下,要挨个去获取的麻烦。
- 把插件引入到自己网页中
- 使用 serialize(数据,{获取数据结构,获取空值}) 函数
<body>
<form action="javascript:;" class="example-form">
<input type="text" name="username">
<br>
<input type="text" name="password">
<br>
<input type="button" class="btn" value="提交">
</form>
<!--
目标:在点击提交时,使用form-serialize插件,快速收集表单元素值
-->
<--! 1. 把插件引入到自己网页中 -->
<script src="./lib/form-serialize.js"></script>
<script>
document.querySelector('.btn').addEventListener('click', () => {
/**
* 2. 使用serialize函数,快速收集表单元素的值
* 参数1:要获取哪个表单的数据
* 表单元素设置name属性,值会作为对象的属性名
* 建议name属性的值,最好和接口文档参数名一致
* 参数2:配置对象
* hash 设置获取数据结构
* - true:JS对象(推荐)一般请求体里提交给服务器
* - false: 查询字符串
* empty 设置是否获取空值
* - true: 获取空值(推荐)数据结构和标签结构一致
* - false:不获取空值
*/
const form = document.querySelector('.example-form')
const data = serialize(form, { hash: true, empty: true }) //{username:'itheima007',password:'7654321'}
// const data = serialize(form, { hash: false, empty: true }) username=itheima007&password=7654321
// const data = serialize(form, { hash: true, empty: false })
console.log(data)
})
</script>
</body>
图书管理案例
图片上传
<body>
<!-- 文件选择元素 -->
<input type="file" class="upload">
<img src="" alt="" class="my-img">
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
/**
* 目标:图片上传,显示到网页上
* 1. 获取图片文件
* 2. 使用 FormData 携带图片文件
* 3. 提交到服务器,获取图片url网址使用
*/
// 文件选择元素->change改变事件
document.querySelector('.upload').addEventListener('change', e => {
// 1. 获取图片文件
console.log(e.target.files[0])
// 2. 使用 FormData 携带图片文件
const fd = new FormData()
fd.append('img', e.target.files[0])
// 3. 提交到服务器,获取图片url网址使用
axios({
url: 'http://hmajax.itheima.net/api/uploadimg',
method: 'POST',
data: fd
}).then(result => {
console.log(result)
// 取出图片url网址,用img标签加载显示
const imgUrl = result.data.data.url
document.querySelector('.my-img').src = imgUrl
})
})
</script>
</body>
AJAX原理 - XMLHttpRequest
定义:MLHttpRequest (XHR)对象用于与服务器交互。通过 XMLHttpRequest 可以在不刷新页面的情况下请求特定 URL,获取数据。这允许网页在不影响用户操作的情况下,更新页面的局部内容。 XMLHttpRequest 在AJAX 编程中被大量使用。
关系:axios 内部采用 XMLHttpRequest 与服务器交互
好处:掌握使用XHR与服务器进行数据交互,有更多与服务器数据通信方式,了解 axios 内部原理
使用:轻量化,就用 XHR,不需要轻量化就用 axios
步骤:
- 创建 XMLHttpRequest 对象
- 配置请求方法和请求url地址
- 监听 loadend 事件,接收响应结果
- 发起请求
例:
<body>
<p class="p"></p>
<script>
const xhr = new XMLHttpRequest()
xhr.open('GET', 'http://hmajax.itheima.net/api/province')
xhr.addEventListener('loadend', () => {
// console.log(xhr.response)
const data = JSON.parse(xhr.response)
document.querySelector('.p').innerHTML = data.list.join('<br>')
})
xhr.send()
</script>
</body>
数据查询 - 查询参数
定义:浏览器提供给服务器的额外信息,让服务器返回浏览器想要的数据
<body>
<p class="p"></p>
<script>
const xhr = new XMLHttpRequest()
xhr.open('GET', 'http://hmajax.itheima.net/api/city?pname=辽宁省')
xhr.addEventListener('loadend', () => {
// console.log(xhr.response)
const data = JSON.parse(xhr.response)
document.querySelector('.p').innerHTML = data.list.join('<br>')
})
xhr.send()
</script>
</body>
拼接多个查询参数
像上面一般一个参数的话直接写在open里没什么大问题,但如果传参过多则不易书写,此时可以借助 URLSearchParams 这个 api
// 1. 创建 URLSearchParams 对象
const paramsObj = new URLSearchParams({
参数名1: 值1,
参数名2: 值2,
})
// 2. 生成指定格式查询参数 字符串
const queryString = paramsObj.toString()
// 结果:参数名1=值1&参数名2=值2
例:
<body>
<p class="p"></p>
<script>
const qObj = {
pname: '辽宁省',
cname: '大连市'
}
// 1. 创建 URLSearchParams 对象
const paramsObj = new URLSearchParams(qObj)
// 2. 生成指定格式查询参数 字符串
const queryString = paramsObj.toString()
// 结果:参数名1=值1&参数名2=值2
const xhr = new XMLHttpRequest()
xhr.open('GET', `http://hmajax.itheima.net/api/area?${queryString}`)
xhr.addEventListener('loadend', () => {
// console.log(xhr.response)
const data = JSON.parse(xhr.response)
document.querySelector('.p').innerHTML = data.list.join('<br>')
})
xhr.send()
</script>
</body>
注意!因为规定 url 中只能出现英文、数字、以及一些特殊符号,不能出现中文,所以在于服务器沟通的过程中传递的网址会被浏览器进行 url 编码,服务器那边拿到网址则会进行 url 解码,拿到对应的中文
数据提交
<body>
<button class="reg-btn">注册用户</button>
<script>
/* 目标:使用xhr进行数据提交-完成注册功能 */
document.querySelector('.reg-btn').addEventListener('click', () => {
const xhr = new XMLHttpRequest()
xhr.open('POST', 'http://hmajax.itheima.net/api/register')
xhr.addEventListener('loadend', () => {
console.log(xhr.response)
})
// 设置请求头-告诉服务器内容类型(JSON字符串)
xhr.setRequestHeader('Content-Type', 'application/json')
// 准备提交的数据
const userObj = {
username: 'itheima007',
password: '7654321'
}
const userStr = JSON.stringify(userObj)
// 设置请求体,发起请求
xhr.send(userStr)
})
</script>
</body>
Promise
Promise 对象用于表示一个异步操作的最终完成(或失败)及其结果值
好处:1.逻辑更清晰 2. 了解 axios 函数内部运作机制 3. 能解决回调函数地狱问题
<body>
<script>
/**
* 目标:认识Promise状态
*/
// 1. 创建Promise对象(pending-待定状态)
const p = new Promise((resolve, reject) => {
// Promise对象创建时,这里的代码都会执行了
// 2. 执行异步代码
setTimeout(() => {
// resolve() => 'fulfilled状态-已兑现' => then()
resolve('模拟AJAX请求-成功结果')
// reject() => 'rejected状态-已拒绝' => catch()
reject(new Error('模拟AJAX请求-失败结果'))
}, 2000)
})
console.log(p)
// 3. 获取结果
p.then(result => {
console.log(result)
}).catch(error => {
console.log(error)
})
</script>
</body>
Promise + XHR
用一个获取省份列表的案例,将 promise + AJAX原理(XMLHttpRequest) 这两种融合在一起
<body>
<p class="my-p"></p>
<script>
/**
* 目标:使用Promise管理XHR请求省份列表
* 1. 创建Promise对象
* 2. 执行XHR异步代码,获取省份列表
* 3. 关联成功或失败函数,做后续处理
*/
// 1. 创建Promise对象
const p = new Promise((resolve, reject) => {
// 2. 执行XHR异步代码,获取省份列表
const xhr = new XMLHttpRequest()
xhr.open('GET', 'http://hmajax.itheima.net/api/province')
xhr.addEventListener('loadend', () => {
// xhr如何判断响应成功还是失败的?
// 2xx开头的都是成功响应状态码
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.response))
} else {
reject(new Error(xhr.response))
}
})
xhr.send()
})
// 3. 关联成功或失败函数,做后续处理
p.then(result => {
console.log(result)
document.querySelector('.my-p').innerHTML = result.list.join('<br>')
}).catch(error => {
// 错误对象要用console.dir详细打印
console.dir(error)
// 服务器返回错误提示消息,插入到p标签显示
document.querySelector('.my-p').innerHTML = error.message
})
</script>
</body>
封装 axios
get 查询数据
无参查询
<body>
<p class="my-p"></p>
<script>
/**
* 目标:封装_简易axios函数_获取省份列表
* 1. 定义myAxios函数,接收配置对象,返回Promise对象
* 2. 发起XHR请求,默认请求方法为GET
* 3. 调用成功/失败的处理程序
* 4. 使用myAxios函数,获取省份列表展示
*/
// 1. 定义myAxios函数,接收配置对象,返回Promise对象
function myAxios(config) {
return new Promise((resolve, reject) => {
// 2. 发起XHR请求,默认请求方法为GET
const xhr = new XMLHttpRequest()
xhr.open(config.method || 'GET', config.url)
xhr.addEventListener('loadend', () => {
// 3. 调用成功/失败的处理程序
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.response))
} else {
reject(new Error(xhr.response))
}
})
xhr.send()
})
}
// 4. 使用myAxios函数,获取省份列表展示
myAxios({
url: 'http://hmajax.itheima.net/api/province'
}).then(result => {
console.log(result)
document.querySelector('.my-p').innerHTML = result.list.join('<br>')
}).catch(error => {
console.log(error)
document.querySelector('.my-p').innerHTML = error.message
})
</script>
</body>
多参查询
<body>
<p class="my-p"></p>
<script>
/**
* 目标:封装_简易axios函数_获取地区列表
* 1. 判断有params选项,携带查询参数
* 2. 使用URLSearchParams转换,并携带到url上
* 3. 使用myAxios函数,获取地区列表
*/
function myAxios(config) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
// 1. 判断有params选项,携带查询参数
if (config.params) {
// 2. 使用URLSearchParams转换,并携带到url上
const paramsObj = new URLSearchParams(config.params)
const queryString = paramsObj.toString()
// 把查询参数字符串,拼接在url?后面
config.url += `?${queryString}`
}
xhr.open(config.method || 'GET', config.url)
xhr.addEventListener('loadend', () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.response))
} else {
reject(new Error(xhr.response))
}
})
xhr.send()
})
}
// 3. 使用myAxios函数,获取地区列表
myAxios({
url: 'http://hmajax.itheima.net/api/area',
params: {
pname: '辽宁省',
cname: '大连市'
}
}).then(result => {
console.log(result)
document.querySelector('.my-p').innerHTML = result.list.join('<br>')
})
</script>
</body>
post 请求数据(多参)
<body>
<button class="reg-btn">注册用户</button>
<script>
/**
* 目标:封装_简易axios函数_注册用户
* 1. 判断有data选项,携带请求体
* 2. 转换数据类型,在send中发送
* 3. 使用myAxios函数,完成注册用户
*/
function myAxios(config) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
if (config.params) {
const paramsObj = new URLSearchParams(config.params)
const queryString = paramsObj.toString()
config.url += `?${queryString}`
}
xhr.open(config.method || 'GET', config.url)
xhr.addEventListener('loadend', () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.response))
} else {
reject(new Error(xhr.response))
}
})
// 1. 判断有data选项,携带请求体
if (config.data) {
// 2. 转换数据类型,在send中发送
const jsonStr = JSON.stringify(config.data)
xhr.setRequestHeader('Content-Type', 'application/json')
xhr.send(jsonStr)
} else {
// 如果没有请求体数据,正常的发起请求
xhr.send()
}
})
}
document.querySelector('.reg-btn').addEventListener('click', () => {
// 3. 使用myAxios函数,完成注册用户
myAxios({
url: 'http://hmajax.itheima.net/api/register',
method: 'POST',
data: {
username: 'itheima999',
password: '666666'
}
}).then(result => {
console.log(result)
}).catch(error => {
console.dir(error)
})
})
</script>
</body>
同步代码和异步代码
同步代码:我们应该注意的是,实际上浏览器是按照我们书写代码的顺序一行一行地执行程序的。浏览器会等待代码的解析和工作,在上一行完成后才会执行下一行。这样做是很有必要的,因为每一行新的代码都是建立在前面代码的基础之上的。这也使得它成为一个同步程序; 异步代码:异步编程技术使你的程序可以在执行一个可能长期运行的任务的同时继续对其他事件做出反应而不必等待任务完成。与此同时,你的程序也将在任务完成后显示结果;
同步代码:逐行执行,需原地等待结果后,才继续向下执行;
异步代码:调用后耗时,不阻塞代码继续执行(不必原地等待),在将来完成后触发一个回调函数(异步代码依靠回调函数来接收结果)
js中有哪些异步代码?setTimeout、setInterval、时间、AJAX
回调函数地狱
Promise - 链式调用
概念:使用 then 函数返回新 Promise 对象特性,一直串联下去,直到结束
细节:then 回调函数中,return 返回值会传给 then 函数生成的新 Promise 对象
好处:通过链式调用,解决回调函数嵌套问题
<body>
<script>
/**
* 目标:掌握Promise的链式调用
* 需求:把省市的嵌套结构,改成链式调用的线性结构
*/
// 1. 创建Promise对象-模拟请求省份名字
const p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('北京市')
}, 2000)
})
// 2. 获取省份名字
const p2 = p.then(result => {
console.log(result) // 北京
// 3. 创建Promise对象-模拟请求城市名字
// return Promise对象最终状态和结果,影响到新的Promise对象
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(result)
}, 2000)
})
})
// 4. 获取城市名字
p2.then(result => {
console.log(result) // 北京(上面return返回值在这里 then 生成新 Promise 对象)
})
// then()原地的结果是一个新的Promise对象
console.log(p2 === p) // false
</script>
</body>
例:把上面的获取省市区回调地狱改成 Promise 链式调用
<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(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>
async 和 await
定义:async 函数是使用 async 关键字声明的函数。async 函数是 AsyncFunction 构造函数的实例,并且其中允许使用 await 关键字。async 和 await 关键字让我们可以用一种更简洁的方式写出基于 Promise 的异步行为,而无需刻意地链式调用 promise。
概念:在 async 函数内,使用 await 关键字取代 then 函数,等待获取 Promise 对象成功状态的结果值
错误捕获
try...catch 语句标记要尝试的语句块,并指定一个出现异常时抛出的响应
<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 } })
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)
}
}
getData()
</script>
事件循环
概念:JavaScript 有一个基于事件循环的并发模型,事件循环负责执行代码、收集和处理事件以及执行队列中的子 任务。这个模型与其它语言中的模型截然不同,比如 C 和 Java。
事件循环:执行代码和收集异步任务,在调用栈空闲时,反复调用任务队列里回调函数执行机制
原因:JavaScript 单线程(某一刻只能执行一行代码),为了让耗时代码不阻塞其他代码运行,设计了事件循环模型
JS内代码如何执行:
- 执行同步代码,遇到异步代码交给宿主浏览器环境执行
- 异步有了结果后,把回调函数放入任务队列排队
- 当调用栈空闲后,反复调用任务队列里的回调函数
宏任务与微任务
ES6之后引入了Promise 对象,让 JS 引擎也可以发起异步任务 异步任务分为: 宏任务: 由浏览器环境执行的异步代码 微任务:由 JS 引擎环境执行的异步代码
JS 内代码如何执行?
执行第一个 script 脚本事件宏任务,里面同步代码;遇到 宏任务/微任务 交给宿主环境,有结果回调函数进入对应队列
Promise.all 静态方法
概念:合并多个 Promise 对象,等待所有同时成功完成(或某一个失败),做后续逻辑
例:
<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 = htmlStr
}).catch(error => {
console.dir(error)
})
</script>
</body>
二、项目 - 数据管理平台
项目介绍、准备
功能:
-
登录和权限判断
-
查看文章内容列表(筛选,分页)
-
编辑文章(数据回显)
-
删除文章
-
发布文章(图片上传,富文本编辑器)
技术:
- 基于Bootstrap 搭建网站标签和样式
- 集成 wangEditor 插件实现富文本编辑器使用原生jS 完成增删改查等业务
- 基于axios与黑马头条线上接口交互
- 使用axios 拦截器进行权限判断
验证码登录
/** index.js
* 目标1:验证码登录
* 1.1 在 utils/request.js 配置 axios 请求基地址
* 1.2 收集手机号和验证码数据
* 1.3 基于 axios 调用验证码登录接口
* 1.4 使用 Bootstrap 的 Alert 警告框反馈结果给用户
*/
// 1.2 收集手机号和验证码数据
document.querySelector('.btn').addEventListener('click', () => {
const form = document.querySelector('.login-form')
const data = serialize(form, { hash: true, empty: true })
console.log(data)
// 1.3 基于 axios 调用验证码登录接口
axios({
url: '/v1_0/authorizations',
method: 'POST',
data
}).then(result => {
// 1.4 使用 Bootstrap 的 Alert 警告框反馈结果给用户
myAlert(true, '登录成功')
console.log(result)
// 登录成功后,保存 token 令牌字符串到本地,并跳转到内容列表页面(后面有解释token的作用)
localStorage.setItem('token', result.data.token)
setTimeout(() => {
// 延迟跳转,让 alert 警告框停留一会儿
location.href = '../content/index.html'
}, 1500)
}).catch(error => {
myAlert(false, error.response.data.message)
console.dir(error.response.data.message)
})
})
// 弹窗插件 alert.js
// 需要先准备 alert 样式相关的 DOM
/**
* BS 的 Alert 警告框函数,2秒后自动消失
* @param {*} isSuccess 成功 true,失败 false
* @param {*} msg 提示消息
*/
function myAlert(isSuccess, msg) {
const myAlert = document.querySelector('.alert')
myAlert.classList.add(isSuccess ? 'alert-success' : 'alert-danger')
myAlert.innerHTML = msg
myAlert.classList.add('show')
setTimeout(() => {
myAlert.classList.remove(isSuccess ? 'alert-success' : 'alert-danger')
myAlert.innerHTML = ''
myAlert.classList.remove('show')
}, 2000)
}
token
token 的介绍
概念:访问权限的令牌,本质上是一串字符串
创建:正确登录后,由后端签发并返回
作用:判断是否有登录状态等,控制访问权限
注意:前端只能判断 token 有无,而后端才能判断 token 的有效性
token 的使用
目标:只有登录状态,才可以访问内容
页面步骤:
- 在 utils/auth.js 中判断有无 token 令牌字符串,没有则强制跳转到登录页(手动修改地址栏测试)
- 在登录成功后,保存 token 令牌字符串到本地,再跳转到首页(手动修改地址栏测试)
// 权限插件(引入到了除登录页面,以外的其他所有页面)
/**
* 目标1:访问权限控制
* 1.1 判断无 token 令牌字符串,则强制跳转到登录页
* 1.2 登录成功后,保存 token 令牌字符串到本地,并跳转到内容列表页面
*/
// 1.1 判断无 token 令牌字符串,则强制跳转到登录页
const token = localStorage.getItem('token')
if (!token) {
location.href = '../login/index.html'
}
/* index.js */
// 登录成功后,保存 token 令牌字符串到本地,并跳转到内容列表页面
localStorage.setItem('token', result.data.token)
setTimeout(() => {
// 延迟跳转,让 alert 警告框停留一会儿
location.href = '../content/index.html'
}, 1500)
axios 拦截器
请求拦截器
axios 请求拦截器:发起请求之前,触发的配置函数,对请求参数进行额外配置
/* request.js */
// 添加请求拦截器
axios.interceptors.request.use(function (config) {
// 在发送请求之前做些什么
// 统一携带 token 令牌字符串在请求头上
const token = localStorage.getItem('token')
token && (config.headers.Authorization = `Bearer ${token}`)
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});
/* /utils/auth.js */
/**
* 目标2:设置个人信息
* 2.1 在 utils/request.js 设置请求拦截器,统一携带 token(上面已添加好请求拦截器)
* 2.2 请求个人信息并设置到页面
*/
// 2.2 请求个人信息并设置到页面
axios({
url: '/v1_0/user/profile'
}).then(result => {
const username = result.data.name
document.querySelector('.nick-name').innerHTML = username
})
总结:
- 什么是 axios 请求拦截器? 发起一个请求前,调用一个函数,对请求参数进行设置
- axios 请求拦截器,什么时候使用? 有公共配置和设置时,统一设置在请求拦截器中
响应拦截器
定义:响应回到 then/catch 之前,触发的拦截函数,对响应结果统一处理
例如:身份验证失败,统一判断并做处理
什么时候触发成功/失败的回调函数?
状态为 2xx 触发成功回调,其他则触发失败的回调函数
/* request.js */
// 添加响应拦截器
axios.interceptors.response.use(function (response) {
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么,例如:直接返回服务器的响应结果对象
const result = response.data
return result;
}, function (error) {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么,例如:统一对 401 身份验证失败情况做出处理
console.dir(error)
if (error?.response?.status === 401) {
alert('身份验证失败,请重新登录')
localStorage.clear()
location.href = '../login/index.html'
}
return Promise.reject(error);
});
发布文章
富文本编辑器
富文本:带样式,多格式的文本,在前端一般使用标签配合内联样式实现
富文本编辑器:用于编写富文本内容的容器
目标:发布文章页,富文本编辑器的集成
使用:wangEditor 插件
步骤: 参考文档(快速开始 | wangEditor)
-
引入 CSS 定义样式
-
定义 HTML 结构
-
引入JS 创建编辑器
-
监听内容改变,保存在隐藏文本域(便于后期收集 )
1、2步骤不展开了,参考文档里有说明,重点讲下3、4步
// 富文本编辑器(editor.js)
// 创建编辑器函数,创建工具栏函数
const { createEditor, createToolbar } = window.wangEditor
// 编辑器配置对象
const editorConfig = {
// 占位提示文字
placeholder: '发布文章内容...',
// 编辑器变化时回调函数
onChange(editor) {
// 获取富文本内容
const html = editor.getHtml()
// 也可以同步到 <textarea>
// 为了后续快速收集整个表单内容做铺垫
document.querySelector('.publish-content').value = html
}
}
// 创建编辑器
const editor = createEditor({
// 创建位置
selector: '#editor-container',
// 默认内容
html: '<p><br></p>',
// 配置项
config: editorConfig,
// 配置集成模式(default 全部)(simple 简洁)
mode: 'default', // or 'simple'
})
// 工具栏配置对象
const toolbarConfig = {}
// 创建工具栏
const toolbar = createToolbar({
// 为指定编辑器创建工具栏
editor,
// 工具栏创建的位置
selector: '#toolbar-container',
// 工具栏配置对象
config: toolbarConfig,
// 配置集成模式
mode: 'default', // or 'simple'
})
频道列表
目标:展示频道列表,供用户选择 步骤:1. 获取频道列表数据 2. 展示到下拉菜单中
/**
* 目标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()
封面设置
目标: 文章封面的设置
步骤:
-
准备标签结构和样式
-
选择文件并保存在 FormData
-
单独上传图片并得到图片 URL 地址回显并切换 img 标签展示 (隐藏 + 号上传标签)
-
注意: 图片地址临时存储在 img 标签上,并未和文章关联保存
/**
* 目标2:文章封面设置
* 2.1 准备标签结构和样式
* 2.2 选择文件并保存在 FormData
* 2.3 单独上传图片并得到图片 URL 网址
* 2.4 回显并切换 img 标签展示(隐藏 + 号上传标签)
*/
// 2.2 选择文件并保存在 FormData
document.querySelector('.img-file').addEventListener('change', async e => {
const file = e.target.files[0]
const fd = new FormData()
fd.append('image', file)
// 2.3 单独上传图片并得到图片 URL 网址
const res = await axios({
url: '/v1_0/upload',
method: 'POST',
data: fd
})
// 2.4 回显并切换 img 标签展示(隐藏 + 号上传标签)
const imgUrl = res.data.url
document.querySelector('.rounded').src = imgUrl
document.querySelector('.rounded').classList.add('show')
document.querySelector('.place').classList.add('hide')
})
// 优化:点击 img 可以重新切换封面
// 思路:img 点击 => 用 JS 方式触发文件选择元素 click 事件方法
document.querySelector('.rounded').addEventListener('click', () => {
document.querySelector('.img-file').click()
})
收集并提交保存
目标:收集文章内容,并提交保存
步骤:
-
基于 form-serialize 插件收集表单数据对象
-
基于axios 提交到服务器保存
-
调用 Alert 警告框反馈结果给用户
-
重置表单并跳转到列表页
/**
* 目标3:发布文章保存
* 3.1 基于 form-serialize 插件收集表单数据对象
* 3.2 基于 axios 提交到服务器保存
* 3.3 调用 Alert 警告框反馈结果给用户
* 3.4 重置表单并跳转到列表页
*/
// 3.1 基于 form-serialize 插件收集表单数据对象
document.querySelector('.send').addEventListener('click', async e => {
// 因为新增和编辑页共用一套页面,所以这里要对发布和修改做区分,是发布才能继续往下走
if (e.target.innerHTML !== '发布') return
const form = document.querySelector('.art-form')
const data = serialize(form, { hash: true, empty: true })
// 发布文章的时候,不需要 id 属性,所以可以删除掉(id 为了后续做编辑使用)
delete data.id
console.log(data)
// 自己收集封面图片地址并保存到 data 对象中
data.cover = {
type: 1, // 封面类型
images: [document.querySelector('.rounded').src] // 封面图片 URL 网址
}
// 3.2 基于 axios 提交到服务器保存
try {
const res = await axios({
url: '/v1_0/mp/articles',
method: 'POST',
data: data
})
// 3.3 调用 Alert 警告框反馈结果给用户
myAlert(true, '发布成功')
// 3.4 重置表单并跳转到列表页
form.reset()
// 封面需要手动重置
document.querySelector('.rounded').src = ''
document.querySelector('.rounded').classList.remove('show')
document.querySelector('.place').classList.remove('hide')
// 富文本编辑器重置
editor.setHtml('')
setTimeout(() => {
location.href = '../content/index.html'
}, 1500)
} catch (error) {
myAlert(false, error.response.data.message)
}
})
内容管理
文章列表展示
目标:获取文章列表并展示 步骤
- 准备查询参数对象
- 获取文章列表数据
- 展示到指定的标签结构中
/**
* 目标1:获取文章列表并展示
* 1.1 准备查询参数对象
* 1.2 获取文章列表数据
* 1.3 展示到指定的标签结构中
*/
// 1.1 准备查询参数对象
const queryObj = {
status: '', // 文章状态(1-待审核,2-审核通过)空字符串-全部
channel_id: '', // 文章频道 id,空字符串-全部
page: 1, // 当前页码
per_page: 2 // 当前页面条数
}
let totalCount = 0 // 保存文章总条数
// 获取并设置文章列表
async function setArtileList() {
// 1.2 获取文章列表数据
const res = await axios({
url: '/v1_0/mp/articles',
params: queryObj
})
// 1.3 展示到指定的标签结构中
const htmlStr = res.data.results.map(item => `<tr>
<td>
<img src="${item.cover.type === 0 ? `https://img2.baidu.com/it/u=2640406343,1419332367&fm=253&fmt=auto&app=138&f=JPEG?w=708&h=500`: 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>
<!--4.1 关联文章id到删除图标-->
<td data-id="${item.id}">
<i class="bi bi-pencil-square edit"></i>
<i class="bi bi-trash3 del"></i>
</td>
</tr>`).join('')
document.querySelector('.art-list').innerHTML = htmlStr
// 3.1 保存并设置文章总条数
totalCount = res.data.total_count
document.querySelector('.total-count').innerHTML = `共 ${totalCount} 条`
}
setArtileList()
筛选功能(查询)
目标: 根据筛选条件,获取匹配数据展示
步骤:
-
设置频道列表数据
-
监听筛选条件改变,保存查询信息到查询参数对象
-
点击筛选时,传递查询参数对象到服务器
-
获取匹配数据,覆盖到页面展示
/* /page/content/index.js */
/**
* 目标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事件->绑定到查询参数对象上
document.querySelectorAll('.form-check-input').forEach(radio => {
radio.addEventListener('change', e => {
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()
})
分页功能
目标:完成文章列表,分页管理功能
步骤
- 保存并设置文章总条数(在上面文章列表展示里有相关代码)
- 点击下一页,做临界值判断,并切换页码参数请求最新数据
- 点击上一页,做临界值判断,并切换页码参数请求最新数据
/**
* 目标3:分页功能
* 3.1 保存并设置文章总条数
* 3.2 点击下一页,做临界值判断,并切换页码参数并请求最新数据
* 3.3 点击上一页,做临界值判断,并切换页码参数并请求最新数据
*/
// 3.2 点击下一页,做临界值判断,并切换页码参数并请求最新数据
document.querySelector('.next').addEventListener('click', e => {
// 当前页码小于最大页码数 (最大页码如果是小数得向上取整)
if (queryObj.page < Math.ceil(totalCount / queryObj.per_page)) {
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()
}
})
删除功能
目标:完成删除文章功能
步骤:
- 关联文章 id 到删除图标(在上面文章列表展示里有相关代码)
- 点击删除时,获取文章 id
- 调用删除接口,传递文章 id 到服务器
- 重新获取文章列表,并覆盖展示
注意!在删除最后一条时会有bug,解决方案:
- 删除成功时,判断 DOM 元素只剩一条,让当前页码 page --
- 当前页码为1时不能继续向前翻页
- 重新设置页码数,获取最新列表展示
/**
* 目标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 => {
// 判断点击的是删除元素
if (e.target.classList.contains('del')) {
const delId = e.target.parentNode.dataset.id
// 4.3 调用删除接口,传递文章 id 到服务器
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()
}
})
编辑 - 回显文章
目标: 编辑文章时,回显数据到表单
步骤:
- 页面跳转传参 (URL 查询参数方式)
- 发布文章页面接收参数判断 (共用同一套表单)
- 修改标题和按钮文字
- 获取文章详情数据并回显(填充到)表单中
/* /page/content/index.js */
// 点击编辑时,获取文章 id,跳转到发布文章页面传递文章 id 过去
document.querySelector('.art-list').addEventListener('click', e => {
if (e.target.classList.contains('edit')) {
const artId = e.target.parentNode.dataset.id
console.log(artId)
location.href = `../publish/index.html?id=${artId}`
}
})
/* page/publish/index.js */
/**
* 目标4:编辑-回显文章
* 4.1 页面跳转传参(URL 查询参数方式)
* 4.2 发布文章页面接收参数判断(共用同一套表单)
* 4.3 修改标题和按钮文字
* 4.4 获取文章详情数据并回显表单
*/
; (function () {
// 4.2 发布文章页面接收参数判断(共用同一套表单)
const paramsStr = location.search
const params = new URLSearchParams(paramsStr)
params.forEach(async (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
}
// 遍历数据对象属性,映射到页面元素上,快速赋值
Object.keys(dataObj).forEach(key => {
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]
}
})
}
})
})();
编辑 - 保存文章
目标: 确认修改,保存文章到服务器
步骤:
- 判断按钮文字,区分业务 (因为共用一套表单)
- 调用编辑文章接口,保存信息到服务器
- 基于 Alert 反馈结果消息给用户
/* /page/publish/index.js */
/**
* 目标3:发布文章保存
* 3.1 基于 form-serialize 插件收集表单数据对象
* 3.2 基于 axios 提交到服务器保存
* 3.3 调用 Alert 警告框反馈结果给用户
* 3.4 重置表单并跳转到列表页
*/
// 3.1 基于 form-serialize 插件收集表单数据对象
document.querySelector('.send').addEventListener('click', async e => {
if (e.target.innerHTML !== '发布') return
const form = document.querySelector('.art-form')
const data = serialize(form, { hash: true, empty: true })
// 发布文章的时候,不需要 id 属性,所以可以删除掉(id 为了后续做编辑使用)
delete data.id
console.log(data)
// 自己收集封面图片地址并保存到 data 对象中
data.cover = {
type: 1, // 封面类型
images: [document.querySelector('.rounded').src] // 封面图片 URL 网址
}
// 3.2 基于 axios 提交到服务器保存
try {
const res = await axios({
url: '/v1_0/mp/articles',
method: 'POST',
data: data
})
// 3.3 调用 Alert 警告框反馈结果给用户
myAlert(true, '发布成功')
// 3.4 重置表单并跳转到列表页
form.reset()
// 封面需要手动重置
document.querySelector('.rounded').src = ''
document.querySelector('.rounded').classList.remove('show')
document.querySelector('.place').classList.remove('hide')
// 富文本编辑器重置
editor.setHtml('')
setTimeout(() => {
location.href = '../content/index.html'
}, 1500)
} catch (error) {
myAlert(false, error.response.data.message)
}
})
退出登录
目标: 完成退出登录效果
步骤:
- 绑定点击事件
- 清空本地缓存,跳转到登录页面
/* /utils/auth.js */
/**
* 目标3:退出登录
* 3.1 绑定点击事件
* 3.2 清空本地缓存,跳转到登录页面
*/
// 3.1 绑定点击事件
document.querySelector('.quit').addEventListener('click', e => {
// 3.2 清空本地缓存,跳转到登录页面
localStorage.clear()
location.href = '../login/index.html'
})
完结!