JS高阶(七)彻底掌握基于HTTP网络层的“前端性能优化”

313 阅读26分钟

彻底掌握基于HTTP网络层的“前端性能优化”

1、产品性能优化方案

  • HTTP网络层优化
  • 代码便一层优化 webpack
  • 代码运行层优化 html/css + javascript + vue + react
  • 安全优化 xss + csrf
  • 数据埋点及性能监控
  • .........

2、CRP(Critical [ˈkrɪtɪkl] Rendering [ˈrendərɪŋ] Path)关键渲染路径

3、从输入URL地址到看到页面,中间都经历了啥

Snipaste_2021-08-29_21-20-57

正常的步骤:【第一次访问页面】
  • URL解析
  • DNS解析:找到服务器外网IP
  • TCP三次握手:找到服务器并且建立好通信的通道
  • 通信
  • TCP四次挥手:关闭通道
  • 渲染:客户端把从服务器获取的信息进行渲染
第二次访问,我们休要保证比第一次更快【我们需要基于缓存机制来完成】
  • URL解析
  • 缓存检查(第二次访问增加缓存检查)
  • DNS解析 找到服务器外网IP
  • TCP三次握手:找到服务器并且建立好通信的通道
  • 通信
  • TCP四次挥手:关闭通道
  • 渲染:客户端把从服务器获取的信息进行渲染

输入域名到DNS服务器,通过DNS服务器获取到外网IP,客户端通过获取到的外网IP找到该服务器

微信图片_20210829210024


第一步:URL解析

  • 地址解析

    Snipaste_2021-08-29_21-23-21

传输协议:基于它实现客户端和服务器端的数据通信「快递小哥」 >

  • 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.html

escape & 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为主

微信图片_20210829225309

Expires是HTTP/1.0版本使用的,存储的是具体的过期时间 Cache-Control是HTTP/1.1版本使用的,可以基于 max-age=2592000 指定过期时间,单位是秒 两者都有则以最高支持的版本为主!!

HTML页面是不能做强缓存的:

​ html页面时项目的渲染入口,假设html也做了强缓存,设置过期时间为30天;那么以后30天内,我们访问页面,用的都是缓存的内容,即使服务器更新,客户端也不会看到最新的

HTML页面“绝对不能”设置强缓存,否则无法保证服务器资源更新,客户端可以随时获取最新的信息!!

清除强缓存的副作用

清除强缓存的副作用

可使用拼接时间戳实现页面更新(src:文件?时间戳)

微信图片_20210829225707

浏览器对于清缓存的处理:根据第一次请求资源时返回的响应头来确定的
  • Expires:缓存过期时间,用来指定资源到期的时间(HTTP/1.0)
  • Cache-Control:cache-control:max-age=2592000 第一次拿到资源后的2592000秒内(30天),再次发送请求,读取缓存中的信息(HTTP/1.1)
  • 两者同时存在的话,Cache-Control优先级高于Expires

Snipaste_2021-08-29_23-12-57

清除强缓存的副作用


协商缓存 Last-Modified / ETag

每一次请求,都需要和服务器进行协商「看服务器资源是否更新,如果有更新,则直接获取最新的,如果没有更新,则获取缓存」

协商缓存就是强制缓存失效后,浏览器携带缓存表示向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程

Snipaste_2021-08-29_23-40-57

缓存标识: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」;客户端会把最新的信息以及标识重新缓存在本地!!

每一次请求都需要问服务器是否更新,所以可以保证随时获取最新的信息;但是不如强缓存效率高!!

微信图片_20210829233709

微信图片_20210829233902

真实项目中,我们一般是:

  • HTML只做协商缓存

  • 其余的资源即做强化存也做协商缓存,这样在强缓存失效后,还可以基于协商缓存二次进行处理!!

    两种缓存都是“服务器设置”,客户端浏览器自动执行,无需前端编写代码;而且都是对静态资源文件的缓存处理!!


数据缓存

把不需要经常更新的数据接口,做缓存处理

Snipaste_2021-08-29_23-45-19

