网络大师课 浏览器的通信能力
2023-03-25 渡一网络大师课
Teather Yuan said:
ChatGPT 做一些普通的文案,还不能是非常高阶的文案,
比如普通短视频里面的电影解说,就是中规中矩出一大堆文案出来,批量生产的文案是特别有帮助的,
还包括一些低端的绘图,很是有帮助的,
替代不了高端的活,高端的活 ChatGPT 简直就是无助
沟通成本极高,基本跟 AI 聊一天,才能获得那么一点点的灵感,
但是对低端工作确实还可以,
比如询问一些编程里面的 api,一些函数是干什么用的,或实现一些小功能,AI 它听得懂,比如冒泡排序,一些知名的算法问题...,跟 AI 还聊的出来,其他的就是那么回事
现在 ChatGPT 给 Teather Yuan 的感觉是,
每天有多大的帮助是真谈不上,就是偶尔它可以激发你一些灵感,你没有想到的东西,它给你一些提示的作用,但是你要学会分辨,
但是根本性的作用,还谈不上替代开发
Teather Yuan said:
概念、原理、思想,告诉我们这个东西是怎么来的,在将来会越来越重要
因为概念原理涉及到我们对计算机,对我们工作环境了解的深度,
软件开发的本质就是人与计算机的沟通,
你对计算机了解的越深刻,沟通的成本就越低,你的价值就越高,
说的在直白一点,你的薪资待遇收入就越高,
这是最内核的东西
而像 API 这些东西是最不重要的东西,
因为这些在mdn上、官方文档上是非常容易查到的,查阅成本极低,
现在有了 AI 直接问它就完了,比如你要实现什么样的功能,有什么样的 API 可以用
但是有一点是 AI 无法替代的,就是你对技术理解的深度,
目前 AI 对技术理解的深度非常有限,只能达到初级的理解,
但高级的理解也有,你挖掘不出来,因为挖掘成本很高,因为 AI 的神经网络就是那样训练出来的,
要跟 AI 进行深入的交流,必须要具有足够的专业性,才能跟 AI 聊的出来,
但是沟通成本极高,袁老师说他有一次聊了很长时间,才聊出一点点有启发的内容,
所以我们要重视原理和概念,因为这是最重要的
一、用户代理
从上节课的思考题开始,
如果不使用浏览器,是否能够完成页面浏览?
答案是可以的,
上节课用 VsCode 的插件 REST Client 可以发请求,拿到响应结果,
响应结果里面有各种标签,比如有一个 img 标签上有图片的地址,然后在去用 http 请求就可以看到图片,只是非常不友好
浏览器出现的目的,
就是来帮助用户发送 http 请求,帮助用户读懂解析这些响应结果,所以浏览器叫做“user agent”用户代理
因此浏览器的默认样式上面写的是 user agent stylesheet 意思是用户代理设置的样式
仅从网络通信层面,对于前端开发者,必须要知道浏览器拥有的两大核心能力(当然浏览器有很多的能力)
1. 自动发出请求的能力
2. 自动解析响应的能力
1、自动发出请求的能力
就是有一些时候,浏览器自己就直接帮我们把 http 请求发出了,常见的就这么几种情况
1. 用户在地址栏输入一个 url 地址,并按下回车
2. 用户点击了页面中的 a 元素
3. 用户点击了提交按钮 <button type="submit">...</button>
4. 当解析 HTML 页面遇到 <link> <img> <script> <video> <audio> 等元素的时候
5. 当用户点击了刷新
1. 用户在地址栏输入一个 url 地址,并按下回车
浏览器会自动解析 url,并发出一个 GET 请求,同时抛弃当前页面
Network
General 表示汇总信息
Request Headers 请求行、请求头
Response Headers 响应行、响应头
2. 用户点击了页面中的 a 元素
浏览器会拿到 a 元素的 href 地址,发出一个 GET 请求,同时抛弃当前页面
说到到 a 元素的 href 地址,
袁老师讲解了一下“相对路径”和“绝对路径”
袁老师问
/a/b/c 这个路径是“相对路径”还是“绝对路径”?
答,这是绝对路径
相对路径相对的是什么?
答,相对的是 url 中的 path
“相对路径”和“绝对路径”在不同的环境里面有很多的语义
在操作系统里面,相对的是文件的结构
但是在前端网络这里“相对路径”、“绝对路径”相对的是 url,跟文件、文件夹没有任何关系
相对路径有三种写法(其它的都属于绝对路径)
路径 | |
./a | 点斜杠开头 |
../a | 点点斜杠开头 |
a/b | 直接写 |
看一个相对路径的示例,
页面的 url 是 http://www.baidu.com/news/detail.html
页面中的 a 元素使用了相对路径 <a href="./data.html"></a>
相对路径的 ./ 是相对谁呢?
1. 当前页面 path 部分是 /news/detail.html
2. ./ 相对的是当前页面 path 的前半段 /news/,就是把 path 最后一段 detail.html 去掉
3. 拼接上了 /news/ + data.html 就是一个绝对路径 /news/data.html
为什么说 /news/data.html 是绝对路径呢?
下面看一下什么是绝对路径,把绝对路径认识清楚后,在来看相对路径
绝对路径
袁老师首先说了一个事实“所有要发出 http 请求只能是绝对路径,相对路径是无效的”,
因为发出一个请求,要一个完整的 url 地址,需要知道协议、域名、端口号,所以只能用绝对路径
绝对路径有哪些?
绝对路径有三种写法
绝对路径 | |
http://www.baidu.com/a/b | 一个完整的 url 地址是绝对路径 |
//www.baidu.com/a/b | 绝对路径可以省略一些东西,比如省略“协议”也是绝对路径,省略的协议用当前页面的协议 |
/a/b | 省略协议、域名、端口也是绝对路径,省略的部分复用当前页面的协议、端口和域名 |
当然,省略了协议、域名、端口的绝对路径,
最后也会转换成完整的 url 才能发出请求,不然信息不完整发不出去请求
绝对路径有一些缺陷,
就是它无法应对迁移,比如网站的资源是这样的
http://www.duyi.com/index.html
http://www.duyi.com/js/index.js
http://www.duyi.com/css/index.css
页面用绝对路径引用 css 文件 <link href="http://www.duyi/css/index.css">,但是这样是最不好的
1. 因为引用文件的地址 href="http://www.duyi/css/index.css 耦合度太高了
跟协议产生了耦合
跟域名产生了耦合
2. 今后域名变成 www.duyiedu.com 就要改很多的路径
可以这样写,
<link href="/css/index.css">
只保留一个路径,复用页面的域名、协议
但是有时候还是解决不了问题,
比如,现在要做很多的产品线,之前开发的是一套新闻的网站,想把新闻的网站前面全部加上前缀 /news,迁移一下
http://www.duyi.com/news/index.html
http://www.duyi.com/news/js/index.js
http://www.duyi.com/news/css/index.css
因为用的是绝对路径,而绝对路径的 path 变了,代码里面路径全部要改
之前 <link href="/css/index.css">
要改成 <link href="/news/css/index.css">
因此我们发现,
当一个站点迁移的时候,前面加上一个 /news 子路径,很多东西都变了,
但是唯独这些文件的相对位置没有发生变化
index.html 和 js/index.js 与 css/index.css 的相对位置没有发生变化
于是就出现了相对路径,
相对路径并不是有效的路径,它最终一定要转换成完整的 url 地址才能访问
相对路径
相对路径相对的是当前页面的 path 部分
比如,
当前页面是 http://www.duyi.com/index.html
当前页面访问 js 文件 http://www.duyi.com/js/index.js
相对路径的写法 <script src="./js/index.js"></script>
相对路径是怎么转换成完整的 url 地址的呢?
1. 复用当前页面的协议、域名、端口号
http://www.duyi.com
2. 路径部分,
./ 的意思是从当前页面的 url 最后一个字符开始向前找,一直找到第一个斜杠 /
然后从该斜杠开始,一直到整个路径结束,
然后放到协议域名端口号后面 http://www.duyi.com/
3. 然后拼接上 ./ 后面的 js/index.css
就形成一个完整的 url 地址 http://www.duyi.com/js/index.css
下面来印证相对路径的转换,
首页页面写一个相对路径的 a 元素 <a href="./js/index.js"></a>
打开 live Server
调试工具 -> 选项 Elements -> 选中 a 元素 -> 发现有一个用于调试的 $0
打开 Console 选项
$0 表示选中的这个元素 <a href="./js/index.js"></a>
$0.href 通过 href 属性,相对路径转换成一个完整的 url 地址 http://localhost:5500/js/index.js
$0.getAttrbute('href') 是原始代码里 href 属性的值 ./js/index.js
相对路径好处是,
将来页面的路径变了,路径前面加了 /news 变成 http://www.duyi.com/news/index.html
代码里的相对路径不用改 <script src="./js/index.js"></script>
因为点斜杠 ./ 的意思是
1. http://www.duyi.com/news/index.html <- 从右边往前开始找
找到到第一个 / 斜杠 - 到路径结束,就是这部分 /news/
2. 然后 /news/ 后面拼接上 js/index.js
/news/js/index.js
3. 转换后就是完整路径 http://www.duyi.com/news/js/index.js
这就是相对路径的好处是,
只要相对位置没有变化,代码就不用重新写,
但是最终要生效,要能够发出请求,绝对要转换成绝对路径,完整的 url 地址才行,不然信息不全
点点斜杠 ../ 的意思
<script src="../js/index.js"></script>
1. 从页面地址 http://www.duyi.com/news/index.html <- 右边开始找到第二个的斜杠
从第二个斜杠到路径结束 http://www.duyi.com/
2. 然后拼接上 js/index.js 就是完整路径 http://www.duyi.com/js/index.js
这才是“相对路径”和“完整路径”最准确的解释,
是相对 url 地址的,跟文件没有什么关系
之所以会误以为是跟文件相关,
是因为平时用的是“静态资源服务器”,一个路径就对应到相应的文件,
但实际情况是很复杂的,不一定是这种情况,
我们平时访问的很多站点的时候,页面都是服务器动态生成渲染出来的,这个时候就没法理解相对路径和绝对路径了
3. 用户点击了提交按钮 <button type="submit">...</button> 的时候浏览器会自动发出请求
from 元素必须要学了网络之后才能真正的理解,
官方设计 form 元素出来就是用来发请求的,它可以发 GET 请求同时也可以发 POST 请求
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>form元素</title> </head> <body> <form action="https://study.duyiedu.com/api/user/login" method="post"> 账号:<input type="text" name="loginId"> 密码:<input type="text" name="loginPwd"> <button type="submit">提交</button> </form> </body> </html>
form 元素怎么自动发请求呢?
1. action 属性是请求的 url 地址
2. method 属性配置请求方法 GET/POST
3. 什么时候发请求的呢?用户点击了 <button type="submit"> 按钮的时候,并且 type 属性的值是 submit(submit 是提交的意思)
url 地址,请求方法都有了,接下来是请求体,
请求体是键值对的方式 loginId=xxx&loginPwd=xxx 组织到请求体中
文本框的 name 属性是请求体里面的属性名,
文本框的值是属性名
点击 button 按钮,
浏览器自动的找到 <form> 元素下面所有文本框的数据,并组织到请求体中,
然后发出指定方法的请求,同时抛弃当前页面
Network 选项下,
Preserve log 勾选,表示持续日志,不然页面刷新之前的日志就没有了
Payload 是请求体 loginId=admin&loginPwd=123
Review/Response 服务器的响应结果
form 元素现在很少用,但是 from 元素仍然有用,
它最大的用处是在填写表单的时候,在任何地方一按回车就能提交,这是浏览器自带的功能,
虽然给文本框注册键盘监听回车事件,如果有多个文本框,要给每个文本框都要注册监听事情
我们希望利用 from 元素回车提交的能力,但又不希望 from 真正的发请求(发请求)
1. 首先这两个 action、method 属性就不用写了
2. 然后注册 submit 事件监控 from 元素,
然后点击 button 提交按钮,
或在 input 框按了回车都会触发该事情
3. 提交的时候阻止默认行为
<form> 账号:<input type="text" name="loginId"> 密码:<input type="text" name="loginPwd"> <button type="submit">提交</button> </form> <script> const form = document.querySelector("form"); form.onsubmit = e => { e.preventDefault(); console.log('手动发请求'); } </script>
袁老师说,
这种做法一定要会,这并不是什么高级的做法,
不管是做搜索框还是表单,都要套一个 form 标签实现回车提交,做统一的监控
这样提交的时候页面不会刷新
4. 当解析 HTML 页面时,遇到 <link> <img> <script> <video> <audio> 等元素的时候
这些元素都有一个外连接,连接到别的资源,
遇到这些元素的时候,浏览器会发出相应的 GET 请求,就是在渲染页面的过程中发出的请求
经常有一道面试题:
当用户在浏览器输入 url 地址,按下回车后会发生哪些事?
你知识面越广,了解的越深,回答的程度越不一样,
它涉及到很多问题,一方面是网络,一方面是渲染
5. 当用户点击了刷新
Ctrl + r 或点击刷新,
浏览器会拿到当前页面的地址,以及当前页面的请求方法,重新发一次请求,
同时抛弃当前页面
浏览器在发出请求时,会自动附带一些请求头
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
表示告诉服务器,我希望接收哪些格式的内容,
大部分服务器不太 caer 这个,因不知道服务器要不要所以还是发了
Accept-Encoding: gzip, deflate, br, zstd
浏览器告诉服务器,我支持的一些压缩格式
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
浏览器告诉服务器,我支持的一些语言
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0
User-Agent 表示我是谁
自我介绍,我是那个浏览器,
一些搜索爬虫,比如百度、谷歌不停的发出请求访问页面分析,会通过 User-Agent 会通过告诉是那家公司的爬虫
下面没有讲是 md 上面的笔记
从古至今,浏览器都有一个约定
当发送GET请求时,浏览器不会附带请求体
这个约定深刻的影响着后续的前后端各种应用,现在,几乎所有人都在潜意识中认同了这一点,无论是前端开发人员还是后端开发人员。
由于前后端程序的默认行为,逐步造成了 GET 和 POST 的各种差异:
1. 浏览器在发送 GET 请求时,不会附带请求体
2. GET 请求的传递信息量有限,适合传递少量数据;
POST 请求的传递信息量是没有限制的,适合传输大量数据。
3. GET 请求只能传递 ASCII 数据,遇到非 ASCII 数据需要进行编码;
POST 请求没有限制
3. 大部分 GET 请求传递的数据都附带在 path 参数中,能够通过分享地址完整的重现页面,但同时也暴露了数据,若有敏感数据传递,不应该使用 GET 请求,至少不应该放到 path 中
4. POST 不会被保存到浏览器的历史记录中
5. 刷新页面时,若当前的页面是通过 POST 请求得到的,则浏览器会提示用户是否重新提交。若是 GET 请求得到的页面则没有提示。
2、自动解析响应的能力
浏览器除了自动发请求之外,还有一个自动解析响应的能力,能根据不同响应结果做出不同的自动处理,呈现给用户一个漂亮的结果
浏览器是如何解析响应结果的的呢?
1. 自动识别响应码
2. 根据不同的响应结果做不同的处理
1. 自动识别响应码
浏览器能够自动识别响应码,当出现一些特殊响应码时浏览器会自动完成处理,
比如,301、302 重定向后浏览器会自动的完成跳转
示例
http://www.baidu.com
百度不是 301 或 302,用的是 307 都是内部重定向,有些细微的差别都差不多
1. 当浏览器看到 HTTP/1.1 307 Internal Redirect
2. 浏览器会自动的从响应头里面找 Location: https://www.baidu.com/,自动重新发出请求 https://www.baidu.com/
2. 根据不同的响应结果做不同的处理
浏览器能够自动分析响应头中的 Content-Type,根据不同的值进行不同的解析
Content-Type 属性值 | |
text/plain | 普通的纯文本,浏览器通常会将响应体原封不动的显示到页面上 |
text/html | html文档,浏览器通常会将响应体作为页面进行渲染 |
text/javascript 或 application/javascript | js代码,浏览器通常会使用JS执行引擎将它解析执行 |
text/css | css代码,浏览器会将它视为样式 |
image/jpeg | 浏览器会将它视为jpg图片 |
application/octet-stream | 二进制数据,会触发浏览器下载功能 |
attachment | 附件,会触发下载功能。该值和其他值不同,应放到 Content-Disposition 头中 |
示例
一个简单的服务器
启动服务器 node server.js
监听的是 7000 端口
const app = require('express')(); const text = `<h1>两只老虎爱跳舞</h1> 小兔子乖乖拔萝卜 我和小鸭子学走路 童年是最美的礼物`; app.get('/source/file.txt', (req, res) => { res.set('Content-Type', 'text/plain; charset=utf-8'); // 修改Content-Type的值 res.end(text); }); app.get('/d/file.txt', (req, res) => { res.set('Content-Type', 'text/plain; charset=utf-8'); res.set('Content-Disposition', 'attachment; filename=baby.txt'); res.end(text); }); app.listen(7000, () => { console.log('server started'); });
服务器响应头显示的是纯文本 res.set('Content-Type', 'text/plain; charset=utf-8')
请求 http://localhost:7000/source/file.txt
<h1> 标签也按照文本显示
如果改成 res.set('Content-Type', 'text/html; charset=utf-8')
重新启动服务器
再请求 http://localhost:7000/source/file.txt
<h1> 标签就变成一级标题了
如果改成 res.set('Content-Type', 'image/png')
看到这个响应头,浏览器知道响应体的内容是一个二进制
重新启动服务器
再请求的时候 http://localhost:7000/source/file.txt
不是一个有效的二进制,渲染出一张空白图片
请求的这个地址的时候 http://localhost:7000/d/file.txt
触发下载文件
res.set('Content-Type', 'text/plain; charset=utf-8')
res.set('Content-Disposition', 'attachment; filename=baby.txt')
Content-Disposition 响应头
attachment 附件
filename=baby.txt 默认的下载文件名
Teacher Yuan Said 通过这些范例,
知道 url 地址跟服务器的文件没有必然联系,把这个心结解开,认识到本质是什么,
本质就是一个 url 请求过去,服务器处理一些事情, 它想怎么处理就怎么处理
总结,
浏览器自动请求自动响应的基本流程
当请求一个 url 地址的时候 https://oss.duyiedu.com/test/index.html
1. 补全 url 地址,比如协议、端口
2. 如果地址里面有中文参数,
比如 ?a="中文"
中文会自动编码,通过映射关系变成 ASCII 字符
因为请求头里面只能是 ASCII 编码,包含请求行,
行和头里面不能有中文或非 ASCII 字符,只能用英文、数字、英文标点符号
3. 开始发送 GET 请求,拿到 HTML 文档,浏览器会丢弃旧页面,这个时候浏览器变成空白了,准备接收新页面
4. 然后解析新的 HTML 文档
解析过程中,
发现 link 元素,请求解析并应用 css 样式
发现 img 图片,请求 img 元素
发现 script 元素,请求 js 文件,拿到 js 后还要执行 js
...
整个过程持续到文档全部解析完成,会触发一个事件 DomContentLoaded
当页面上所有的资源都已加载完毕,比如所有的图片都加载完后,触发 loaded 事件
5. 如果用户点击了 a 元素,又会重复这个流程,因为意味着一个新的请求发出了
二、AJAX
当深刻理解的网络,当深刻的理解了浏览器,剩下的写代码水到渠成,
因为写代码就意味着利用这些概念,然后使用一些 js 的 api
浏览器本身就具备网络通信的能力,但在早期,浏览器并没有把这个能力开放给JS,
最早是微软在IE浏览器中把这一能力向JS开放,让JS可以在代码中实现发送请求,并不会刷新页面,这项技术在2005年被正式命名为AJAX(Asynchronous Javascript And XML)
AJAX 的意思,说的直白点,就是在浏览器里面通过 js 发出请求,获取响应结果的能力
所以说,又了 ajax 现在用 form 来发请求没有什么意义了,因为它要刷新,
但是用 js 发出请求然后接收响应,可以不刷新页面用户体验更好,AJAX 就是这样的能力
AJAX 目前有两套 api,实现 web 应用程序中异步的向服务器发送请求的能力
1. XMLHttpRequest 简称 XHR 比较久远
2. Fetch H5出来的
功能点 | XHR | Fetch |
基本的请求能力 | ✅ | ✅ |
基本的获取响应能力 | ✅ | ✅ |
监控请求进度 | ✅ | ❌ |
监控响应进度 | ✅ | ✅ |
Service Worker中是否可用 | ❌ | ✅ |
控制cookie的携带 | ❌ | ✅ |
控制重定向 | ❌ | ✅ |
请求取消 | ✅ | ✅ |
自定义referrer | ❌ | ✅ |
流 | ❌ | ✅ |
API风格 | Event | Promise |
活跃度 | 停止更新 | 不断更新 |
袁老师说这些目前还没讲
Service Worker
cookie
自定义referrer
流
XHR 跟 Fetch 的风格不一样
XHR 是基于事件风格的,就是 addEventlistener 或 on 什么什么发生的时候处理什么函数
Fetch 是基于 2015 ES6 的 Promise 风格
Fetch API 基本上能够涵盖 XHR,
但是请求进度监控这一个地方取代不了,这一块在上传文件的时候特别有用,
不是官方做不到,因为基于 Promise 这套 API 不好设计
三、fetch
第一个参数写 url 地址,就可以发出了一个 GET 请求
fetch('https://study.duyiedu.com/api/herolist');
运行 js 代码的时候就发出一个请求
Network -> 选中 herolist,可以看到响应
1. Pesponse 原始的 json 格式
2. Preview 看到美化便于我们查看的格式
怎么在 js 里面拿到响应结果?
fetch 返回的是一个 promise,
所以可以使用 await fetch(url) 等待,等待完成后返回的是一个“响应对象”
async function getHero(){ const resp = await fetch('https://study.duyiedu.com/api/herolist'); console.log(resp); } getHero();
返回的响应对象里面有什么呢?
{
body: (...)
bodyUsed: false
headers: Headers {} 请求头在这里面
ok: true
redirected: false
status: 200 响应状态码
statusText: "OK" 响应状态的单词
type: "cors"
url: "https://study.duyiedu.com/api/herolist"
}
响应头在 headers 属性里面,
可以通过 get() 方法拿到某一个响应头,或者通过 entries() 方法拿到所有的响应头
async function getHero(){ const resp = await fetch('https://study.duyiedu.com/api/herolist'); for (let header of resp.headers.entries()) { console.log(header); } } getHero();
entries() 方法是一个迭代器,迭代器可以用 for of 循环,浏览器不会给我们全部的响应头,只给我们部分响应头信息
['content-length', '28247'] 表示响应体的字节数
['content-type', 'application/json; charset=utf-8'] 响应体的类型
也可以通过 get() 方法拿到响应头 content-type
async function getHero(){ const resp = await fetch('https://study.duyiedu.com/api/herolist'); console.log(resp.headers.get('content-type')); // application/json; charset=utf-8 } getHero();
如何获取到响应体?
我们想这样一个问题,如果服务器响应的内容特别巨大,有十个G,我们要等多次时间才能请求结束
浏览器的处理方式是,
1. 响应头是很小的,只要能拿到响应头 promise 就完成了,浏览器不管后面的响应体还有多少
2. 现在响应体可能还没有传输完,
3. 先拿到响应头了,要拿到响应体要等待后面传输完成以及解析完成
所以要拿响应体,需要告诉浏览器用那种方式来解析响应体
resp.json() json格式解析响应体
resp.text() 纯文本格式解析响应体
resp.blob() 二进制的方式解析响应体
具体用什么方式要看服务器响应的是什么,
这里服务器 content-type 响应的是 Application/json,
所以这里用 json() 方法解析,因为数据还要传输一会,所以又要 await 等待
async function getHero(){ const resp = await fetch('https://study.duyiedu.com/api/herolist'); const body = await resp.json(); // 这里又要等待,等待用json格式解析后面的传过来的数据,解析后的结果直接是 js 对象了 console.log(body); } getHero();
这就是 fetch api 要设计等待两次的原因
1. 第一次等待,是响应头到达客户端了,我们能拿到响应头了
2. 第二次等待,是要继续得到响应体,并按照某一种格式进行解析,等待后才能拿到响应体
示例,
英雄联盟
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Hero</title> <style> * {padding:0;margin:0;box-sizing:border-box;} ul {list-style: none;} a {text-decoration:none;color:inherit;} body {background:#ecedf2;} .container {width:1140px;margin:1em auto;} .list { display:grid; grid-template-columns:repeat(10, 1fr); justify-items: center; text-align: center; font-size: 14px; row-gap: 20px; color: #333; } .list img { width:77px;height:77px;display:block; margin-bottom:5px;border:2px solid #5f89cf;border-radius:10px 0 10px 0; } </style> </head> <body> <div class="container"> <ul class="list"></ul> </div> <script> async function getHero(){ const resp = await fetch('https://study.duyiedu.com/api/herolist'); const body = await resp.json(); const heroes = body.data; const ul = document.querySelector('.list'); ul.innerHTML = heroes.map(h => `<li> <a href="https://pvp.qq.com/web201605/herodetail/${h.ename}.shtml" target="_blank"> <img src="https://game.gtimg.cn/images/yxzj/img201606/heroimg/${h.ename}/${h.ename}.jpg" alt=""/> <span>${h.cname}</span> </a> </li>`).join(''); } getHero(); </script> </body> </html>
四、上传文件
1. 两个操作界面的辅助函数
F12 检查 -> console 测试两个函数的效果
1. showArea 函数负责切换显示区域(有三个区域),就是切换 div 的显示\隐藏,跟文件的上传没有关系
showArea('select') 初始的界面,选择文件的区域
showArea('progress') 上传中...上传进度条的区域,进度条背景是一张预览图
showArea('result') 结果区域,右上角有一个关闭按钮
2. setProgress 函数设置进度条
showArea('progress') 先切换到进度条区域,在通过 setProgress 函数设置
setProgress(10) 进度条变成 10%
setProgress(50) 进度条变成 50%
结合这两个函数完成文件上传,
上节课说过,
上传文件的本质就是 HTTP 协议的 POST 请求,把指定的格式发过去,然后返回结果
2. 如何选择要上传的文件
上传文件就是 <input type="file"> 元素
当我们点击了 .upload-select 虚线框框,就相当于点击了里面的 <input type="file">
因此给 .upload-select 注册一个 click 点击事件
点击外面的虚线框框,就相当于点击 <input type="file">
因此 doms.selectFile.click() 可以弹出选择上传文件的对话框(这行代码应该是激活上传 type="file")
怎么知道 <input type="file"> 选择了文件呢?
我们要监控 input 元素,注册 change 事件监控选择的文件
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>上传文件</title> <style> .upload { --border-color: #dcdfe6; --font-color: #8c939d; --primary-color: #409eff; --danger-color: #eb685e; } .upload * { box-sizing: border-box; } .upload { width: 150px; height: 150px; position: absolute; overflow: hidden; border-radius: 5px; top:50%;left:50%;transform:translate(-50%, -50%); } .upload .preview { object-fit: contain; z-index: 1; } .upload > * { position: absolute; left: 0; top: 0; width: 100%; height: 100%; } .upload > div { display: none; z-index: 2; } .upload.select .upload-select { display: block; } .upload.select .preview { display: none; } .upload.progress .upload-progress { display: block; } .upload.result .upload-result { display: block; } .upload-select { border-radius: inherit; border: 1px dashed var(--border-color); cursor: pointer; } .upload-select::before, .upload-select::after { content: ''; position: absolute; left: 50%; top: 50%; width: 30px; height: 3px; border-radius: 3px; background: var(--font-color); transform: translate(-50%, -50%); } .upload-select::after { transform: translate(-50%, -50%) rotate(90deg); } .upload-select input { display: none; } .upload-select:hover { border-color: var(--primary-color); } .upload-progress { background: #00000080; } .progress-bar { position: absolute; width: 90%; height: 3px; background: #fff; border-radius: 3px; left: 50%; top: 50%; transform: translate(-50%, -50%); color: #fff; font-size: 12px; } .progress-bar::before { counter-reset: progress var(--percent); content: counter(progress) '%'; position: absolute; left: 50%; top: -20px; transform: translateX(-50%); } .progress-bar::after { content: ''; position: absolute; left: 0; border-radius: inherit; width: calc(1% * var(--percent)); height: 100%; background: var(--primary-color); } .upload-progress::after { content: '文件上传中...'; position: absolute; left: 50%; top: 50%; transform: translate(-50%, 5px); white-space: nowrap; opacity: 0.8; color: #fff; font-size: 12px; } .upload button { border: none; outline: none; background: none; color: inherit; font-size: inherit; cursor: pointer; user-select: none; } .progress-bar button { left: 50%; position: absolute; top: 25px; transform: translateX(-50%); } .progress-bar button:hover { color: var(--danger-color); } .upload-result { border: 1px dashed var(--border-color); border-radius: inherit; overflow: hidden; } .upload-result button { width: 30px; height: 20px; background: var(--font-color); position: absolute; right: 0; top: 0; border-radius: 2px; color: #fff; } .upload-result button:hover { background: var(--danger-color); } </style> </head> <body> <div class="upload select"> <div class="upload-select"> <input type="file"/> </div> <div class="upload-progress" style="--percent: 20"> <div class="progress-bar"> <button>取消</button> </div> </div> <div class="upload-result"> <button>x</button> </div> <img src="" alt="" class="preview"> </div> <div class="box"></div> <script> const $ = document.querySelector.bind(document); const doms = { img: $('.preview'), container: $('.upload'), select: $('.upload-select'), selectFile: $('.upload-select input'), progress: $('.upload-progress'), cancelBtn: $('.upload-progress button'), delBtn: $('.upload-result button'), }; function showArea(areaName){ doms.container.className = `upload ${areaName}`; } function setProgress(value){ doms.progress.style.setProperty('--percent', value); } // 点击.upload-select就相当于点击了里面的input,因此注册一个点击事件 doms.select.onclick = function(){ doms.selectFile.click(); // 点击外面的虚线框框就相当点击input } // 监控input元素 doms.selectFile.onchange = function(){ console.log('change'); } </script> </body> </html>
现在还没有到 ajax,都是在做事件交互,
ajax 唯一的作用是发请求,获取响应
2. 如何获取上传文件的二进制数据
这部分还不是 ajax 的内容
// 监控input元素 doms.selectFile.onchange = function(){ const file = this.files; console.dir(file) }
<input type="file"> 元素里面有一个属性 files,表示选中的文件,因为有可能选择多个文件所以是复数
数组的第 0 位 File 就可以当做选中文件二进制,它背后就是文件二进制
FileList = [
0: File {name: '20140120.PNG', lastModified: 1705732971473, lastModifiedDate: Sat Jan 20 2024 14:42:51 GMT+0800 (中国标准时间), webkitRelativePath: '', size: 90621, …}
length: 1
]
const file = this.files[0]
因为只选择了一个文件,[0] 第一项就是我们要上传的文件
还可以判断 file.length 选择了几个文件,
比如做一个提示只可以选择一个
还是可以通过 size: 90621 判断文件的大小
通过 name: '20140120.PNG' 判断后缀名(需要字符串处理一下)
3. 现在可以发出 ajax 请求了
因为要监控请求的进度,所以用 XMLHttpRequest() 的方式请求
// 监控input元素 doms.selectFile.onchange = function(){ const file = this.files[0]; // 请求 const xhr = new XMLHttpRequest(); // 配置请求 xhr.open('POST', 'http://localhost/mycode/upload/single.php'); const form = new FormData(); form.append('avatar', file); // 发送请求 xhr.send(form); }
配置请求
请求方法 POST
请求地址 http://localhost/mycode/upload/single.php
消息格式 Content-Type: multipart/form-data;
字段名称 avatar
构造函数 FormData() 的作用是,帮我们构建下面这种“请求头”和“请求体”
Content-Type: multipart/form-data; boundary=aaa --aaa Content-Disposition: form-data; name="loginId" admin --aaa Content-Disposition: form-data; name="loginPwd" 123456 --aaa Content-Disposition: form-data; name="avatar"; filename="small.jpg" Content-Type: image/jpeg 文件的二进制 --aaa--
const form = new FormData()
form.append('avatar', 文件的二进制数据)
form 对象里面有一个 append 方法,表示追加一个字段
参数一,填 name, 对应的是 name="avatar"
参数二、是上传文件的二进制数据
参数三、是可以选的,对应的是 filename="small.jpg",不写默认是所上传文件的名字
通过 append 函数就和上传文件的请求格式对应起来了,
调用一次 append 函数就相当于加了一个字段,每一个字段用 --aaa 分割
然后把 from 对跟 ajax 联系起来,通过 send 直接把 form 发送到服务器
xhr.send(form)
Network 下面筛选请求类型,选中 fetch/XHR
4. xhr.send(form) 这句的作用
1. 自动生成了 boundary 分割符(之前用的是aaa)
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryDU5F0ycQ9D06BP2v
2. 选中 Payload 是上传的请求体,跟上面的请求体格式是一样的,只不过 binary 二进制显示不了只能空着
------WebKitFormBoundaryjsEXobRp3pY8rF7D
Content-Disposition: form-data; name="avatar"; filename="20181109191516857.jpg"
Content-Type: image/jpeg
------WebKitFormBoundaryjsEXobRp3pY8rF7D--
3. Response 是服务器的响应结果
{"code":0, "msg":"", "data": {"path": "http://localhost/my-code/upload/uploads/pic_51518_1728475573.jpg"}}
Ps:
axios 封装的是 xhr
umi-request 用的是 fetch
请求发过去了,图片上传成功了,
下面做上传进度,需要切换进度到的界面
5. 上传进度
doms.selectFile.onchange = function(){ const file = this.files[0]; const xhr = new XMLHttpRequest(); xhr.upload.addEventListener("progress", (e) => { // xhr.upload里面有一个propress事件 const percent = Math.floor((e.loaded / e.total) * 100); setProgress(percent); }); xhr.open('POST', 'http://localhost/mycode/upload/single.php'); const form = new FormData(); form.append('avatar', file); xhr.send(form); showArea('progress'); // 切换进度界面 }
请求发过去了,该切换界面了,
showArea('progress') 函数切换进度到的界面,
接下来就是设置进度
为什么 xhr 可以监控到请求进度呢?
因为 xhr 是基于事件的,
xhr.upload 里面有一个 propress 事件
监控 propress 事件,请求的数据发出一点就会触发该事件,事件对象 e 里面有两个数据
e.loaded 上传了多少数据
e.total 总数据量
为了看清楚把网络调慢一点
默认 No throtting
限流设置 3G
计算百分比 const percent = Math.floor((e.loaded / e.total) * 100)
有了百分比就可以调用函数 setProgress(percent)
ajax 的部分完成了,下面的与 ajax 无关了
6. 上传文件中,当前的预览图片的显示
// 监控input元素 doms.selectFile.onchange = function(){ const file = this.files[0]; const reader = new FileReader();// 文件读取器 reader.addEventListener('load', (e) => { // 文件读取完会触发该事件 doms.img.src = e.target.result; }); reader.readAsDataURL(file); // 请求 const xhr = new XMLHttpRequest(); // 请求配置 xhr.open('POST', 'http://localhost/mycode/upload/single.php'); xhr.upload.addEventListener("progress", (e) => { const percent = Math.floor((e.loaded / e.total) * 100); setProgress(percent); }); const form = new FormData(); form.append('avatar', file); // 发送请求 xhr.send(form); showArea('progress'); }
预览图片显示上传的文件 const file = this.files[0]
怎么显示上传的文件呢?
创建一个文件读取器 const reader = new FlieReader()
文件读取器里面有一个函数 readAsDataURL,把要读取的文件传进去,可以把一个文件读取成一个 data url
文件读取器有一个 load 事件,当文件读取完会触发这个事件
事件函数 e 里面,
e.target.result 得到结果是 base64 叫 data url
简单的说,
data url 是一个字符串,可以代替一个 url 地址
这个数据可以直接给预览图的 src 赋值
7. 剩下的事情
// 监控input元素 doms.selectFile.onchange = function(){ const file = this.files[0]; const reader = new FileReader(); reader.addEventListener('load', (e) => { doms.img.src = e.target.result; }); reader.readAsDataURL(file); // 请求 const xhr = new XMLHttpRequest(); // 请求配置 xhr.open('POST', 'http://localhost/mycode/upload/single.php'); xhr.upload.addEventListener("progress", (e) => { const percent = Math.floor((e.loaded / e.total) * 100); setProgress(percent); }); xhr.addEventListener('load', (e) => { // 响应完成切换界面 showArea('result'); }); doms.cancelBtn.onclick = () => { // 取消请求 xhr.abort(); showArea('select'); } const form = new FormData(); form.append('avatar', file); xhr.send(form); showArea('progress'); }
上传完成后切换界面,切换到初始界面
XML 是事件驱动的,它里面有很多事件,load表示拿到了响应结果,
运行 showArea('result')
如何上传过程中,取消请求呢?
非常简单,
调用 xhr 对象里面 xhr.abort();
Network 里面的 Status 显示 (canceled) 表示请求取消了
8.
// 监控input元素 doms.selectFile.onchange = function(){ const file = this.files[0]; const reader = new FileReader(); reader.addEventListener('load', (e) => { doms.img.src = e.target.result; }); reader.readAsDataURL(file); // 请求 const xhr = new XMLHttpRequest(); // 请求配置 xhr.open('POST', 'http://localhost/mycode/upload/single.php'); xhr.upload.addEventListener("progress", (e) => { const percent = Math.floor((e.loaded / e.total) * 100); setProgress(percent); }); xhr.addEventListener('load', (e) => { // 响应完成切换界面 }); doms.cancelBtn.onclick = () => { // 取消请求 xhr.abort(); showArea('select'); // 恢复到初始界面 } xhr.onreadystatechange = function(){ // 响应完成切换界面 if(xhr.readyState == 4){ showArea('result'); console.log(xhr.responseText); // 响应结果 } } const form = new FormData(); form.append('avatar', file); xhr.send(form); showArea('progress'); } // 上传完成后点击取消,切换到初始界面 doms.delBtn.onclick = () => { showArea('select'); }