彻底掌握基于HTTP网络层的“前端性能优化”
1、产品性能优化方案
- HTTP网络层优化
- 代码便一层优化 webpack
- 代码运行层优化 html/css + javascript + vue + react
- 安全优化 xss + csrf
- 数据埋点及性能监控
- .........
2、CRP(Critical [ˈkrɪtɪkl] Rendering [ˈrendərɪŋ] Path)关键渲染路径
3、从输入URL地址到看到页面,中间都经历了啥
正常的步骤:【第一次访问页面】
- URL解析
- DNS解析:找到服务器外网IP
- TCP三次握手:找到服务器并且建立好通信的通道
- 通信
- TCP四次挥手:关闭通道
- 渲染:客户端把从服务器获取的信息进行渲染
第二次访问,我们休要保证比第一次更快【我们需要基于缓存机制来完成】
- URL解析
- 缓存检查(第二次访问增加缓存检查)
- DNS解析 找到服务器外网IP
- TCP三次握手:找到服务器并且建立好通信的通道
- 通信
- TCP四次挥手:关闭通道
- 渲染:客户端把从服务器获取的信息进行渲染
输入域名到DNS服务器,通过DNS服务器获取到外网IP,客户端通过获取到的外网IP找到该服务器
第一步:URL解析
-
地址解析
传输协议:基于它实现客户端和服务器端的数据通信「快递小哥」 >
http 超文本传输协议(除了传输文本外,还可以传输音视频、图片等富文本资源)
https 经过SSL加密处理的HTTP,所以更安全「涉及支付类的网站都是使用https」
ftp 一般用于把本地的文件上传到服务器
域名:
- 顶级域名:qq.com
- www.qq.com 一级域名
- sports.qq.com 二级域名
- kbs.sprots.qq.com 三级域名
端口号:区分同一台服务器上部署的不同的项目【服务】 取值范围:0~65535之间
http协议:默认端口号 80 浏览器设置 www.baodu.com:80/
https协议:443
ftp协议:21
浏览器地址栏中输入的URL地址,如果我们不自己写端口号,浏览器会根据传输协议,帮助我们自动把端口号加上
问号参数实现信息传输:
- 客户端基于”问号参数“可以把信息传递给服务器「GET系列请求经常这样干」(客户端 -> 服务器)
- 客户端两个页面之间(或者两个组件之间)也可以基于”问号参数“方式,实现信息传输「例如:列表到详情」(A页面 -> B页面)
- .....
哈希值(HASH值)「俗称:#xxx」
- 锚点定位
- 基于HASH值实现哈希路由
- .....
-
编码
如果URL地址中出现了中文以及某些特殊符号,为了防止传输过程中乱码,则需要进行加密(对称加密)/解密
对整个URL加密:encodeURI & decodeURI 【会对空格以及中文汉字等特殊值进行编码】
let apiURL = `http://api.zhufeng.cn/user/list?name="阿松大"&age=20` encodeURI(apiURL) //"http://api.zhufeng.cn/user/list?name=%22%E9%98%BF%E6%9D%BE%E5%A4%A7%22&age=20"对URL查询字符串中传递的值进行单独加密:encodeURIComponent/decodeURIComponent 在encodeURI的基础上,还可以对“://?#@”等特殊符号进行加密,所以不用其处理整个URL,只是处理传递参数的一部分值而已!!
let apiURL = `http://api.zhufeng.cn/user/list?from=${encodeURIComponent('微信')}&to=${encodeURIComponent('http://www.zhufeng.cn/stu/index.html')}`; encodeURI(apiURL) // http://api.zhufeng.cn/user/list?from=%E5%BE%AE%E4%BF%A1&to=http%3A%2F%2Fwww.zhufeng.cn%2Fstu%2Findex.htmlescape & unescape:用于客户端A页面和B页面之间通信中内容编码
第二步:缓存检查
如果设定了缓存机制,则从服务器获取的信息会存储在:
缓存位置:
- Memory Cache:内存缓存
- Disk Cache:硬盘缓存
物理内存可以持久存储,但是虚拟内存在页面关闭后,存储的信息就都释放掉了
页面关闭再打开:查找 disk cache 中是否有匹配,如有则使用,如没有则发送网络请求
普通刷新(F5):因TAB没关闭,因此 memory cache是可用的,会被优先使用,其次才是 disk cache
强制刷新(Ctrl + F5):浏览器不使用缓存,因此发送的请求头部据带有 Cache-control:no-cache,服务器直接返回200和最新内容
真实项目中,哪些东西需要从服务器获取?
- 资源文件 html/css/js/img...
- 强缓存
- 协商缓存 304
- 数据信息 api
- 自己处理数据临时存储【cookie、localStorage、sessionStorage、vuex/redux....】
- 从DNS服务器获取外网IP【他也有自己的缓存】
一般情况下,对于资源文件,我们强缓存和协商缓存会共同设置【有的只设置其中一种】;如果都设置了:缓存检查的时候,先看看强缓存是否生效【是否缓存、是否过期】,强缓存生效则使用强缓存存储的信息处理;如果不生效,再去看协商缓存是否生效【协商缓存是对强缓存的一种补充】,生效则使用,如果还是不生效,则从服务器重新获取。
强缓存 Expires / Cache-Control
只要本地缓存生效(有且未过期)则使用本地缓存的信息,不会向服务器发送请求;本地缓存失效才会从服务器重新获取最新的内容!!
- 强缓存由服务器设置,客户端浏览器去执行,无需前端写啥代码
- 服务器再返回给客户端信息的时候,在“响应头”中携带 Cache-Control(或者Expires) 字段;客户端浏览器拿到这个字段后,则按照规则去缓存获取的信息及标识!!
- 每当客户端发送请求的时候,都去看一下缓存是否生效;生效则直接获取缓存的数据,不生效再从服务器获取....
- 无论是从服务器获取的,还是从缓存中读取的,HTTP状态码都是以200为主
Expires是HTTP/1.0版本使用的,存储的是具体的过期时间 Cache-Control是HTTP/1.1版本使用的,可以基于 max-age=2592000 指定过期时间,单位是秒 两者都有则以最高支持的版本为主!!
HTML页面是不能做强缓存的:
html页面时项目的渲染入口,假设html也做了强缓存,设置过期时间为30天;那么以后30天内,我们访问页面,用的都是缓存的内容,即使服务器更新,客户端也不会看到最新的
HTML页面“绝对不能”设置强缓存,否则无法保证服务器资源更新,客户端可以随时获取最新的信息!!
清除强缓存的副作用
可使用拼接时间戳实现页面更新(src:文件?时间戳)
浏览器对于清缓存的处理:根据第一次请求资源时返回的响应头来确定的
- Expires:缓存过期时间,用来指定资源到期的时间(HTTP/1.0)
- Cache-Control:cache-control:max-age=2592000 第一次拿到资源后的2592000秒内(30天),再次发送请求,读取缓存中的信息(HTTP/1.1)
- 两者同时存在的话,Cache-Control优先级高于Expires
协商缓存 Last-Modified / ETag
每一次请求,都需要和服务器进行协商「看服务器资源是否更新,如果有更新,则直接获取最新的,如果没有更新,则获取缓存」
协商缓存就是强制缓存失效后,浏览器携带缓存表示向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程
缓存标识:ETag(HTTP1.1) / last-modified(HTTP1.0)
服务器资源只要更新:
- 都会产生一个新的Etag标识值 例如:W/"5e8c1c8a-23239"
- 服务器资源最后更新时间,存储到Last-Modified中 例如:Tue, 07 Apr 2020 06:24:10 GMT
每一次都向服务器发送请求,在请求头中携带 If-None-Match「存储的是Etag的值」(或者 If-Modified-Since「存储的是Last-Modified值」)给服务器;服务器接收到请求,会拿传递过来的值和服务器上资源最后更新的标识和时间做对比:
- 一样:说明服务器资源距离上次缓存期间并没有更新过,此时服务器只需要返回 304 状态码即可(没有返回资源信息);客户端接收到服务器的反馈结果,发现是304,则从缓存信息中获取内容渲染!!
- 不一样:说明资源更新了,则服务器返回最新的资源信息以及相关的标识「Etag或者Last-Modified」;客户端会把最新的信息以及标识重新缓存在本地!!
每一次请求都需要问服务器是否更新,所以可以保证随时获取最新的信息;但是不如强缓存效率高!!
真实项目中,我们一般是:
HTML只做协商缓存
其余的资源即做强化存也做协商缓存,这样在强缓存失效后,还可以基于协商缓存二次进行处理!!
两种缓存都是“服务器设置”,客户端浏览器自动执行,无需前端编写代码;而且都是对静态资源文件的缓存处理!!
数据缓存
把不需要经常更新的数据接口,做缓存处理
需求:本地有缓存数据且未过期,则从本地获取;本地缓存失效,则重新从服务器获取「类似于强缓存」;
// 首先校验本地缓存是否生效 let newsBefore = localStorage.getItem("newsBefore"); if (newsBefore) { newsBefore = JSON.parse(newsBefore); if (+new Date() - newsBefore.time < 3600000) { console.log("成功「缓存」:", newsBefore.data); return; } } // 本地缓存失效:从服务器获取 & 存储到本地 let result = await this.$api.queryNewsBefore("20211226"); console.log("成功「服务器」:", result); localStorage.setItem( "newsBefore", JSON.stringify({ time: +new Date(), data: result, }) );封装成方法
// 基于localStorage实现数据缓存 // + func:这个方法执行,可以向服务器发送请求,返回promise实例,并且根据请求结果决定实例的状态 // + name:localStorage存储信息时的key // + limit:有效期(单位毫秒)「默认1小时 3600000」 export const cacheStorage = function cacheStorage(func, name, limit) { if (typeof func !== "function") throw new TypeError("func is not a function~"); if (typeof name !== "string") throw new TypeError("name is not a string~"); if (typeof limit !== "number" || isNaN(limit)) limit = 3600000; return new Promise(async (resolve, reject) => { let result = localStorage.getItem(name), now = +new Date(); if (result) { let { time, data } = JSON.parse(result); if (now - time < limit) { // 缓存有效 resolve(data); return; } } // 缓存失效 try { result = await func(); localStorage.setItem(name, JSON.stringify({ time: +new Date(), data: result })); resolve(result); } catch (err) { reject(err); } }); };调用
<template> <div id="app">珠峰培训</div> </template> <script> import { cacheStorage } from "@/assets/utils"; export default { name: "App", async created() { let result = await cacheStorage( () => this.$api.queryNewsBefore("20211226"), "newsBefore" ); console.log("成功「往日」", result); result = await cacheStorage(() => this.$api.queryNewsLatest(), "newsLaest"); console.log("成功「今日」", result); }, }; </script>
第三步:DNS解析
- 递归查询
- 迭代查询
每一次DNS解析时间预计在20~120毫秒
- 减少DNS请求次数
- DNS预获取(DNS Prefetch)
服务器拆分的优势
- 资源的合理利用
- 抗压能力加强
- 提高HTTP并发
- ......
第四步:TCP三次握手
TCP:稳定、可靠的通信机制 UDP:快速、不稳定的通信机制(直播流可使用)
- sql序号,用来标识从TCP源端想目的端发送的字节流,发起方发送数据对此进行标记
- ack确认序号,只有ACK标志位为1时,确认序号字段才有效,ack=seq+1
- 标志位
- ACK:确认序号有效
- RST:重置连接
- SYN:发起一个新的连接
- FIN:释放一个连接
- ......
三次握手为什么不用两次,或者四次?
如果只是用两次,则无法保证通信的顺畅;如果是用四次,第四次时多余的,减少不必要的性能浪费;
TCP作为一种可靠传输控制协议,其核心思想:既要保证数据可靠传输,又要提高传输的效率!
第五步:数据传输
- HTTP报文
- 请求报文
- 响应报文
- 响应状态码
- 200 OK
- 202 Accepted :服务器已接受请求,但尚未处理(异步)
- 204 No Content:服务器成功处理了请求,但不需要返回任何实体内容
- 206 Partial Content:服务器已经成功处理了部分 GET 请求(断点续传 Range/If-Range/Content-Range/Content-Type:”multipart/byteranges”/Content-Length….)
- 301 Moved Permanently
- 302 Move Temporarily
- 304 Not Modified
- 305 Use Proxy
- 400 Bad Request : 请求参数有误
- 401 Unauthorized:权限(Authorization)
- 404 Not Found
- 405 Method Not Allowed
- 408 Request Timeout
- 500 Internal Server Error
- 503 Service Unavailable
- 505 HTTP Version Not Supported
- ……
第六步:TCP四次挥手
在TCP通道建立好之后,客户端把信息给了服务器的时候,由客户端主动发送释放连接的请求
为什么连接的时候是三次握手,关闭的时候却是四次握手?
- 服务器端收到客户端的SYN连接请求报文后,可以直接发送 SYN + ACK 报文
- 但关闭连接时,当服务器端收到FIN报文时,很可能并不会立即关闭链接,所以只能先回复一个ACK报文,告诉客户端:”你发的FIN报文我收到了”,只有等到服务器端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送,故需要四步握手。
保持:TCP通道持久化 Connection: keep-alive
第七步:页面渲染
基于JS管理的本地存储方案:
控制台->Application可以查看「都是以明文形式存储,所以重要隐秘的信息尽可能不要存储;非要存储一定要加密处理! 存储到本地的信息都是以字符串形式存储的!!」
@1 cookie
操作:document.cookie 实现获取和设置
- 具备有效期:我们在设置cookie信息的时候可以设置有效期;在有效期内,不论页面是刷新还是关闭重新打开,存储的cookie信息都在
- 受“源”和“浏览器”限制:cookie信息只允许同源访问、而且更换浏览器后也无法获取
- 存储大小有限制:同源下最多只允许存储4KB
- 不稳定:清理电脑垃圾(或者清除浏览器历史记录)可以选择性把存储的cookie都干掉、浏览器隐私模式(无痕浏览器模式)下是禁止设置cookie的
- 和服务器之间有“猫腻儿”:服务器在响应头中携带set-cookie字段,客户端浏览器会自动设置对应的cookie;客户端只要本地有cookie信息,不论服务器是否需要,都会基于请求头Cookies字段,把cookie传递给服务器!「所以:本地cookie存储的越多,每一次向服务器发送请求传送的东西也就越多,速度也就越慢!!」
@2 localStorage
操作:localStorage.setItem([key],[value]) localStorage.getItem([key]) localStorage.removeItem([key]) localStorage.clear()清除所有 ...
- 持久化存储:存储的信息只要不手动移除,会一直存在
- 受“源”和“浏览器”限制
- 存储大小有限制:同源下最多可以允许存储5MB
- 稳定:清除电脑垃圾或者历史记录对localStorage存储的信息没有影响,而且无痕模式下也可以设置信息!
- 和服务器之间毫无关联:除非自己手动的把本地存储的信息传递给服务器,否则和服务器没关系
@3 sessionStorage
和localStorage只有一个区别
- localStorage是持久存储,页面刷新或者关闭,存储的信息还在
- sessionStorage是会话存储,页面刷新存储的信息在,但是页面一但关闭(会话结束),存储的信息都会释放
@4 IndexDB @5 webSQL
以上本地存储方案,都是存储到计算机物理内存中的;但是还有一些存储方案是存储在虚拟内存中的: 特点:页面刷新(或者页面关闭),之前存储的信息都会被释放掉
- 全局变量
- vuex / redux
4、性能优化汇总
1、利用缓存
- 对于静态资源文件实现强缓存和协商缓存(扩展:文件有更新,如何保证及时刷新?)
- 对于不经常更新的接口数据采用本地存储做数据缓存(扩展:cookie / localStorage / vuex|redux 区别?)
2、DNS解析
- 分服务器部署,增加HTTP并发性(导致DNS解析变慢)
- DNS Prefetch
3、TCP的三次握手和四次挥手
- Connection:keep-alive
4、数据传输
- 减少数据传输的大小
- 内容或者数据压缩(webpack等)
- 服务器端一定要开启GZIP压缩(一般能压缩60%左右)
- 大批量数据分批次请求(例如:下拉刷新或者分页,保证首次加载请求数据少)
- 减少HTTP请求的次数
- 资源文件合并处理
- 字体图标
- 雪碧图 CSS-Sprit
- 图片的BASE64
- .....
5、CDN服务器“地域分布式”
6、采用HTTP2.0
网络优化是前端性能优化的中的重点内容,因为大部分的消耗都发生在网络层,尤其是第一次页面加载,如何减少等待时间很重要“减少白屏的效果和时间”
- LOADDING 人性化体验
- 骨架屏:客户端骨屏 + 服务器骨架屏
- 图片延迟加载
- .....
5、HTTP1.0 VS HTTP1.1 VS HTTP2.0
1、HTTP1.0和HTTP1.1的一些区别
缓存处理,HTTP1.0中主要使用 Last-Modified,Expires 来做为缓存判断的标准,HTTP1.1则引入了更多的缓存控制策略:ETag,Cache-Control…带宽优化及网络连接的使用,HTTP1.1支持断点续传,即返回码是206(Partial Content)错误通知的管理,在HTTP1.1中新增了24个错误状态响应码,如409(Conflict)表示请求的资源与资源的当前状态发生冲突;410(Gone)表示服务器上的某个资源被永久性的删除…Host头处理,在HTTP1.0中认为每台服务器都绑定一个唯一的IP地址,因此,请求消息中的URL并没有传递主机名(hostname)。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个IP地址。HTTP1.1的请求消息和响应消息都应支持Host头域,且请求消息中如果没有Host头域会报告一个错误(400 Bad Request)长连接,HTTP1.1中默认开启Connection: keep-alive,一定程度上弥补了HTTP1.0每次请求都要创建连接的缺点
2、HTTP2.0和HTTP1.X相比的新特性
新的二进制格式(Binary Format),HTTP1.x的解析是基于文本,基于文本协议的格式解析存在天然缺陷,文本的表现形式有多样性,要做到健壮性考虑的场景必然很多,二进制则不同,只认0和1的组合,基于这种考虑HTTP2.0的协议解析决定采用二进制格式,实现方便且健壮header压缩,HTTP1.x的header带有大量信息,而且每次都要重复发送,HTTP2.0使用encoder来减少需要传输的header大小,通讯双方各自cache一份header fields表,既避免了重复header的传输,又减小了需要传输的大小服务端推送(server push),例如我的网页有一个sytle.css的请求,在客户端收到sytle.css数据的同时,服务端会将sytle.js的文件推送给客户端,当客户端再次尝试获取sytle.js时就可以直接从缓存中获取到,不用再发请求了
// 通过在应用生成HTTP响应头信息中设置Link命令
Link: </styles.css>; rel=preload; as=style, </example.png>; rel=preload; as=image
多路复用(MultiPlexing)
- HTTP/1.0 每次请求响应,建立一个TCP连接,用完关闭
- HTTP/1.1 「长连接」 若干个请求排队串行化单线程处理,后面的请求等待前面请求的返回才能获得执行机会,一旦有某请求超时等,后续请求只能被阻塞,毫无办法,也就是人们常说的线头阻塞;
- HTTP/2.0 「多路复用」多个请求可同时在一个连接上并行执行,某个请求任务耗时严重,不会影响到其它连接的正常执行;
非同源策略(跨域)请求的处理方案
http://127.0.0.1:5500/index.html
- 自己本地启动服务【iis、nginx...】
- node
- webpack-dev-server
- vscod LiveServer 插件
file:///Users/zhouxiaotian/Documents/.../index.html
- file协议不允许发送ajax请求
平时项目中,同源请求的需求很少
- 服务器部署的时候是分开的【web服务器、数据服务器....】
- 调用第三方平台的接口
- 本地开发的时候,在自己电脑上启动预览项目,而数据接口是请求其他服务器的
开发的时候跨域;部署的时候,是部署到同一台服务器的同一个服务下【生产同源】;
前后端不分离的项目,开发的时候本地也会把后台服务器启动,所以开发也是同源,部署到服务器也是同源
全栈开发,基于SSR渲染【js+node.js】,这也是同源的
1、聊聊对跨域的理解
前端开发进化史
- 服务器渲染【半服务器渲染 SSR】
- 客户端渲染
为啥会产生跨域
- 服务器分离:WEB服务器、数据服务器、照片服务器....
- 云信息共享:第三方API接口
- 有助于分离开发:开发跨域、部署同源
- ....
解决方案
-
修改本地HOST【现在基本不用】
修改本地HOST:开发的时候是跨域的,但是部署的时候是同源的,所以我们只需要解决开发跨域的问题即可
- DNS解析 -> 找本地的DNS缓存记录【本地host文件中去查找】
- 客户端浏览器地址栏输入www.zhufeng.cn
- hosts配置:www.zhufeng.cn:80 127.0.0.1:80
- 这样保证了地址栏是www.zhufeng.cn,但是访问的是本地开发的这个项目,在这个基础上,我们去 www.zhufeng.cn/user/list 发送请求,相当于欺骗了浏览器,让浏览器认为我们是同源的
-
JSONP
-
CORS
-
Proxy
-
....
2、JSONP
script标签的src请求资源文件【基于GET请求方式】,不存在跨域限制
JSONP的原理利用的就是这个机制
JSONP:必须需要服务器的支持,只能是GET请求
axios、fetch...默认基于promise处理,所以我们自己封装的jsonp函数,也要基于promise管理
JSONP测试代码
<!DOCTYPE html> <html> <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>珠峰在线Web高级正式课「为大厂而生」</title> </head> <body> <script> (function() { window['fn'] = function fn(result) { console.log(result); }; })(); </script> <script src="https://www.baidu.com/sugrec?prod=pc&wd=珠峰&cb=fn"></script> </body> </html>封装JSONP函数-jsonp.js
(function() { // 检测是否为纯粹对象 const isPlainObject = function isPlainObject(obj) { let proto, Ctor; if (!obj || Object.prototype.toString.call(obj) !== "[object Object]") return false; proto = Object.getPrototypeOf(obj); if (!proto) return true; Ctor = proto.hasOwnProperty('constructor') && proto.constructor; return typeof Ctor === "function" && Ctor === Object; }; // 把普通对象变为URLENCODED格式字符串 const stringify = function stringify(obj) { let str = ``, keys = Object.keys(obj).concat(Object.getOwnPropertySymbols(obj)); keys.forEach(key => { str += `&${key}=${obj[key]}`; }); return str.substring(1); }; /* 封装JSONP函数 */ const jsonp = function jsonp(url, config) { return new Promise((resolve, reject) => { // 初始化参数 if (typeof url !== "string") throw new TypeError('url is not a string!'); if (!isPlainObject(config)) config = {}; config = Object.assign({ params: null, jsonp: 'callback' }, config); // 创建一个全局函数 let f_name = `jsonp${+new Date()}`; window[f_name] = value => { // 请求成功 resolve(value); delete window[f_name]; document.body.removeChild(script); }; // 处理URL「拼接问号参数 & 拼接函数名」 let params = config.params; if (params) { if (isPlainObject(params)) params = stringify(params); url += `${url.includes('?')?'&':'?'}${params}`; } url += `${url.includes('?')?'&':'?'}${config.jsonp}=${f_name}`; // 发送请求 let script = document.createElement('script'); script.src = url; script.onerror = err => { // 请求失败 reject(err); }; document.body.appendChild(script); }); }; /* 暴露API */ if (typeof module === "object" && typeof module.exports === "object") module.exports = jsonp; if (typeof window !== "undefined") window.jsonp = jsonp; })();接口-serve.js
/*-CREATE SERVER-*/ const express = require('express'), app = express(); app.listen(1001, () => { console.log(`THE WEB SERVICE IS CREATED SUCCESSFULLY AND IS LISTENING TO THE PORT:1001`); }); app.get('/user/list', (req, res) => { let { callback } = req.query; // callback存储的就是客户端传递的全局函数名 let result = { code: 0, data: ['张三', '李四'] }; // 返回给客户端指定的格式 res.send(`${callback}(${JSON.stringify(result)})`); }); /* STATIC WEB */ app.use(express.static('./'));调用
<!DOCTYPE html> <html> <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>珠峰在线Web高级正式课「为大厂而生」</title> </head> <body> <script src="jsonp.js"></script> <script> jsonp('https://www.baidu.com/sugrec', { params: { prod: 'pc', wd: '哈哈哈' }, jsonp: 'cb' }).then(value => { console.log(value); }); jsonp('http://127.0.0.1:1001/user/list').then(value => { console.log(value); }); </script> </body> </html>
服务器端代码
app.get('/list', (req, res) => {
let {
callback = Function.prototype
} = req.query;
let data = {
code: 0,
message: '珠峰培训'
};
res.send(`${callback}(${JSON.stringify(data)})`);
});
客户端处理
(function () {
const jsonp = function jsonp(config) {
config == null ? config = {} : null;
typeof config !== "object" ? config = {} : null;
let {
url,
params = {},
jsonpName = 'callback',
success = Function.prototype
} = config;
// 自己创建一个全局的函数
let f_name = `jsonp${+new Date()}`;
window[f_name] = function (result) {
typeof success === "function" ? success(result) : null;
delete window[f_name];
document.body.removeChild(script);
};
// 处理URL
params = Qs.stringify(params);
if (params) url += `${url.includes('?')?'&':'?'}${params}`;
url += `${url.includes('?')?'&':'?'}${jsonpName}=${f_name}`;
// 发送请求
let script = document.createElement('script');
script.src = url;
// script.onerror = () => {};
document.body.appendChild(script);
};
if (typeof window !== "undefined") {
window.jsonp = jsonp;
}
})();
3、CORS
CORS 跨域资源共享:只需要服务器设置允许资源即可,允许当前客户端发请求,这样就可以避开浏览器的安全策略
服务器设置:Access-Control-Allow-Origin
- 设置为“*”,允许所有资源访问【不安全】,不允许携带资源凭证【例如:cookie】,所以Access-Control-Allow-Origin必须为false
- 不设置“*”,只能设置“单一原”,但是这样可以携带资源凭证
我们自己会搞一套白名单
客户端向服务器发送请求
serve-CORS.js
/*-CREATE SERVER-*/ const express = require('express'), app = express(); app.listen(1001, () => { console.log(`THE WEB SERVICE IS CREATED SUCCESSFULLY AND IS LISTENING TO THE PORT:1001`); }); /*-MIDDLE WARE-*/ // 设置白名单 let safeList = ["http://127.0.0.1:5500", "http://127.0.0.1:3000", "http://127.0.0.1:8080"]; app.use((req, res, next) => { let origin = req.headers.origin || req.headers.referer || ""; origin = origin.replace(/\/$/g, ''); origin = !safeList.includes(origin) ? '' : origin; res.header("Access-Control-Allow-Origin", origin); res.header("Access-Control-Allow-Credentials", true); res.header("Access-Control-Allow-Headers", "Content-Type,Content-Length,Authorization, Accept,X-Requested-With"); res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS,HEAD"); req.method === 'OPTIONS' ? res.send('OK') : next(); }); /*-API-*/ app.get('/list', (req, res) => { res.send({ code: 0, message: 'zhufeng' }); }); /* STATIC WEB */ app.use(express.static('./'));调用接口
axios.get('http://127.0.0.1:1001/list', { withCredentials: true // 允许携带资源凭证 }) .then(response => { console.log(response); });
服务器端代码
/*-MIDDLE WARE-*/
let safeList = ["http://127.0.0.1:5500", "http://127.0.0.1:3000", "http://127.0.0.1:8080"];
app.use((req, res, next) => {
/*
* Allow-Origin:
* + 单一源
* + * 所有源「但是此时不安全,而且不允许携带资源凭证」
* 设置白名单
*/
let origin = req.headers.origin || req.headers.referer || "";
origin = origin.replace(/\/$/g, '');
origin = !safeList.includes(origin) ? '' : origin;
res.header("Access-Control-Allow-Origin", origin);
// res.header("Access-Control-Allow-Credentials", true);
// res.header("Access-Control-Allow-Headers", "Content-Type,Content-Length,Authorization, Accept,X-Requested-With");
// res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS,HEAD");
// CORS跨域资源共享的时候
// + 在发送真实的请求之前,浏览器会先发送一个试探性的请求 OPTIONS「目的:测试客户端和服务器之间是否可以正常的通信」,如果可以正常通信,接下来在发送真实的请求信息!!
req.method === 'OPTIONS' ? res.send('OK') : next();
});
/*-API-*/
app.get('/list', (req, res) => {
res.send({
code: 0,
message: '珠峰培训'
});
});
4、Proxy
原理:利用后端和后端通信默认是没有安全策略限制的
中间代理的服务器:
- 开发环境
- node.js自己写
- vue/react -> webpack-dev-server
- 生产环境
- nginx反向代理
- ....
作用:
- 预览web页面
- 帮助我们从其他服务器获取数据
server-proxy.js
/*-CREATE SERVER-*/ const express = require('express'), app = express(); app.listen(1001, () => { console.log(`THE WEB SERVICE IS CREATED SUCCESSFULLY AND IS LISTENING TO THE PORT:1001`); }); // 代理 const request = require('request'); app.get('/asimov/subscriptions/recommended_collections', (req, res) => { let jianURL = `https://www.jianshu.com${req.url}`; req.pipe(request(jianURL)).pipe(res); }); /* STATIC WEB */ app.use(express.static('./'));调用
axios.get('/asimov/subscriptions/recommended_collections') .then(response => { console.log(response.data); });
webpack-dev-server
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'production',
entry: './src/main.js',
output: {
filename: 'main.[hash].min.js',
path: path.resolve(__dirname, 'build')
},
devServer: {
port: '3000',
compress: true,
open: true,
hot: true,
proxy: {
'/': {
target: 'http://127.0.0.1:3001',
changeOrigin: true
}
}
},
// 配置WEBPACK的插件
plugins: [
new HtmlWebpackPlugin({
template: `./public/index.html`,
filename: `index.html`
})
]
};
vue.config.js
module.exports = {
devServer: {
//proxy: '<url>'
proxy: {
'/api': {
target: '<url>',
ws: true,
changeOrigin: true
}
}
}
}
部署到服务器上:基于nginx实现反向代理
处理原理
自己基于node实现
const request = require('request');
app.get('/subscriptions/recommended_collections', function (req, res) {
let url = 'https://www.jianshu.com/asimov' + req.url;
req.pipe(request(url)).pipe(res);
});
5、扩展:其他跨域方案【配合iframe】
- postMessage
- window.name
- document.domin
- location.hash
- ……
proxy用的最多,cors用的也不少,jsonp偶尔用,其余方案基本不用
AJAX核心基础
json格式字符串
[{ "name": "珠峰培训", "age": 12 }, { "name": "珠峰培训", "age": 12 }]xml格式字符串
<!DOCTYPE xml> <root> <item> <name> 珠峰培训 </name> <age> 12 </age> </item> <item> <name> 珠峰培训 </name> <age> 12 </age> </item> </root>
AJAX核心:基于XMLHttpRequest创建HTTP请求
创建xhr实例
打开一个URL地址「发送请求前的一些配置信息」
method 请求方式:GET(get/delete/head/options...) / POST(post/put/patch...)
GET
- get:从服务器拿数据,或给服务器传输数据;一般给服务器拿的少,从服务器拿的多
- delete:一般应用于把服务器上的文件删除掉
- head:只想从服务器获取响应头信息,但不想获得相应主体信息;使用后服务器可能返回204
- options
PSOT
- post:给服务器推送信息,一般只返回一个结果;给服务器多,从服务器拿的少;
- put:想在服务器上存放一个文件,或者存放大量文件
- patch
GET和POST在官方定义中是没有明确的区别的,但是浏览器或者开发的时候,都有一套约定俗成的规范:
GET请求传递给服务器的信息,除了请求头传递以外,要求基于URL问号传参传递给服务器
xhr.open('GET', './1.json?lx=1&name=xxx')POST请求要求传递给服务器的信息,是基于请求主体传递
xhr.send('lx=1&name=xxx')面试题:GET和POST的区别
- 1)GET传递的信息不如POST多,因为URL有长度限制「IE->2KB」,超过这个长度的信息会被自动截掉,这样导致传递内容过多,最后服务器收到的信息是不完整的!!POST理论上是没有限制的,但是传递的东西越多,速度越慢,可能导致浏览器报传输超时的错误,所以实际上我们会自己手动做限制!!
- 2)GET会产生缓存「浏览器默认产生的,不可控的缓存」:两次及以上,请求相同的API接口,并且传递的参数也一样,浏览器可能会把第一次请求的信息直接返回,而不是从服务器获取最新的信息!!
//在请求URL的末尾设置随机数,以此来清除GET缓存的副作用 xhr.open('GET', './1.json?lx=1&name=xxx&_'+Math.random())
- 3)POST相对于GET来讲更安全一些:GET传递的信息是基于URL末尾拼接,这个随便做一些劫持或者修改,都可以直接改了,而POST请求主体信息的劫持,没那么好做!!但是“互联网面前,人人都在裸奔”!!所以不管什么方式,只要涉及安全的信息,都需要手动加密「因为默认所有的信息传输都是明文的」!!
url 请求的URL地址
async 是否采用异步 默认是TRUE
username
userpass
let xhr = new XMLHttpRequest; xhr.open('GET', './1.json');监听请求的过程,在不同的阶段做不同的处理「包含获取服务器的响应信息」
- ajax状态 xhr.readyState
- 0 UNSENT
- 1 OPENED
- 2 HEADERS_RECEIVED 响应头信息已经返回
- 3 LOADING 响应主体信息正在处理
- 4 DONE 响应主体信息已经返回
- HTTP状态码 xhr.status/xhr.statusText
- 200 OK
- 202 Accepted :服务器已接受请求,但尚未处理(异步)
- 204 No Content:服务器成功处理了请求,但不需要返回任何实体内容
- 206 Partial Content:服务器已经成功处理了部分 GET 请求(断点续传 Range/If-Range/Content-Range/Content-Type:”multipart/byteranges”/Content-Length….)
- 301 Moved Permanently 永久转移 「域名迁移」
- 302 Move Temporarily 临时转移 「负载均衡」
- 304 Not Modified
- 305 Use Proxy
- 400 Bad Request : 请求参数有误
- 401 Unauthorized:权限(Authorization)
- 403 Forbidden 服务器拒绝执行「为啥可能会已响应主体返回」
- 404 Not Found 地址错误
- 405 Method Not Allowed 请求方式不被允许
- 408 Request Timeout 请求超时
- 500 Internal Server Error 未知服务器错误
- 503 Service Unavailable 超负荷
- 505 HTTP Version Not Supported
- ......
- 获取响应信息
- onload 信息返回 HTTP状态码不一定是200
- onerror 信息没有返回【可能断网了】
- onreadystatechange 当请求被发送到服务器时,我们需要执行一些基于响应的任务。每当 readyState 改变时,就会触发 onreadystatechange 事件。readyState 属性存有 XMLHttpRequest 的状态信息。
- 获取响应主体信息 xhr.response/responseText/responseXML...
- 服务器返回的响应主体信息的格式
- 字符串「一般是JSON字符串」 「最常用」
- XML格式数据
- 文件流格式数据「buffer/二进制...」
- ...
- 获取响应头信息 xhr.getResponseHeader/getAllResponseHeaders
发送请求「send中传递的信息,就是设置的请求主体信息」
基于请求主体传递给服务器的数据格式是有要求的「Postman接口测试工具」
- 1.form-data 主要应用于文件的上传或者表单数据提交
xhr.setRequestHeader('Content-Type', 'multipart/form-data'); ------ let fd = new FormData; fd.append('lx', 0); fd.append('name', 'xxx'); xhr.send(fd);
2.x-www-form-urlencoded格式的字符串
格式:“lx=1&name=xxx” 「常用」
Qs库:$npm i qs
Qs.stringify/parse:实现对象和urlencoded格式字符串之间的转换
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); ------ xhr.send(Qs.stringify({ lx: 0, name: 'xxx' })); // => "lx=0&name=xxx" // Qs.parse("lx=0&name=xxx") => {lx: "0", name: "xxx"}
- 3.raw字符串格式
- 普通字符串 -> text/plain
- JSON字符串 -> application/json => JSON.stringify/parse 「常用」
- XML格式字符串 -> application/xml
- ...
- 4.binary进制数据文件「buffer/二进制...」
- 一般也应用于文件上传
- 图片 -> image/jpeg
- EXCEL -> application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
- ...
- 5.GraphQL
let xhr = new XMLHttpRequest; xhr.open('GET', './1.json'); // 设置请求头信息&超时时间&携带资源凭证 需要在open之后send之前 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); // xhr.setRequestHeader('name', '珠峰'); 请求头信息中不允许出现中文 xhr.onreadystatechange = function () { if (xhr.status !== 200) return; if (xhr.readyState === 2) { console.log(xhr.getAllResponseHeaders());//获取响应头信息 //console.log(xhr.getResponseHeader("keep-alive")); } if (xhr.readyState === 4) { console.log(xhr.response);//获取响应主体信息 } }; xhr.send(Qs.stringify({ name: 'xxx', lx: 0 }));面试题:xhr里有多少个方法
私有属性
xhr.abort() 终端请求 xhr.onabort
xhr.timeout = 0 设置超时时间 xhr.ontimeout
xhr.withCredentials = true 再CORS跨域资源请求中 允许携带资源凭证 例如:cookie
xhr.upload.onprogress 监听文件上传的进度
_proto_:XMLHttpRequest
abort
getAllResponseHeaders
getResponseHeader
open
overrideMimeType
send
setRequestHeader
从后端获取数据
let xhr = new XMLHttpRequest; xhr.open('GET', '/userInfo?id=1'); //=>router Query // xhr.open('GET', '/userInfo/1'); //=>router Params // => 需后端处理: app.get('/userInfo/:id') xhr.onreadystatechange = function () { if (xhr.status !== 200) return; if (xhr.readyState === 4) { console.log(xhr.response); } }; xhr.send();result api
案例:AJAX核心基础知识之倒计时抢购案例
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>珠峰在线Web高级</title> <style> * { margin: 0; padding: 0; } .box { width: 300px; height: 50px; line-height: 50px; text-align: center; background: lightblue; letter-spacing: 3px; } .box .text { color: red; } </style> </head> <body> <div class="box"> 距离抢购还剩下 <span class="text">00:00:00</span> </div> <script src="2.js"></script> </body> </html>/* * 两个时间: * + 目标时间 18:00:00 * + 当前时间 * 目标时间-当前时间=时间差 「毫秒差:计算时间差中包含多少小时,多少分钟,多少秒」 * 每间隔一秒中都需要重新获取当前时间「定时器 setInterval」,重算时间差等 * * 核心的问题: * 当前时间是不可以获取客户端本地的(因为本地的时间客户自己可以肆意的修改),需要统一获取服务器的时间「响应头->Date」 * + 获取服务器时间会存在时间偏差问题 --> HEAD AJAX状态码为2 * * 在页面不刷新的情况下,每间隔1秒,不是再次从服务器获取(如果这样:服务器会崩溃,用户得到的时间误差也会越大...),而是基于第一次获取的结果之上,手动给其累加1000ms即可 */ let countdownModule = (function () { let textBox = document.querySelector('.text'), serverTime = 0, targetTime = +new Date('2020/12/05 16:00:00'), timer = null; // 获取服务器时间 const queryServerTime = function queryServerTime() { return new Promise(resolve => { let xhr = new XMLHttpRequest; xhr.open('HEAD', '/'); xhr.onreadystatechange = () => { if ((xhr.status >= 200 && xhr.status < 300) && xhr.readyState === 2) { let time = xhr.getResponseHeader('Date'); // 获取的时间是格林尼治时间 -> 变为北京时间 resolve(+new Date(time)); } }; xhr.send(null); }); }; // 倒计时计算 const supplyZero = function supplyZero(val) { val = +val || 0; return val < 10 ? `0${val}` : val; }; const computed = function computed() { let diff = targetTime - serverTime, hours = 0, minutes = 0, seconds = 0; if (diff <= 0) { // 到达抢购时间了 textBox.innerHTML = '00:00:00'; clearInterval(timer); return; } // 没到时间则计算即可 hours = Math.floor(diff / (1000 * 60 * 60)); diff = diff - hours * 1000 * 60 * 60; minutes = Math.floor(diff / (1000 * 60)); diff = diff - minutes * 1000 * 60; seconds = Math.floor(diff / 1000); textBox.innerHTML = `${supplyZero(hours)}:${supplyZero(minutes)}:${supplyZero(seconds)}`; }; return { async init() { serverTime = await queryServerTime(); computed(); // 设置定时器 timer = setInterval(() => { serverTime += 1000; computed(); }, 1000); } }; })(); countdownModule.init();
Ajax并发管控
当不确定异步请求个数时,为防止当一瞬间发生上百个http请求时,导致堆积了无数调用栈进而导致内存溢出问题。
ajax的并行和串行:多个异步的ajax/fetch请求,如何进行管理?
- 串行:一个异步请求完了以后在进行下一个请求
- 并行:多个请求间没有依赖,可以同时进行「一般会设置一个需求:等到所有请求都成功后,我们干啥事」
// 模拟数据请求 const delay = function delay(interval) { return new Promise((resolve, reject) => { setTimeout(() => { // if (interval === 1003) reject('xxx'); resolve(interval); }, interval); }); }; // 任务列表:数组、数组中每一项是个函数,函数执行就是发送一个请求(返回promise实例) let tasks = [() => { return delay(1000); }, () => { return delay(1001); }, () => { return delay(1002); }, () => { return delay(1003); }, () => { return delay(1004); }, () => { return delay(1005); }, () => { return delay(1006); }]; // 并行 delay(1000).then(value => { console.log(`第一个请求:${value}`); }); delay(2000).then(value => { console.log(`第二个请求:${value}`); }); delay(3000).then(value => { console.log(`第三个请求:${value}`); }); Promise.all([delay(1000), delay(2000), delay(3000)]).then(values => { console.log(`三个请求都成功:${values}`); }); ------------------------------------------------------------------------ // 串行 delay(1000).then(value => { console.log(`第一个请求:${value}`); return delay(2000); }).then(value => { console.log(`第二个请求:${value}`); return delay(3000); }).then(value => { console.log(`第三个请求:${value}`); }); // 串行(async&await) (async function () { let value = await delay(1000); console.log(`第一个请求:${value}`); value = await delay(2000); console.log(`第二个请求:${value}`); value = await delay(3000); console.log(`第三个请求:${value}`); })();模拟一次发送所有请求
// 模拟数据请求 const delay = function delay(interval) { return new Promise((resolve, reject) => { setTimeout(() => { // if (interval === 1003) reject('xxx'); resolve(interval); }, interval); }); }; // 任务列表:数组、数组中每一项是个函数,函数执行就是发送一个请求(返回promise实例) let tasks = [() => { return delay(1000); }, () => { return delay(1001); }, () => { return delay(1002); }, () => { return delay(1003); }, () => { return delay(1004); }, () => { return delay(1005); }, () => { return delay(1006); }]; let values = [], n = 0; tasks.forEach(async task => { let result = await task(); values.push(result); n++; if (n >= tasks.length) { console.log(values); } });asyncPool实现并发控制
// 模拟数据请求 const delay = function delay(interval) { return new Promise((resolve, reject) => { setTimeout(() => { // if (interval === 1003) reject('xxx'); resolve(interval); }, interval); }); }; // 任务列表:数组、数组中每一项是个函数,函数执行就是发送一个请求(返回promise实例) let tasks = [() => { return delay(1000); }, () => { return delay(1001); }, () => { return delay(1002); }, () => { return delay(1003); }, () => { return delay(1004); }, () => { return delay(1005); }, () => { return delay(1006); }]; // asyncpool:实现ajax的并发限制 // + 当请求成功后,我们无法知道当前请求在任务集合中的索引「可以自己处理」 // + 最后的onComplete回调函数可能会被触发多次 let values = []; asyncPool(2, tasks, async(task, next) => { // task:当前需要发送请求的任务 // next:执行下一个任务 let result = await task(); values.push(result); next(); }, () => { console.log(values); });asyncPool源码
(function () { /** * async Pool * @param {Number} threadCount Thread Count * @param {Array} stack The task list to deal with * @param {Function} func The function to deal each individual task * @param {Function} onComplete The callback function when all tasks are done */ var asyncPool = function asyncPool(threadCount, stack, func, onComplete) { if (!threadCount) threadCount = 1; if (!Array.isArray(stack)) stack = []; if (typeof func !== 'function') func = function (data, callback) { callback(); }; stack = stack.slice(); var processingCount = 0; var eventUtil = {}; eventUtil.subList = {}; eventUtil.on = function (e, callback) { if (!eventUtil.subList[e]) { eventUtil.subList[e] = []; } eventUtil.subList[e].push(callback); }; eventUtil.trigger = function (e, data) { if (eventUtil.subList[e] && eventUtil.subList[e].length) { eventUtil.subList[e].forEach(function (callback) { callback(e, data); }); } }; eventUtil.on('empty', function (threadIndex) { if (!stack.length) { if (!processingCount && onComplete) { onComplete(); } return; } var target = stack.shift(); setTimeout(function () { processingCount++; func(target, function () { processingCount--; eventUtil.trigger('empty', threadIndex); }); }, 0); }); for (var i = threadCount; i--;) { eventUtil.trigger('empty', i); } }; /* 暴露API */ if (typeof window !== "undefined") window.asyncPool = asyncPool; if (typeof module === "object" && typeof module.exports === "object") module.exports = asyncPool; })();
实现Ajax并发管控
方案一:基于创造多个工作区,实现并发管控(常用方案)
// 模拟数据请求 const delay = function delay(interval) { return new Promise((resolve, reject) => { setTimeout(() => { // if (interval === 1003) reject('xxx'); resolve(interval); }, interval); }); }; // 任务列表:数组、数组中每一项是个函数,函数执行就是发送一个请求(返回promise实例) let tasks = [() => { return delay(1000); }, () => { return delay(1001); }, () => { return delay(1002); }, () => { return delay(1003); }, () => { return delay(1004); }, () => { return delay(1005); }, () => { return delay(1006); }]; /** * createRequest:实现并发管控 * @param {Array} tasks 需要并发的任务列表(每一项都是个函数,函数执行发送请求,返回promise实例) * @param {Number} limit 需要限制并发的数量(默认值:2) * @returns {Promise} 返回一个promise实例,当所有任务都请求成功后,实例为fulfilled,值是每一个请求成功的结果 */ const createRequest = function createRequest(tasks, limit) { // init params if (!Array.isArray(tasks)) throw new TypeError('tasks is not an array'); limit = +limit; if (isNaN(limit)) limit = 2; limit = limit < 1 ? 1 : (limit > tasks.length ? tasks.length : limit); // 限制几个并发,就需要创造几个工作区{每个工作区都返回promise} let works = new Array(limit).fill(null), values = [], index = 0; works = works.map(() => { return new Promise(resolve => { // 每一次都去任务列表中取出一个任务执行,请求成功后,再去拿一个任务执行...直到任务列表中无任务可取,则当前工作区算作处理成功了,promise变为fulfilled const next = async() => { let prevIndex = index, task = tasks[index++], value; if (typeof task === "undefined") { // 当前工作区处理完,让当前工作区的promise为fulfilled resolve(); return; } try { value = await task(); values[prevIndex] = value; } catch (_) { values[prevIndex] = null; } next(); }; next(); }); }); // 所有工作区的promise都是成功态,则证明请求都发送完成了 // return Promise.all(works).then(() => values); return Promise.all(works).then(() => { return values; }); }; createRequest(tasks).then(values => { console.log('请求都完成:', values); });方案二:利用队列和runing记录正在运行的任务等方式,控制并发执行
// 模拟数据请求 const delay = function delay(interval) { return new Promise((resolve, reject) => { setTimeout(() => { // if (interval === 1003) reject('xxx'); resolve(interval); }, interval); }); }; // 任务列表:数组、数组中每一项是个函数,函数执行就是发送一个请求(返回promise实例) let tasks = [() => { return delay(1000); }, () => { return delay(1001); }, () => { return delay(1002); }, () => { return delay(1003); }, () => { return delay(1004); }, () => { return delay(1005); }, () => { return delay(1006); }]; // 方案二:利用队列和runing记录正在运行的任务等方式,控制并发执行 class TaskQueue { constructor(tasks, limit, onComplete) { // 把信息挂载到实例上,方便在其它的方法中基于实例获取 let self = this; self.tasks = tasks; self.limit = limit; self.onComplete = onComplete; self.queue = []; //存放任务的队列 self.runing = 0; //记录正在运行的任务数量 self.index = 0; //记录取出任务的索引 self.values = []; //记录每个任务完成的结果 } pushStack(task) { // 把任务存储到队列中 let self = this; self.queue.push(task); self.next(); } async next() { // 核心方法:根据runing控制哪些任务执行 let self = this, { tasks, limit, onComplete, queue, runing, values, index } = self; // 如果运行的任务数小于并发限制,而且能够取出对应的任务:取出对应任务并且去发送 if (runing < limit && index <= tasks.length - 1) { self.runing++; let prevIndex = index, task = queue[self.index++], value; try { value = await task(); values[prevIndex] = value; } catch (err) { values[prevIndex] = null; } self.runing--; self.runing === 0 ? onComplete(values) : self.next(); } } } const createRequest = function createRequest(tasks, limit, onComplete) { if (!Array.isArray(tasks)) throw new TypeError('tasks must be an array'); if (typeof limit === 'function') onComplete = limit; limit = +limit; if (isNaN(limit)) limit = 2; if (typeof onComplete !== 'function') onComplete = Function.prototype; // 把任务列表中的任务,依次存放到任务队列中 let TQ = new TaskQueue(tasks, limit, onComplete); tasks.forEach(task => { TQ.pushStack(task); }); }; createRequest(tasks, values => { console.log(`所有请求都成功`, values); });
设计模式
设计模式:是一种思想,更规范更合理去管理代码「方便维护、升级、扩展、开发」;
每一种设计模式都是解决了一类问题,而且问题偏向于“更好的去管理代码”!
Singleton单例模式 && Command命令模式
最早期的模块化编程思想「同样的还有:AMD/CMD/CommonJS/ES6Module」
避免全局变量的污染
实现模块之间的相互调用「提供了模块导出的方案」
在实际的业务开发中,我们还可以基于命令模式管控方法的执行顺序,从而有效的实现出对应的功能
// 公用版块 utils let utils = (function () { function debounce(func, wait) {} //... return { debounce: debounce }; })(); // A版块 let AModule = (function () { utils.debounce(); function fn() {} function query() {} return { query: query }; })(); // B版块{实现当前模块下需要完成的所有的功能} let BModule = (function () { utils.debounce(); AModule.query(); // 获取数据 function getData() {} // 绑定数据 function binding() {} // 处理事件绑定 function handle() {} // 处理其它事情的 function fn() {} return { // 基于命令模式管控方法的执行顺序 // 模块的入口「相当于模块的大脑,控制模块中方法的执行顺序」 init() { getData(); binding(); handle(); fn(); } }; })(); BModule.init();
Constructor构造器模式「站在面向对象的思想上去构建项目」
自定义类和实例
私有&公有属性和方法
编写公共的类库 & 插件组件
----插件
- 每一次调用插件我们都是创造这个类的一个实例,既保证每个实例之间「每次调用之间」有自己的私有属性,互不影响;也可以保证一些属性方法还是公用的,有效避免代码的冗余...
// 原型上添加方法 function Fn() { this.xxx = xxx; } Fn.prototype = { constructor: Fn, query() {}, // ... }; Fn.xxx = function () {};// Constructor构造器模式 class Fn { constructor() { this.xxx = xxx; } query() {} static xxx() {} } let f1 = new Fn; let f2 = new Fn;
Factory工厂模式
简单的工厂模式「一个方法根据传递参数不同,做了不同事情的处理」
JQ中的工厂模式「加工转换」
经验分享:做后台开发的时候,我们有一个需求,一个产品需要适配多套数据库「mysql sqlserver oracle」,项目需要根据一些配置,轻松转换到对应的数据库上...
function factory(options) { if (options == null) options = {}; if (!/^(object|function)$/i.test(typeof options)) options = {}; let { type, payload } = options; if (type === 'MYSQL') { // ... return; } if (type === 'SQLSERVER') { // ... return; } // ... } factory({ type: 'SQLSERVER', payload: { root: '', pass: '', select: '' } });(function () { function jQuery(selector, context) { return new jQuery.fn.init(selector, context); } jQuery.fn = jQuery.prototype = { constructor: jQuery, //... }; // 中间转换 function init(selector, context, root) {} jQuery.fn.init = init; init.prototype = jQuery.fn; if (typeof window !== "undefined") { window.$ = window.jQuery = jQuery; } })(); // $() -> jQuery实例
Publish & Subscribe 发布订阅模式「自定义事件处理的一种方案」
灵感来源于:addEventListener DOM2事件绑定
给当前元素的某一个事件行为,绑定多个不同的方法「事件池机制」
事件行为触发,会依次通知事件池中的方法执行
支持内置事件{标准事件,例如:click、dblclick、mouseenter...}
应用场景:凡是某个阶段到达的时候,需要执行很多方法「更多时候,到底执行多少个方法不确定,需要编写业务边处理的」,我们都可以基于发布订阅设计模式来管理代码;创建事件池->发布计划 向事件池中加入方法->向计划表中订阅任务 fire->通知计划表中的任务执行
jQuery自带的方法
let $plan1 = $.Callbacks(); // add remove fire $plan1.add(function () { console.log(1, arguments); }); $plan1.add(function () { console.log(2, arguments); }); setTimeout(() => { $plan1.fire(100, 200); }, 1000); let $plan2 = $.Callbacks(); $plan2.add(function () { console.log(3, arguments); }); $plan2.add(function () { console.log(4, arguments); }); setTimeout(() => { $plan2.fire(300, 400); }, 2000);(function () { // 自己创造的事件池 let pond = []; // 向事件池中注入方法 function subscribe(func) { // 去重处理 if (!pond.includes(func)) { pond.push(func); } // 每一次执行,返回的方法是用来移除当前新增的这一项的 return function unsubscribe() { pond = pond.filter(item => item !== func); }; } // 通知事件池中的每个方法执行 subscribe.fire = function fire(...params) { pond.forEach(item => { if (typeof item === "function") { item(...params); } }); }; window.subscribe = subscribe; })(); // 需求:从服务获取数据,获取数据后要干很多事情 // A const fn1 = data => {}; subscribe(fn1); // B const fn2 = data => {}; subscribe(fn2); // C const fn3 = data => {}; subscribe(fn3); // D const fn4 = data => {}; subscribe(fn5); // E const fn5 = data => {}; subscribe(fn5); // F query().then(data => { subscribe.fire(data); });一个项目中,我们可能会出现多个事情都需要基于发布订阅来管理,一个事件池不够
思路一:基于面向对象管理,每一次new执行都单独创建一个自定义事件池,实例可以调用:on/off/emit 「自己扩展」
面向对象 类&实例
每个实例都有一个自己的私有事件池
subscribe/unsubscribe/fire公用的
class Sub{...} let s1=new Sub(); s1.subscribe() s1.fire() let s2=new Sub(); s2.subscribe() s2.fire()class Sub { // 实例私有的属性:私有的事件池 pond = []; // 原型上设置方法:向事件池中订阅任务 subscribe(func) { let self = this, pond = self.pond; if (!pond.includes(func)) pond.push(func); return function unsubscribe() { let i = 0, len = pond.length, item = null; for (; i < len; i++) { item = pond[i]; if (item === func) { pond.splice(i, 1); break; } } }; } // 通知当前实例所属事件池中的任务执行 fire(...params) { let self = this, pond = self.pond; pond.forEach(item => { if (typeof item === "function") { item(...params); } }); } } let sub1 = new Sub; sub1.subscribe(function () { console.log(1, arguments); }); sub1.subscribe(function () { console.log(2, arguments); }); setTimeout(() => { sub1.fire(100, 200); }, 1000); let sub2 = new Sub; sub2.subscribe(function () { console.log(3, arguments); }); sub2.subscribe(function () { console.log(4, arguments); }); setTimeout(() => { sub2.fire(300, 400); }, 2000);思路二:全局只有一个自定义事件池,基于自定义事件名称来区分要执行的方法
例如:把需要做的事情放入到容器中,当需要用的时候只需通知执行即可
const $sub = (function () { // 自定义事件池 let listeners = {}; // 向事件池中加入方法 const on = function on(name, func) { let arr = listeners[name]; if (!arr) { // 事件池中从来没有这个自定义事件:加一个即可 listeners[name] = [func]; return; } // 之前已经存在这个自定义事件,则把方法存储到数组中:去重处理 if (arr.indexOf(func) > -1) return; arr.push(func); }; // 从事件池中移除方法 const off = function off(name, func) { let arr = listeners[name], index; if (!arr) return; index = arr.indexOf(func); if (index === -1) return; // arr.splice(index, 1); //这样会导致数组塌陷问题 arr[index] = null; }; // 通知事件池中的方法执行 const emit = function emit(name, ...params) { let arr = listeners[name]; if (!arr) return; for (let i = 0; i < arr.length; i++) { let item = arr[i]; if (typeof item !== "function") { // 如果当前不是函数,则把它移除掉 arr.splice(i, 1); i--; continue; } item(...params); } }; return { on, off, emit }; })(); //--------------------- const fn1 = (x, y) => { console.log('fn1', x + y); }; $sub.on('@A', fn1); const fn2 = (x, y) => { console.log('fn2', x + y); $sub.off('@A', fn1); $sub.off('@A', fn2); }; $sub.on('@A', fn2); const fn3 = () => console.log('fn3'); $sub.on('@A', fn3); const fn4 = () => console.log('fn4'); $sub.on('@A', fn4); const fn5 = () => console.log('fn5'); $sub.on('@A', fn5); document.body.onclick = function () { $sub.emit('@A', 10, 20); };数组塌陷
什么叫数组坍塌?当数组执行删除单元操作时,被删除单元,之后的单元,会前移,进而顶替被删除单元,出现在被删除单元的位置上,造成数组长度减少的情况,这样的现象称为数组的坍塌。
当我们知道数组塌陷这个原因之后,再删除元素的时候,不应该直接删掉,而应该是使用 null 替换掉当前的元素。当第二次遍历数组的时候,把为 null 的元素过滤掉就 ok 了。
解决数组塌陷的方法
1.设置删除起始位置为0
<script> let length=arr.length for(var i=0;i<length;i++){ arr.splice(0,1) } console.log(arr); </script>删除干净,需要将数组的长度先单独保存(let length=arr.length),不然数组的长度会随着数组的变化而变化,从而4>3,后面三个删除不了
2.从后面开始删除,倒着删除
<script> for(var i=arr.length-1;i>=0;i--){ arr.splice(i,1) } console.log(arr); </script>
Observer 观察者模式 & Mediator 中介者模式
观察者模式
定义观察者:形式可以不一样,只需要具备update方法即可
class OBSERVER { update(msg) { console.log(`我是观察者1,我接收到的消息是:${msg}`); } } let DEMO = { update(msg) { console.log(`我是观察者2,我接收到的消息是:${msg}`); } }; // 目标 class Subject { observerList = []; add(observer) { this.observerList.push(observer); } remove(observer) { // 没有考虑塌陷问题 this.observerList = this.observerList.filter(item => item !== observer); } notify(...params) { this.observerList.forEach(item => { if (item && typeof item.update === "function") { item.update(...params); } }); } } let sub = new Subject; sub.add(new OBSERVER); sub.add(DEMO); setTimeout(() => { sub.notify('hello world~~'); }, 1000);
中介者模式
let mediator = (function () {
let topics = [];
const subscribe = function subscribe(callback) {
topics.push(callback);
};
const publish = function publish(...params) {
topics.forEach(callback => {
if (typeof callback === "function") {
callback(...params);
}
});
};
return {
subscribe,
publish
};
})();
mediator.subscribe(() => console.log(1));
mediator.subscribe(() => console.log(2));
setTimeout(() => {
mediator.publish();
}, 1000);
Axios 基于promise封装的ajax库【核心还是基于 XMLHttpRequest 发送请求的】
向服务器发送请求 ajax 原生 XMLHttpRequest $.ajax JQ中基于回调函数的方式对ajax进行了封装「回调地狱」 axios 基于Promise封装的ajax库「最常用的」 --------核心都是 XMLHttpRequest
ES6新增了一个 fetch API,告别了 XMLHttpRequest ,基于新的方式实现客户端和服务器之间的通信
1、基于axios发送数据请求,返回结果都是一个promise实例
默认情况下
-
服务器返回的HTTP状态码是以2开始,则让promise状态为成功,值是一个response对象
response = {
config:{...},发送axios请求设置的配置项
data:{...},服务器返回的响应主体信息
headers:{...},服务器返回的响应头信息
request:XMLHttpRequest实例对象,原生的xhr对象
status:200,服务器响应的HTTP状态码
statusText:'OK',状态码的描述
}
async mounted() { let response = await axios.get('/user/list', { param: { departmentId: 0, search: '', }, }) console.log(response) } -
promise状态为失败
async mounted() { try { let response = await axios.get('/user/list2', { param: { departmentId: 0, search: '', }, }) } catch (error) { console.dir(error) } }@1 服务器有返回信息【response对象存在】,只不过HTTP状态码不是以2开始的
reason = {
config:{...},
isAxiosError:true,
request:XMLHttpRequest实例对象,
response:等同于成功获取的response对象,
toJson:function....,
message:'xxx',
.....
}
@2 请求超时 或者 请求中断了
reason = {
code:"ECONNABORTED",表示请求超时
response:undefined,
....
}
@3 断网了,特点:服务器没有反馈任何的信息
-
我们可以自定义服务返回的HTTP状态码为多少是成功,为多少是失败
axios.get([url],{ ..., //axios的validateStatus配置项,就是自定义promise实例状态是成功的条件 validateStatus:(status)=>{ return status >= 200 && status < 300; //默认处理机制 } };
基于axios发送请求的方式
- axios([config]) 或者 axios([url],[config])
- axios.request([config])
- axios.get/delete/head/options([url],[config])
- axios.post/put/patch([url],[data],[config]) -> [data]基于请求主体传递给服务器的信息
- let instance = axios.create([config])
- 创建的instance等同于axios,使用起来和axios一样
- instance.get([url],[config])
axios发送请求时候的配置项 config
-
url:请求的地址 发送请求的时候,但凡没有单独设置url的,都需要在配置项中指定
-
baseURL:请求地址的通用前缀
最后发送请求的时候,是把baseURL和url拼接在一起发送的
axios.get('/user/list',{
baseURL:'api.zhufeng.cn'
....
})
最后发送请求的地址是 'api.zhufeng.cn/user/list'
特殊情况:如果url地址本身已经存在了 http或者https 等信息,说明url本身就已经是完成的地址了,baseURL的值则无需再拼接了
-
transformRequest:(data,headers)={}
transformRequest:(data,headers)={
// data:自己传递[data]
// headers:设置的请求头信息{对象}
return xxx; //返回值是啥,最后基于请求主体传递的就是啥
}
它“只针对post系列请求”,把我们自己传递 [data] 格式化为指定的格式,后期在基于请求主体发送给服务器
axios内部做了一个处理,根据我们最后处理好的[data]的格式,自动设置请求头中的Content-Type值【不一定完全准确】
@1 客户端基于请求主体传递给服务器的数据格式
- form-data Content-Type:multipart/form-data 主要应用于文件上传/表单提交
- urlencoded Content-Type:application/x-www-form-urlencoded
- GET请求系列:是基于URL问好传参把信息传递给服务器的 ?xxx=xxx&xxx=xxx
- xxx=xxx&xxx=xxx 这种字符串就是urlencoded格式字符串
- raw 泛指,代指文本格式【含:普通格式文本字符串、JSON格式字符串....】
- 普通字符串 Content-Type:text/plain
- JSON格式字符串 Content-Type:application/json
- ....
- binary 进制格式数据 主要用于文件上传
@2 axios内部在默认情况况下,如果我们[data]传递的是个普通对象,而且也没有经过transformRequest处理,则内部默认把对象变为JSON格式字符串传递给服务器
-
transformResponse:data => {}
transformResponse:data => {
// data:从服务器获取的结果,而且是响应主体信息【服务器响应主体返回的信息一般都是JSON格式字符串】
return data;
}
会默认转换成Json格式字符串
-
headers:{...} 自定义请求头信息
-
params:{...} GET系列请求,基于URL问号传参,把信息传递给服务器,我们params一般设置为对象,axios内部会对象变为 urlencoded 格式拼接到URL的末尾
-
data:{....} POST系列请求,基于请求主体传递的信息...
-
timeout:0 设置超时时间,写0就是不设置
-
withCredentials:false 在CORS跨域请求中,是否允许携带资源凭证
-
responseType:把服务器返回的结果设置为指定的格式 ‘arraybuffer’,‘blob’,‘document’,‘json[默认]’,‘text’,‘stream’....
-
onUploadProgress:progressEvent=>{} 监听上传的进度
-
onDownloadProgress:progressEvent=>{} 监听下载的进度
-
validateStatus:status=>status=>200 && status<300 定义服务器返回的状态码是多少,promise实例是成功的
-
......
axios请求的取消,依赖于 axios.CancelToken 完成
const source = axios.CancelToken.source();
axios.get([url],{
...,
cancelToken:source.token,
}).catch(reason=>{
//取消后,promise实例是失败的
});
source.cancel('...');取消发送
axios中拦截器
请求拦截器:当axios把各方面配置都处理好了,在即将基于这些配置项服务器发送请求的时候,触发请求拦截器
axios.interceptors.request.use(config=>{ //config存储的是axios处理好的配置,我们一般在请求拦截器中修改配置 //... return config; })响应拦截器:服务器返回结果,axios已经知道返回的promis实例状态是成功还是失败的,在自己调用 .then/catch 之前,先根据promise状态,把响应拦截器中设置的方法执行
axios.interceptors.response.use( response=>{ //promise实例是成功的,执行这个方法;response存储服务器返回的结果 }, reason=>{ //promise实例是失败的,执行这个方法:reason存储失败的原因 } ); axios.get([url],[config]) .then(value=>{}) .catch(reason=>{})
代码
<template> <div id="app">珠峰培训</div> </template> <script> import axios from 'axios' import qs from 'qs' import md5 from 'blueimp-md5' import { isPlainObject } from '@/assets/utils' export default { name: 'App', async mounted() { /* let fm = new FormData(); fm.append("account", "18310612838"); fm.append("password", "1234567890"); */ const source = axios.CancelToken.source() axios .post( '/user/login', { account: '18310612838', password: md5('1234567890'), }, { transformRequest: (data) => { if (isPlainObject(data)) { //把对象变为urlencoded格式字符串 return qs.stringify(data) } return data }, /* transformResponse: (data) => { // return JSON.parse(data); return data; }, */ cancelToken: source.token, } ) .then((response) => { console.log(response) }) .catch((reason) => { console.log(reason) // {message:'我把请求取消了~~'} -> Cancel.prototype }) setTimeout(() => { source.cancel('我把请求取消了~~') }) }, } </script>import Vue from 'vue'; import App from './App.vue'; // import api from './api'; // import 'element-ui/lib/theme-chalk/index.css'; import axios, { CancelToken } from 'axios'; import { isPlainObject } from '@/assets/utils'; import qs from 'qs'; /* 接口请求测试 */ //中断请求 const source = CancelToken.source(); //获取最新新闻 axios.get('/api/news_latest', { validateStatus: status => (status >= 200 && status < 300), timeout: 100, cancelToken: source.token }).then(response => { return response.data; }).then(value => { console.log('请求成功', value) //想要的响应主体信息 }).catch(reason => { // reason // @1 服务器有反馈信息,但是HTTP状态码不以2开始的 reason.response.status // @2 请求超时 reason.code = "ECONNABORTED" reason.response = undefined // @3 请求中断 reason是Cancel的实例对象,reason.message存储中断原因,可以基于 axios.isCancel(reason) 检测是否为手动中断请求的 // @4 网络出问题了 console.dir(reason) }) source.cancel('请求中断'); //获取以往新闻 axios.get('/api/news_before', { params: { time: '20211227' }, timeout: 6000, validateStatus: status => (status >= 200 && status < 300) }).then(response => response.data).then(value => { console.log("请求成功", value) }).catch(reason => { console.dir("请求失败", reason) }) //获取手机验证码 axios.post('/api/phone_code', { phone: '13161883402' }, { transformRequest: data => { if (isPlainObject(data)) data = qs.stringify(data); return data; } }).then(response => response.data).then(value => { console.log("请求成功", value) }).catch(reason => { console.dir(reason); }) //用户登录 axios.post('/api/login', { phone: '13161883402', code: '464156' }, { transformRequest: data => { if (isPlainObject(data)) data = qs.stringify(data); return data; } }).then(response => response.data).then(value => { console.log("请求成功", value) }).catch(reason => { console.dir(reason); }) //"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MywicGhvbmUiOiIxMzI0NzY0OTAzMiIsImlhdCI6MTY0MDYwODkzNiwiZXhwIjoxNjQxMjEzNzM2fQ.xBCldqcGXmmFDBSbgp73rfaeV4sVzwFDKi1RixnfPKg" //以下接口需要在请求头中携带TOKEN信息 authorzation:token「客户端登录成功后存储在本地的令牌信息(从服务器获取)」 //检测是否登录 axios.get('/api/check_login', { headers: { authorzation: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwicGhvbmUiOiIxMzE2MTg4MzQwMiIsImlhdCI6MTY0MDYyNDg5NiwiZXhwIjoxNjQxMjI5Njk2fQ.r4t6LUyiWeEsyBo3aHndArBIkpC459ZbnNkUXl5WHAo' }, timeout: 6000, validateStatus: status => (status >= 200 && status < 300) }, { transformRequest: data => { if (isPlainObject(data)) data = qs.stringify(data); return data; } }).then(response => response.data).then(value => { console.log("请求成功", value) }).catch(reason => { console.dir(reason); }) Vue.config.productionTip = false; new Vue({ render: h => h(App), }).$mount('#app');
2、axios的二次封装
axios的二次封装,就是根据项目需要、后台要求,把一些 axios发送请求,公共的部分进行提取;这样再次基于axiso发送请求,就可以简化一些了
对于大型项目来讲,如果需要封装多套
- 把多套公共的部分提取出来
- 不同的部分单独封装
请求中某几个请求和封装的不同,只需要在发送请求的时候单独配置,这样一定是以自己单独配置的为主
基础配置
import axios from 'axios'; import qs from 'qs'; import { isPlainObject } from '@/assets/utils'; import { Message } from 'element-ui'; /* 如果是基于proxy实现的跨域代理,则无需配置baseURL「我们只需要想dev-server启动的服务发送请求即可」 如果是基于cors跨域资源共享方式,则需要配置baseURL 一般真实项目中,我们需要区分环境:开发、测试、生产... let env = process.env.NODE_ENV || "development", baseURL = 'http://127.0.0.1:9999'; switch (env) { case 'test': baseURL = 'http://168.12.1.1:8080'; break; case 'production': baseURL = 'http://api.zhufeng.cn'; break; } axios.defaults.baseURL = baseURL; 自己设置环境变量:基于 cross-env 插件 $ npm i cross-env --save-dev -- package.json -> scripts cross-env NODE_ENV=development vue-cli-service serve */ const instance = axios.create(); instance.defaults.baseURL = '/api'; instance.defaults.timeout = 60000; //配置超时时间 // instance.defaults.withCredentials = true; //针对于CORS跨域资源共享 // instance.defaults.headers['Content-Type'] = 'application/x-www-form-urlencoded'; //自定义公共的请求头信息「axios内部会自动根据请求主体的格式,这是对应的Content-Type」 instance.defaults.validateStatus = status => { // 自定义HTTP状态码是多少算请求成功 return status >= 200 && status < 400; }; // transformRequest只针对于POST系列请求,只是对请求主体格式的处理 instance.defaults.transformRequest = data => { // 如果用户传递的DATA是一个普通对象,根据当前服务器要求,会把其处理为urlencoded格式 if (isPlanObject(data)) data = qs.stringify(data); // 如果DATA指定的不是普通对象,则用户传递的啥,就基于请求主体把啥传递给服务器「axios内部会根据请求主体的数据格式,对常见的格式自动设置请求头中的 Content-Type」 return data; }; // 拦截器:请求拦截器(配置项已处理好,向服务器发请求之前) & 响应拦截器(服务器已返回信息,业务层自己THEN之间) instance.interceptors.request.use(config => { // config:axios内部已经准备好的配置项,最后返回啥就按照啥配置发送请求 // 一般在此处都是对配置项的进一步修改,例如:传递Token const token = localStorage.getItem('token'); if (token) config.headers['authorzation'] = token; return config; }); /* 响应拦截器 */ instance.interceptors.response.use(response => { // 请求成功:服务器有反馈信息 & HTTP状态码通过了validateStatus的校验 return response.data; }, reason => { // 请求失败:根据不同的失败原因做不同的提示 //reason && reason.response && reason.response.status let status = reason ? .response ? .status, code = reason ? .code; if (status) { // 有反馈但是状态码错误 switch (+status) { case 404: Message.error('请求地址出现错误~'); break; case 500: Message.error('服务器出现错误~'); break; // ... } } else if (code === "ECONNABORTED") { Message.error('请求超时~'); } else if (axios.isCancel(reason)) { Message.error('请求被中断~'); } else { Message.error('网络异常~'); } return Promise.reject(reason); }); export default instance;二次封装公共部分
base.js
import axios from "axios"; import { Message } from 'element-ui'; export default function instanceBase(instance) { instance.defaults.timeout = 60000; instance.defaults.validateStatus = status => { return status >= 200 && status < 400; }; instance.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) config.headers['authorzation'] = token; return config; }); instance.interceptors.response.use(response => { return response.data; }, reason => { let status = reason?.response?.status, code = reason?.code; if (status) { switch (+status) { case 404: Message.error('请求地址出现错误~'); break; case 500: Message.error('服务器出现错误~'); break; // ... } } else if (code === 'ECONNABORTED') { Message.error('请求超时~'); } else if (axios.isCancel(reason)) { Message.error('请求被中断~'); } else { Message.error('网络异常~'); } return Promise.reject(reason); }); };二次封装私有部分
http_jian.js
import axios from "axios"; import { isPlainObject } from '@/assets/utils'; import instanceBase from './base'; const instance = axios.create(); instanceBase(instance); instance.defaults.baseURL = '/jian'; instance.defaults.transformRequest = data => { if (isPlainObject(data)) return JSON.stringify(data); return data; }; export default instance;http_zhi.js
import axios from "axios"; import qs from 'qs'; import { isPlainObject } from '@/assets/utils'; import instanceBase from './base'; /* baseURL:有的公司会区分多种环境 开发环境 : webpack-dev-server部署 测试环境 : webpack-dev-server部署 灰度环境 : nginx部署 生产环境 : nginx部署 const env = process.env.NODE_ENV; switch (env) { case 'development': instance.defaults.baseURL = '/api'; break; case 'test': instance.defaults.baseURL = '/apiTest'; break; } */ const instance = axios.create(); instanceBase(instance); instance.defaults.baseURL = '/api'; instance.defaults.transformRequest = data => { if (isPlainObject(data)) return qs.stringify(data); return data; }; export default instance;封装方法暴露API
index.js
import instzhi from "./http_zhi"; import instjian from "./http_jian"; const queryNewsLatest = () => { return instzhi.get('/news_latest'); }; const queryNewsBefore = time => { return instzhi.get('/news_before', { params: { time } }); }; const queryPhoneCode = phone => { return instzhi.post('/phone_code', { phone }); }; const login = (phone, code) => { return instzhi.post('/login', { phone, code }); }; const checkLogin = () => { return instzhi.get('/check_login'); }; export default { queryNewsLatest, queryNewsBefore, queryPhoneCode, login, checkLogin };main.js
import Vue from 'vue'; import App from './App.vue'; import api from './api'; import 'element-ui/lib/theme-chalk/index.css'; Vue.prototype.$api = api; Vue.config.productionTip = false; new Vue({ render: h => h(App), }).$mount('#app');使用
<template> <div id="app">珠峰培训</div> </template> <script> export default { name: 'App', async created() { // 获取最新新闻 let result1 = await this.$api.queryNewsLatest() console.log(result1) // 获取以往新闻 let result2 = await this.$api.queryNewsBefore('20211228') console.log(result2) // 获取手机验证码 await this.$api.queryPhoneCode('13161883402') // 用户登录 存储token let result = await this.$api.login('13161883402', '297015') if (+result.code === 0) { localStorage.setItem('token', result.token) } // 检测是否登录 let result = await this.$api.checkLogin() console.log(result) }, } </script>
Fetch
fetch基础
每一次fetch请求也会返回一个promise实例:
- 实例成功:服务器只要有返回结果,不论HTTP状态码是多少,promise实例都是fulfilled
- response.body存储的是响应主体信息「ReadableStream」
- arrayBuffer
- blob
- json
- text
- ...
- 执行这些方法是把服务器返回的响应主体信息变为指定格式的数据:返回值是一个新的promise实例,用来管控转换数据的过程是否成功
- 实例失败:服务器没有返回任何的结果「例如超时、中断请求、断网...」
import Vue from 'vue'; import App from './App.vue'; import api from './api'; import 'element-ui/lib/theme-chalk/index.css'; /* 测试fetch的应用 */ const controller = new AbortController(); fetch('/api/news_latest', { signal: controller.signal }).then(response => { /* 只有服务器有响应,不论HTTP状态码为多少,Fetch都会把promise实例设置为成功 response是Response内置类的实例 + status/statusText 状态码及其描述 + headers 是Headers内置类的实例,基于Headers.prototype上的方法可以获取响应头的信息 + get([key]) + has([key]) + keys/values/entries 返回迭代器对象,基于next方法执行可以依次获取响应头的信息 + forEach 循环迭代每一个返回的响应头信息 + body 存储的是响应主体信息,它是一个ReadableStream可读流 ---- Response.prototype + arrayBuffer 以Buffer格式数据读取 + blob + json + text + ... 执行这几个方法,返回的结果是一个promise实例「原因:服务器返回的数据内容格式和我们要读取的方法可能存在误差,例如:服务器返回的是普通文本,而我们基于json方法去读取,想要获取json对象,这样是无法正常读取出来的,此时可以把promise标记为失败...而且这样读取的过程也可以是异步操作的」 一但本次执行了某个方法,则无法再执行其他的方法 */ let { status, statusText } = response; if (status >= 200 && status < 400) { return response.json(); } return Promise.reject({ code: 'STATUS ERROR', status, statusText }); }).then(value => { console.log('成功:', value); }).catch(reason => { /* 服务器没有响应:断开请求 & 网络出现故障,Fetch才会把promise实例设置为失败 + 基于AbortController中断请求 reason={code:20,message:'...',name:'AbortError'} 如果从服务器成功获取内容「状态码以2/3开始的」,但是读取数据失败,也会进入这里「reason是Error对象,具备message属性记录失败原因」 如果从服务器获取到内容,但是状态码不符合要求,也会进入到这里「reason是自定义的信息对象」 */ console.dir(reason); }); controller.abort(); Vue.prototype.$api = api; Vue.config.productionTip = false; new Vue({ render: h => h(App), }).$mount('#app');
fetch('/api/news_latest', { method: 'GET', //设置请求方式 credentials: 'include', //设置是否允许携带资源凭证 omit都不允许 *same-origin同源允许 include都允许 headers: {}, // body: {}, //只有在POST/PUT请求下才允许设置body「设置请求主体,但是需要在headers中指定对应类型的Content-Type值(MIME类型)」 // signal:xxx, //中断请求的信号 cache: 'no-cache', mode: 'cors' }).then(response => { let { status, statusText } = response; if (status >= 200 && status < 300) { return response.json(); } return Promise.reject({ code: 'status error', status, statusText }); }).then(value => { console.log(value); }).catch(reason => { console.log(reason); });
fetch的二次封装
二次封装
request([config])
- url 请求地址
- method 请求方式 *GET/DELETE/HEAD/OPTIONS/POST/PUT/PATCH
- credentials 携带资源凭证 *include/same-origin/omit
- headers:null 自定义的请求头信息「格式必须是纯粹对象」
- body:null 请求主体信息「只针对于POST系列请求,根据当前服务器要求,如果用户传递的是一个纯粹对象,我们需要把其变为urlencoded格式字符串(设定请求头中的Content-Type)...」
- params:null 设定问号传参信息「格式必须是纯粹对象,我们在内部把其拼接到url的末尾」
- responseType 预设服务器返回结果的读取方式 *json/text/arrayBuffer/blob
- signal 中断请求的信号
-----
request.get/head/delete/options([url],[config]) 预先指定了配置项中的url/method
request.post/put/patch([url],[body],[config]) 预先指定了配置项中的url/method/body
http.js
import qs from 'qs'; import { isPlainObject } from '@/assets/utils'; import { Message } from 'element-ui'; /* 核心方法 */ const request = function request(config) { // init config & validate「扩展:可以给每一项都做校验」 if (!isPlainObject(config)) config = {}; config = Object.assign({ url: '', method: 'GET', credentials: 'include', headers: null, body: null, params: null, responseType: 'json', signal: null }, config); if (!isPlainObject(config.headers)) config.headers = {}; if (config.params !== null && !isPlainObject(config.params)) config.params = null; let { url, method, credentials, headers, body, params, responseType, signal } = config; // 处理URL:params存在,我们需要把params中的每一项拼接到URL末尾 if (params) url += `${url.includes('?') ? '&' : '?'}${qs.stringify(params)}`; // 处理请求主体:只针对于POST系列请求;body是个纯粹对象,根据当前后台要求,把其变为urlencoded格式!「扩展:根据body传递格式的数据类型,在内部默认把Content-Type设置好」 if (isPlainObject(body)) { body = qs.stringify(body); headers['Content-Type'] = 'application/x-www-form-urlencoded'; } // 类似于Axios的请求拦截器,例如:把存储在客户端本地的token信息携带给服务器「根据当前后台要求处理」 let token = localStorage.getItem('token'); if (token) headers['authorzation'] = token; // send method = method.toUpperCase(); config = { method, credentials, headers, cache: 'no-cache', mode: 'cors' }; if (/^(POST|PUT|PATCH)$/i.test(method) && body) config.body = body; if (signal) config.signal = signal; return fetch(url, config).then(response => { // 成功则返回响应主体信息 let { status, statusText } = response, result; if (!/^(2|3)\d{2}$/.test(status)) return Promise.reject({ code: -1, status, statusText }); switch (responseType.toLowerCase()) { case 'text': result = response.text(); break; case 'arraybuffer': result = response.arrayBuffer(); break; case 'blob': result = response.blob(); break; default: result = response.json(); } return result.then(null, reason => Promise.reject({ code: -2, reason })); }).catch(reason => { // 根据不同的失败情况做不同的统一提示 /* let code = reason?.code; if (+code === -1) { // 状态码问题 switch (+reason.status) { case 404: // ... break; } } else if (+code === -1) { // 读取数据出现问题 } else if (+code === 20) { // 请求被中断 } else { // 网络问题 } */ Message.error('小主,当前网络出现异常,请稍后再试~~'); return Promise.reject(reason); }); }; /* 快捷方法 */ ['GET', 'HEAD', 'DELETE', 'OPTIONS'].forEach(item => { request[item.toLowerCase()] = function (url, config) { if (!isPlainObject(config)) config = {}; config['url'] = url; config['method'] = item; return request(config); }; }); ['POST', 'PUT', 'PATCH'].forEach(item => { request[item.toLowerCase()] = function (url, body, config) { if (!isPlainObject(config)) config = {}; config['url'] = url; config['method'] = item; config['body'] = body; return request(config); }; }); export default request;index.js
import request from "./http"; const queryNewsLatest = (signal) => { if (signal) { return request.get('/api/news_latest', { signal }); } return request.get('/api/news_latest'); }; const queryNewsBefore = time => { return request.get('/api/news_before', { params: { time } }); }; const queryPhoneCode = phone => { return request.post('/api/phone_code', { phone }); }; const login = (phone, code) => { return request.post('/api/login', { phone, code }); }; const checkLogin = () => { return request.get('/api/check_login'); }; export default { queryNewsLatest, queryNewsBefore, queryPhoneCode, login, checkLogin };
/*
request([url],[config])
+ method:'GET' 请求方式
+ credentials:'include' 在CORS跨域中是否允许携带资源凭证 same-origin, *omit
+ cache:'no-cache' 是否设置缓存 default, reload, force-cache, only-if-cached
+ headers:{} 自定义请求头信息
+ body:null 设置请求主体信息,我们一般传递一个对象
+ params:null 基于URL问号传递的参数值
+ responseType:'json' 预设服务器返回的数据格式 text, arraybuffer, blob
*/
import {
isPlainObject
} from '@/assets/utils';
import qs from 'qs';
import {
Message
} from 'element-ui';
let baseURL = '';
/* let env = process.env.NODE_ENV || "development";
switch (env) {
case 'development':
baseURL = 'http://127.0.0.1:9999';
break;
case 'test':
baseURL = 'http://168.12.1.1:8080';
break;
case 'production':
baseURL = 'http://api.zhufeng.cn';
break;
} */
export default function request(url, config) {
// init params
if (typeof url !== "string") throw new TypeError('url is not a string');
if (!isPlainObject(config)) config = {};
let {
method,
credentials,
cache,
headers,
body,
params,
responseType
} = Object.assign({
method: 'GET',
credentials: 'include',
cache: 'no-cache',
headers: {},
body: null,
params: null,
responseType: 'json'
}, config);
if (!isPlainObject(headers)) headers = {};
// 处理URL:拼接baseURL & 问号传参
if (!/http(s?):\/\//i.test(url)) url = baseURL + url;
if (params) {
if (isPlainObject(params)) params = qs.stringify(params);
url += `${url.includes('?')?'&':'?'}${params}`;
}
// 处理请求主体信息「根据自己的后台要求处理」
if (isPlainObject(body)) body = qs.stringify(body);
headers['Content-Type'] = 'application/x-www-form-urlencoded';
if (body) {
// 根据请求主体的数据格式,设置不同的Content-Type
if (body instanceof FormData) headers['Content-Type'] = 'multipart/form-data';
}
// 类似于请求拦截器
let token = sessionStorage.getItem('token');
if (token) headers['Authorzation'] = token;
// 基于FETCH发送请求
config = {
method: method.toUpperCase(),
credentials,
cache,
headers
};
if (/^(POST|PUT|PATCH)$/i.test(method) && body) config.body = body;
return fetch(url, config)
.then(response => {
let {
status,
statusText
} = response;
if (status >= 200 && status < 400) {
let result;
switch (responseType.toUpperCase()) {
case 'JSON':
result = response.json();
break;
case 'TEXT':
result = response.text();
break;
case 'ARRAYBUFFER':
result = response.arrayBuffer();
break;
case 'BLOB':
result = response.blob();
break;
}
return result.then(null, reason => {
return Promise.reject({
code: 'format error',
reason
});
});
}
return Promise.reject({
code: "status error",
status,
statusText,
});
}).catch(reason => {
// 失败统一处理
if (reason && reason.code) {
if (reason.code === 'format error') {
Message.error(`小主,服务器返回的数据格式化失败~~`);
}
if (reason.code === 'status error') {
switch (reason.status) {
case 403:
Message.error(`小主,服务器拒绝了您的请求~~`);
break;
case 404:
Message.error(`小主,您请求的地址是错误的~~`);
break;
case 500:
Message.error(`小主,服务器开小差了,您稍后再试~~`);
break;
}
}
} else {
Message.error(`小主,当前网络繁忙,请稍后再试~~`);
}
return Promise.reject(reason);
});
};
//调用
request("/user/login", {
method: "POST",
body: {
account: "18310612838",
password: md5("1234567890"),
},
}).then((value) => {
console.log(value);
});
request("/user/list2", {
params: {
departmentId: 0,
search: "",
},
}).then((value) => {
console.log(value);
});