需求:本地有缓存数据且未过期,则从本地获取;本地缓存失效,则重新从服务器获取「类似于强缓存」;
// 首先校验本地缓存是否生效
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解析

  • 递归查询
  • 迭代查询

Snipaste_2021-08-30_00-11-05

每一次DNS解析时间预计在20~120毫秒
  • 减少DNS请求次数
  • DNS预获取(DNS Prefetch)
服务器拆分的优势
  • 资源的合理利用
  • 抗压能力加强
  • 提高HTTP并发
  • ......

Snipaste_2021-08-30_00-37-31


第四步:TCP三次握手

TCP:稳定、可靠的通信机制 UDP:快速、不稳定的通信机制(直播流可使用)

  • sql序号,用来标识从TCP源端想目的端发送的字节流,发起方发送数据对此进行标记
  • ack确认序号,只有ACK标志位为1时,确认序号字段才有效,ack=seq+1
  • 标志位
    • ACK:确认序号有效
    • RST:重置连接
    • SYN:发起一个新的连接
    • FIN:释放一个连接
    • ......

Snipaste_2021-08-30_00-43-40

三次握手为什么不用两次,或者四次?

如果只是用两次,则无法保证通信的顺畅;如果是用四次,第四次时多余的,减少不必要的性能浪费;

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通道建立好之后,客户端把信息给了服务器的时候,由客户端主动发送释放连接的请求

Snipaste_2021-08-30_00-51-51

为什么连接的时候是三次握手,关闭的时候却是四次握手?

  • 服务器端收到客户端的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

Snipaste_2021-08-30_01-01-21

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】

Snipaste_2021-08-31_22-11-41

  • 客户端渲染

Snipaste_2021-08-31_22-14-41

为啥会产生跨域

  • 服务器分离: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管理

Snipaste_2021-08-31_22-18-27

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
  • 不设置“*”,只能设置“单一原”,但是这样可以携带资源凭证

我们自己会搞一套白名单

客户端向服务器发送请求

微信图片_20210831223716

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页面
  • 帮助我们从其他服务器获取数据

微信图片_20210831224050

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实现反向代理

Snipaste_2021-08-31_22-27-01

处理原理

Snipaste_2021-08-31_22-31-16

自己基于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并发管控

sa

方案一:基于创造多个工作区,实现并发管控(常用方案)

// 模拟数据请求
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->通知计划表中的任务执行

订阅发布

微信图片_20211230153848

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); 

思路二:全局只有一个自定义事件池,基于自定义事件名称来区分要执行的方法

例如:把需要做的事情放入到容器中,当需要用的时候只需通知执行即可

微信图片_20211230154237

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);
};

数组塌陷

什么叫数组坍塌?当数组执行删除单元操作时,被删除单元,之后的单元,会前移,进而顶替被删除单元,出现在被删除单元的位置上,造成数组长度减少的情况,这样的现象称为数组的坍塌。

Snipaste_2021-12-31_14-16-56

当我们知道数组塌陷这个原因之后,再删除元素的时候,不应该直接删掉,而应该是使用 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 发送请求的】

www.axios-js.com/zh-cn/docs/

向服务器发送请求 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)
      }
    

    Snipaste_2021-09-02_23-50-26

  • 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',

    ​ .....

    ​ }

    Snipaste_2021-09-03_00-08-50

    @2 请求超时 或者 请求中断了

    ​ reason = {

    ​ code:"ECONNABORTED",表示请求超时

    ​ response:undefined,

    ​ ....

    ​ }

    Snipaste_2021-09-03_00-12-00

    @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格式字符串传递给服务器

    Snipaste_2021-09-03_01-08-27

  • 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发送请求,就可以简化一些了

对于大型项目来讲,如果需要封装多套

  • 把多套公共的部分提取出来
  • 不同的部分单独封装

请求中某几个请求和封装的不同,只需要在发送请求的时候单独配置,这样一定是以自己单独配置的为主

微信图片_20211228012150

基础配置

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');

Snipaste_2021-12-29_16-46-48

Snipaste_2021-12-29_16-58-58

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);
    });