Go to comments

网络大师课 浏览器的通信能力

渡一网络大师课

时间 2023-03-25


袁老师说:

ChatGPT 做一些普通的文案,还不能是高端的文案,

比如普通短视频里面的电影解说,就是中规中矩出一大堆文案出来,批量生产的文案是非常有帮助的,

还包括一些低端的绘图,很是有帮助的

替代不了高端的活,高端的活 ChatGPT 简直就是无助


沟通成本极高,基本跟 AI 聊一天,才能获得那么一点点的灵感,

但是对低端工作还可以, 

比如编程里面的 api、一些函数、实现一些小功能、冒泡排序,一些知名的算法问题...,还聊的出来,其他的就是那么回事


现在 AI 给袁老师的感觉是,

每天有多大的帮助是真谈不上,

偶尔可以激发你一些灵感,你没有想到的东西,给你一些提示的作用,但是你要学会分辨,

但是根本性的作用,还谈不上替代开发


一、用户代理

从上节课的思考题开始,

如果不使用浏览器,是否能够完成页面浏览?


袁老师说:

概念、原理、思想,告诉我们这个东西是怎么来的,在将来会越来越重要


因为概念原理涉及到我们对计算机、对我们工作环境了解的深度,

软件开发的本质就是人与计算机的沟通,

你对计算机了解的越深刻,沟通的成本就越低,你的价值就越高,

说的在直白一点,你的薪资待遇收入就越高,

这是最内核的东西


而像 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 地址,并按下回车


浏览器会自动解析 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>


相对路径的 ./ 是相对谁呢?

相对的是当前页面 path 中的 /news/,就是把 path 最后一段 detail.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>


官方设计 form 元素就是用来发请求的

<!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,

浏览器自动的找到 <form> 元素下面所有文本框的数据,并组织到请求体中

然后发出指定方法的请求,同时抛弃当前页面


文本框的 name 属性是属性名,

以键值对的方式 loginId=xxx&loginPwd=xxx 组织到请求体中


检查工具 Network

Preserve log 勾选,表示持续日志,不然页面刷新之前的日志就没有了

Payload 请求体 loginId=admin&loginPwd=123

Review/Response 服务器的响应结果


form 元素现在很少用,但是它仍然有用,

它最大的用处是在填写表单的时候,在任何地方一按回车就能提交,这是浏览器自带的功能,

虽然可以给文本框注册键盘监听回车事件,如果有多个文本框,要给每个文本框都要注册事情


我们希望用 from 元素回车提交的能力,但又不真正的发请求

1. 首先这两个 action、method 属性就不用写了

2. 然后注册 submit 事件监控 from 元素,然后点击提交按钮、或按回车都会触发该事情

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 表示告诉服务器,我希望接收哪些格式的内容,大部分服务器不 caer 这个

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


浏览器告诉服务器,我支持的一些压缩格式

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 表示我是谁

自我介绍,我是那个浏览器,

一些搜索爬虫,比如百度、谷歌不停的发出请求访问页面分析,会通过 User-Agent 会通过告诉是那家公司的爬虫

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


从古至今,浏览器都有一个约定:

当发送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. 自动识别响应码


浏览器能够自动识别响应码,当出现一些特殊响应码时浏览器会自动完成处理,


比如

301、302 重定向后浏览器会自动的完成跳转


示例

http://www.baidu.com

请求行和请求头

HTTP/1.1 307 Internal Redirect

Location: https://www.baidu.com/

百度用的 307 表示内部重定向

浏览器看到这种重定向的时候,会自动的充响应头里面找 Location,自动的请求 https://www.baidu.com/


2. 根据不同的响应结果做不同的处理


浏览器能够自动分析响应头中的 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');
  res.set('Content-Type', 'text/html; charset=utf-8');
  // res.set('Content-Type', 'image/png');
  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');
});


访问 http://localhost:7000/source/file.txt

服务器响应头是 Content-Type: text/plain; charset=utf-8

显示的是纯文本,<h1> 标签也按照文本显示


如果服务器改成 res.set('Content-Type', 'text/html; charset=utf-8');

重新启动服务器

再请求的时候,<h1> 标签就变成一级标题了


请求这个地址触发下载文件 http://localhost:7000/d/file.txt

Content-Disposition 响应头

attachment 附件

filename=baby.txt 下载的默认文件名


袁老师说:

通过这些范例,

url 地址跟服务器的文件( /d/file.txt )没有必然联系,

得把这个心结解开,才能认识到本质是什么,

本质就是 url 地址请求,服务器可以随便处理,想怎么处理就怎么处理


3、基本流程

浏览器自动请求自动响应的基本流程

访问:https://oss.duyiedu.com/test/index.html


输入 url 地址的时候会发生很多事情


1. 补全url地址,比如协议、端口


2. 如果地址里面有中文 ?a="中文" 中文会自动编码,通过隐射关系变成 ASCII 字符


因为请求头里面只能是 ASCII 编码,包含请求行,

行和头里面不能有中文或非 ASCII 字符,

只能用英文、数字、英文标点符号


3. 开始发生 GET 请求


拿到 HTML 文档,浏览器会丢弃旧页面,变成空白了


4. 解析新的 HTML 文档


5. 解析过程中,


发现 link 元素

请求解析并应用 css 样式


发现 img 图片

请求 img 元素


发现 script 元素

请求 script 文件,拿到后还要执行 js


6. 整个文档全部解析完成,触发 DomContentLoaded


页面上所有的资源都已加载完毕,

比如所有的图片都加载完后,触发 loaded 事件


7. 点击了 a 元素,又会重复这个流程,因为意味着新的请求


二、AJAX

浏览器本身就具备网络通信的能力,但在早期,浏览器并没有把这个能力开放给JS。


最早是微软在IE浏览器中把这一能力向JS开放,让JS可以在代码中实现发送请求,并不会刷新页面,这项技术在2005年被正式命名为AJAX(Asynchronous Javascript And XML)


AJAX 的意思,

说的直白点,就是在浏览器里面通过 js 发出请求,获取响应结果的能力


所以说,现在用 form 来发请求没有什么意义了,它要刷新,

用 js 发出请求然后接收响应,不刷新页面用户体验更好,AJAX 就是这样的能力


AJAX 就是指在 web 应用程序中异步的想服务器发送请求,它目前有两套 api

1. XMLHttpRequest 简称 XHR 比较久远

2. Fetch H5出来的

功能点XHRFetch
基本的请求能力
基本的获取响应能力
监控请求进度
监控响应进度
Service Worker中是否可用
控制cookie的携带
控制重定向
请求取消
自定义referrer
API风格EventPromise
活跃度停止更新不断更新


袁老师说这些目前还没讲

Service Worker 

cookie

自定义referrer


XHR 跟 Fetch 的风格不一样

XHR 是基于事件风格的,就是 addEventlistener、on什么什么发生的时候处理什么函数

Fetch 是基于 2015 ES6 的 Promise 风格


Fetch API 基本上能够涵盖 XHR,

但是有一个地方取代不了,就是请求进度监控,这一块在上传文件的时候特别有用,

不是官方做不到,因为基于 Promise 这套 API 不好设计


三、实战

1、fetch

第一个参数写 url 地址,就可以发出了一个 GET 请求

fetch('https://study.duyiedu.com/api/herolist');

运行 js 代码的时候就发出一个请求

Network -> 选中 herolist -> Preview 看到美化过的响应体


怎么在 js 里面拿到响应结果?

fetch 返回的是一个 promise,

可以使用 await fetch(url) 等待,等待完成后返回的是一个“响应对象“

async function getHero(){

  const resp = await fetch('https://study.duyiedu.com/api/herolist');
  console.log(resp);

  for (let header of resp.headers.entries()) {
    console.log(header);
  }

  console.log(resp.headers.get('content-type')); // application/json; charset=utf-8

}

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() 方法拿到所有的响应头


entries() 方法是一个迭代器,可以用 for of 循环

浏览器不会给我们全部的响应头,只给我们部分响应头信息

['content-length', '28247'] 表示响应体的字节数

['content-type', 'application/json; charset=utf-8'] 响应体的类型


如何获取到响应体?

我们想这样一个问题:

如果服务器响应的内容特别巨大,有十个G,网络传输需要时间,

所以浏览器的处理方式是,

响应头是很小的,只要能拿到响应头 promise 就完成了,浏览器不管后面一大堆的响应体还有多少


所以现在拿不到响应体,可能响应体还没有传输完,

拿到响应头了,响应体还要等待传输完成以及解析完成


要拿响应体,需要告诉浏览器用那种方式拿响应体

resp.json() json格式解析响应体

resp.text() 纯文本格式解析响应体

resp.blob() 二进制的方式


具体要看服务器给我们响应的是什么,一般用 josn 的方式

async function getHero(){
  const resp = await fetch('https://study.duyiedu.com/api/herolist');
  const body = await resp.json(); // 这里又要等待,等待用json格式解析后面的传过来的数据
  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 heros = body.data.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('');
  const ul = document.querySelector('.list');
  ul.innerHTML = heros;
}

getHero();

</script>
</body>
</html>


2、上传文件

1. 两个操作界面的辅助函数


F12 检查 -> console 测试下面两个函数的效果


showArea 函数负责切换显示区域,有三个区域

showArea('select') 初始的界面,选择文件的区域

showArea('progress') 上传中...上传进度条的区域,进度条背景是一张预览图

showArea('result') 结果区域,右上角有一个关闭按钮


就是切换 div 的显示\隐藏,跟文件的上传没有关系


setProgress 函数设置进度条

showArea('progress') 先切换到进度条区域,在通过 setProgress 函数设置

setProgress(10) 进度条变成 10%

setProgress(50) 进度条变成 50%


结合这两个函数完成文件上传,

上节课说过,

上传文件的本质就是 HTTP 协议的 POST 请求,把指定的格式发过去,然后返回结果


2. 首先如何选择要上传的文件

<!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>


上传文件就是 <input type="file"> 元素

当我们点击了 .upload-select 虚线框框,就相当于点击了里面的 <input type="file">

因此给 .upload-select 注册一个 click 点击事件


点击外面的虚线框框,就相当点击 input

因此 doms.selectFile.click() 点击后,可以弹出选择上传文件的对话框


怎么知道 <input type="file"> 选择了文件呢?

我们要监控 input 元素,注册 change 事件监控选择的文件


现在还没有到 ajax,都是在做事件交互,

ajax 唯一的作用是发请求,获取响应


2. 如何获取上传文件的二进制数据

// 监控input元素
doms.selectFile.onchange = function(){
  const file = this.files;
  console.dir(file)
}


这部分不是 ajax 的内容,

<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 请求了

// 监控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);
}


因为要监控请求的进度,所以用 XMLHttpRequest() 的方式请求


配置请求

请求方法 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


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


请求发过去了,图片上传成功了,

下面做上传进度,需要切换进度到的界面


4. 上传进度

doms.selectFile.onchange = function(){
  const file = this.files[0];
  const xhr = new XMLHttpRequest();

  xhr.upload.addEventListener("progress", (e) => {
    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 无关了


5. 上传文件中,当前的预览图片的显示

// 监控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 赋值


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);
  });
  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) 表示请求取消了


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) => { // 响应完成切换界面
    
  });
  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');
}



Leave a comment 0 Comments.

Leave a Reply

换一张