zhangxk-notes

GET VS POST

http 请求方法

方法说明
GET请求指定的资源。一般来说 GET 方法应该只用于数据的读取,而不应当用于会产生副作用的非幂等的操作中。它期望的应该是是安全的和幂等的。这里的安全指的是,请求不会影响到资源的状态
HEADHEAD 方法与 GET 方法一样,都是向服务器发出指定资源的请求。但是,服务器在响应 HEAD 请求时不会回传资源的内容部分,即:响应主体。这样,我们可以不传输全部内容的情况下,就可以获取服务器的响应头信息。HEAD 方法常被用于客户端查看服务器的性能。
POSTPOST 请求会向指定资源提交数据,请求服务器进行处理,如:表单数据提交、文件上传等,请求数据会被包含在请求体中。POST 方法是非幂等的方法,因为这个请求可能会创建新的资源或/和修改现有资源。
PUTPUT 请求会身向指定资源位置上传其最新内容,PUT 方法是幂等的方法。通过该方法客户端可以将指定资源的最新数据传送给服务器取代指定的资源的内容。
OPTIONSOPTIONS 请求与 HEAD 类似,一般也是用于客户端查看服务器的性能。 这个方法会请求服务器返回该资源所支持的所有 HTTP 请求方法,该方法会用'*'来代替资源名称,向服务器发送 OPTIONS 请求,可以测试服务器功能是否正常。JavaScript 的 XMLHttpRequest 对象进行 CORS 跨域资源共享时,就是使用 OPTIONS 方法发送嗅探请求,以判断是否有对指定资源的访问权限。

攻击

CSRF / XSRF(跨站请求伪造)盗用了身份,浏览危险网站 token,添加验证码、密码等,涉及到数据修改操作严格使用 post 请求而不是 get 请求

XSS/CSS(跨站脚本攻击)植入恶意脚本 对用户输入内容和服务端返回内容进行过滤和转译

ClickJacking(点击劫持)利用透明 iframe 覆盖原网页诱导用户进行某些操作达成目的。

缓存

强制缓存优先于协商缓存进行,若强制缓存(Expires 和 Cache-Control)生效则直接使用缓存,若不生效则进行协商缓存

(Last-Modified / If-Modified-Since 和 Etag / If-None-Match),协商缓存由服务器决定是否使用缓存,若协商缓存效,那么代表该请求的缓存失效,重新获取请求结果,再存入浏览器缓存中;生效则返回 304,继续使用缓存

Expires 是到时间点过期,Cache-Control 可以设置一段时间后过期(精确到秒)。

Memory Cache

Memory Cache 也就是内存中的缓存,主要包含的是当前中页面中已经抓取到的资源,例如页面上已经下载的样式、脚本、图片等。读取内存中的数据肯定比磁盘快,内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放。 一旦我们关闭 Tab 页面,内存中的缓存也就被释放了。内存缓存在缓存资源时并不关心返回资源的 HTTP 缓存头 Cache-Control 是什么值,同时资源的匹配也并非仅仅是对 URL 做匹配,还可能会对 Content-Type,CORS 等其他特征做校验。

Disk Cache

Disk Cache 也就是存储在硬盘中的缓存,读取速度慢点,但是什么都能存储到磁盘中,比之 Memory Cache 胜在容量和存储时效性上。它会根据 HTTP Herder 中的字段判断哪些资源需要缓存,哪些资源可以不请求直接使用,哪些资源已经过期需要重新请求。并且即使在跨站点的情况下,相同地址的资源一旦被硬盘缓存下来,就不会再次去请求数据。绝大部分的缓存都来自 Disk Cache。

对于大文件来说,大概率是不存储在内存中的,反之优先; 当前系统内存使用率高的话,文件优先存储进硬盘

用户行为对浏览器缓存的影响

浏览器的渲染过程主要包括以下几步:

  1. 解析 HTML 生成 DOM 树。
  2. 解析 CSS 生成 CSSOM 规则树
  3. 将 DOM 树与 CSSOM 规则树合并在一起生成渲染树
  4. 遍历渲染树开始布局,计算每个节点的位置大小信息。
  5. 将渲染树每个节点绘制到屏幕。

从输入 URL 到页面展示到底发生了什么?

  1. 输入地址
  2. 浏览器查找域名的 IP 地址
    1. 浏览器会首先查看本地硬盘的 hosts 文件,看看其中有没有和这个域名对应的规则,如果有的话就直接使用 hosts 文件里面的 ip 地址。
    2. 如果在本地的 hosts 文件没有能够找到对应的 ip 地址,浏览器会发出一个 DNS 请求到本地 DNS 服务器
      1. 本地 DNS 服务器会首先查询它的缓存记录,如果缓存中有此条记录,就可以直接返回结果,此过程是递归的方式进行查询。如果没有,本地 DNS 服务器还要向 DNS 根服务器进行查询。
      2. ...
  3. 浏览器向 web 服务器发送一个 HTTP 请求
    1. 建立 TCP/IP 的连接(三次握手),发起一个 http 请求
    2. 三次握手的目的:防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误
  4. 服务器的永久重定向响应
    1. 301 表示旧地址 A 的资源已经被永久地移除了(这个资源不可访问了),搜索引擎在抓取新内容的同时也将旧的网址交换为重定向之后的网址
    2. 302 表示旧地址 A 的资源还在(仍然可以访问),这个重定向只是临时地从旧地址 A 跳转到地址 B,搜索引擎会抓取新的内容而保存旧的网址
  5. 浏览器跟踪重定向地址(发送另一个 http)
  6. 服务器处理请求
  7. 服务器返回一个 HTTP 响应
    1. 1xx:信息性状态码,表示服务器已接收了客户端请求,客户端可继续发送请求。
    2. 2xx:成功状态码,表示服务器已成功接收到请求并进行处理。
    3. 3xx:重定向状态码,表示服务器要求客户端重定向。
    4. 4xx:客户端错误状态码,表示客户端的请求有非法内容。
    5. 5xx:服务器错误状态码,表示服务器未能正常处理客户端的请求而出现意外错误。
  8. 浏览器显示 HTML
    1. 浏览器在解析 html 文件时,会自上而下加载,并在加载过程中进行解析渲染。在解析过程中,如果遇到请求外部资源时,如图片、外链的 CSS、iconfont 等,请求过程是异步的,并不会影响 html 文档进行加载。
    2. 浏览器首先会解析 HTML 文件构建 DOM 树,然后解析 CSS 文件构建渲染树,等到渲染树构建完成后,浏览器开始布局渲染树并将其绘制到屏幕上。
    3. 当文档加载过程中遇到 js 文件,html 文档会挂起渲染(加载解析渲染同步)的线程,不仅要等待文档中 js 文件加载完毕,还要等待解析执行完毕,才可以恢复 html 文档的渲染线程。
  9. 浏览器发送请求获取嵌入在 HTML 中的资源(如图片、音频、视频、CSS、JS 等等)
    1. 其实这个步骤可以并列在步骤 8 中,在浏览器显示 HTML 时,它会注意到需要获取其他地址内容的标签。
    2. 这些地址都要经历一个和 HTML 读取类似的过程。所以浏览器会在 DNS 中查找这些域名,发送请求,重定向等等…
    3. 静态文件会允许浏览器对其进行缓存

跨域

因为浏览器出于安全考虑,有同源策略。也就是说,如果协议、域名或者端口有一个不同就是跨域,Ajax 请求会失败。 其实主要是用来防止 CSRF 攻击的。简单点说,CSRF 攻击是利用用户的登录态发起恶意请求。

跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了。因为归根结底,跨域是为了阻止用户读取到另一个域名下的内容,Ajax 可以获取响应,浏览器认为这不安全,所以拦截了响应。

jsonp

<script> 标签没有跨域,通过 <script>标签指向一个需要访问的地址并提供一个回调函数来接收数据。

JSONP 使用简单且兼容性不错,但是只限于 get 请求。

<script src="http://domain/api?param1=a&param2=b&callback=jsonp"></script>
<script>
    function jsonp(data) {
        console.log(data)
    }
</script>

CORS

当你使用 XMLHttpRequest 发送请求时,浏览器发现该请求不符合同源策略,会给该请求加一个请求头:Origin,后台进行一系列处理,如果确定接受请求则在返回结果中加入一个响应头:Access-Control-Allow-Origin;  浏览器判断该相应头中是否包含 Origin 的值,如果有则浏览器会处理响应,我们就可以拿到响应数据。

服务端设置 Access-Control-Allow-Origin 就可以开启 CORS

通过这种方式解决跨域问题的话,会在发送请求时出现两种情况,分别为简单请求和复杂请求。

当满足以下条件时,会触发简单请求

  1. 使用下列方法之一:GET、HEAD、POST
  2. Content-Type 的值仅限于下列三者之一:text/plain、multipart/form-data、application/x-www-form-urlencoded

对于复杂请求来说,首先会发起一个预检请求,该请求是 option 方法的,通过该请求来知道服务端是否允许跨域请求。

document.domain

该方式只能用于主域名相同的情况下,比如 a.test.comb.test.com 适用于该方式。只需要给页面添加 document.domain = 'test.com' 表示主域名都相同就可以实现跨域

postMessage

这种方式通常用于获取嵌入页面中的第三方页面数据。一个页面发送消息,另一个页面判断来源并接收消息

// 发送消息端
window.parent.postMessage("hello", "http://test.com");

// 接收消息端
window.addEventListener("message", (event) => {
  var origin = event.origin || event.originalEvent.origin;
  if (origin === "http://test.com") {
    console.log("验证通过");
  }
});

为什么操作 DOM 慢

因为 DOM 是属于渲染引擎中的东西,而 JS 又是 JS 引擎中的东西。当我们通过 JS 操作 DOM 的时候,其实这个操作涉及到了两个线程之间的通信,那么势必会带来一些性能上的损耗。操作 DOM 次数一多,也就等同于一直在进行线程之间的通信,并且操作 DOM 可能还会带来重绘回流的情况,所以也就导致了性能上的问题。

插入几万个 DOM,如何实现页面不卡顿?

解决问题的重点应该是如何分批次部分渲染 DOM:

  1. 通过 requestAnimationFrame 的方式去循环的插入 DOM
  2. 虚拟滚动(virtualized scroller),只渲染可视区域内的内容,非可见区域的那就完全不渲染了,当用户在滚动的时候就实时去替换渲染的内容。

defer 和 async 的区别

defer VS async

  1. defer 和 async 在网络读取(下载)这块儿是一样的,都是异步的(相较于 HTML 解析)
  2. 它俩的差别在于脚本下载完之后何时执行,显然 defer 是最接近我们对于应用脚本加载和执行的要求的,所有元素解析完成之后执行
  3. 关于 defer,此图未尽之处在于它是按照加载顺序执行脚本的,这一点要善加利用
  4. async 则是一个乱序执行的主,反正对它来说脚本的加载和执行是紧紧挨着的,所以不管你声明的顺序如何,只要它加载完了就会立刻执行
  5. 仔细想想,async 对于应用脚本的用处不大,因为它完全不考虑依赖(哪怕是最低级的顺序执行),不过它对于那些可以不依赖任何脚本或不被任何脚本依赖的脚本来说却是非常合适的

路由 hash 模式和 history 模式的区别

hash 模式 url 带#号,history 模式不带#号。

hash 模式:hash 虽然出现在 URL 中,但不会被包括在 HTTP 请求中,对后端完全没有影响,因此改变 hash 不会重新加载页面。监听 window.onhashchange 事件。带#号不美观。对外分享链接的时候,#后边的内容可能会丢失。

history 模式:利用了 HTML5 History Interface 中新增的 pushState() 和 replaceState() 方法。这两个方法应用于浏览器的历史记录栈,在当前已有的 back()、forward()、go() 方法的基础之上,这两个方法提供了对历史记录进行修改的功能。当这两个方法执行修改时,只能改变当前地址栏的 URL,但浏览器不会向后端发送请求,也不会触发 popstate 事件的执行。但是页面刷新会重新请求,可能会导致页面 404,需要后端配合支持(nginx 路由转发重定向等)。

轮询、长轮询(comet)、长连接(SSE)、WebSocket

轮询

浏览器每隔一段时间向浏览器发送 http 请求,服务器端在收到请求后,不论是否有数据更新,都直接进行响应。需要不断的建立 http 连接,严重浪费了服务器端和客户端的资源。

长轮询(comet)

长轮询本质上也是轮询,只不过对普通的轮询做了优化处理,服务端在没有数据的时候并不是马上返回数据,会 hold 住请求,等待服务端有数据,或者一直没有数据超时处理,然后一直循环下去。长轮询和短轮询比起来,明显减少了很多不必要的 http 请求次数,相比之下节约了资源。长轮询的缺点在于,连接挂起也会导致资源的浪费。

长连接(SSE)

服务器向客户端声明,接下来要发送的是流信息(streaming)。也就是说,发送的不是一次性的数据包,而是一个数据流,会连续不断地发送过来。这时,客户端不会关闭连接,会一直等着服务器发过来的新的数据流,视频播放就是这样的例子。SSE 是单向通道,只能服务器向浏览器发送,因为流信息本质上就是下载。

Http/1.1 想出了持久化连接。只要任意一端没有明确的提出 断开连接,则保持 TCP 连接状态。通过首部字段 Connection:Keep-Alive 实现。Http/1.1 默认为长连接。

WebSocket

服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话。没有同源限制,客户端可以与任意服务器通信。

websocket 心跳及重连机制

websocket 是前后端交互的长连接,前后端也都可能因为一些情况导致连接失效并且相互之间没有反馈提醒。因此为了保证连接的可持续性和稳定性,websocket 心跳重连就应运而生。心跳机制是每隔一段时间会向服务器发送一个数据包,告诉服务器自己还活着,同时客户端会确认服务器端是否还活着,如果还活着的话,就会回传一个数据包给客户端来确定服务器端也还活着,否则的话,有可能是网络断开连接了,需要重连。

大文件分片上传

Blob 对象表示一个不可变、原始数据的类文件对象。它的数据可以按文本或二进制的格式进行读取,同时提供了对数据截取的方法 slice,而 file 继承了 Blob 的功能,所以可以使用此方法将读取的文件进行分片切割,用以拼凑准备上传的数据。

  1. 把大文件进行分段 比如 2M,发送到服务器携带一个标志,暂时使用当前的时间戳,用于标识一个完整文件
  2. 服务端保存各段文件;
  3. 浏览器端所有分片上传完成,发送给服务端一个合并文件的请求
  4. 服务端根据文件标识、类型、各分片顺序进行文件合并

Service Worker

Service Worker 是一个在浏览器后台运行,和当前页面没有关联的脚本,后台运行,用来实现一些不依赖页面或者用户交互的特性,比方说缓存啊,消息推送之类。我们平常浏览器窗口中跑的页面运行的是主 JavaScript 线程,DOM 和 window 全局变量都是可以访问的。而 Service Worker 是走的另外的线程(不会阻塞主线程),可以理解为在浏览器背后默默运行的一个线程,脱离浏览器窗体,因此,window 以及 DOM 都是不能访问的,此时我们可以使用 self 访问全局上下文。

使用 Service Worker 必须是 https 协议。

// 主线程里代码注册
if ("serviceWorker" in navigator) {
  navigator.serviceWorker
    .register("./sw.js", {
      scope: "./", // url 作用域是 ./
    })
    .then(function (registration) {
      console.log("注册成功");
    })
    .catch(function (error) {
      console.log("注册失败");
    });
}

// sw.js 代码
self.addEventListener("install", function (event) {
  /* 安装后... */
});
self.addEventListener("activate", function (event) {
  /* 激活后... */
});
self.addEventListener("fetch", function (event) {
  /* 请求后... */
});

install 用来缓存文件,activate 用来缓存更新,fetch 用来拦截请求直接返回缓存数据。三者齐心,构成了完成的缓存控制结构。

Web Worker

Web Worker 的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。

主线程采用 new 命令,调用 Worker() 构造函数,新建一个 Worker 线程。

// Worker()构造函数的参数是一个脚本文件,该文件就是 Worker 线程所要执行的任务。由于 Worker 不能读取本地文件,所以这个脚本必须来自网络。
var worker = new Worker("work.js");

主线程和子线程通过 postMessage 传递信息。需要注意的是,这种通信是拷贝关系,即是传值而不是传址,Worker 对通信内容的修改,不会影响到主线程。

使用场景

TCP 三次握手

TCP 三次握手,其实就是建立一个 TCP 连接,客户端与服务器交互需要 3 个数据包。握手的主要作用就是为了确认双方的接收和发送能力是否正常,初始序列号,交换窗口大小以及 MSS 等信息。

  1. 第一次握手:客户端发送 SYN 报文,并进入 SYN_SENT 状态,等待服务器的确认;
  2. 第二次握手:服务器收到 SYN 报文,需要给客户端发送 ACK 确认报文,同时服务器也要向客户端发送一个 SYN 报文,所以也就是向客户端发送 SYN + ACK 报文,此时服务器进入 SYN_RCVD 状态;
  3. 第三次握手:客户端收到 SYN + ACK 报文,向服务器发送确认包,客户端进入 ESTABLISHED 状态。待服务器收到客户端发送的 ACK 包也会进入 ESTABLISHED 状态,完成三次握手

为什么 TCP 采用三次握手,二次握手可以吗?

四次挥手

  1. 当主动方发送断开连接的请求(即 FIN 报文)给被动方时,仅仅代表主动方不会再发送数据报文了,但主动方仍可以接收数据报文。
  2. 被动方此时有可能还有相应的数据报文需要发送,因此需要先发送 ACK 报文,告知主动方“我知道你想断开连接的请求了”。这样主动方便不会因为没有收到应答而继续发送断开连接的请求(即 FIN 报文)。
  3. 被动方在处理完数据报文后,便发送给主动方 FIN 报文;这样可以保证数据通信正常可靠地完成。发送完 FIN 报文后,被动方进入 LAST_ACK 阶段(超时等待)。
  4. 如果主动方及时发送 ACK 报文进行连接中断的确认,这时被动方就直接释放连接,进入可用状态。
  1. 服务端设置 Access-Control-Allow-Credentials: true
  2. 服务端的 Access-Control-Allow-Origin 不能是 *
  3. 客户端 xhr.withCredentials = true

Cookie,Session 与 Token

cookie 是一个非常具体的东西,指的就是浏览器里面能永久存储的一种数据,仅仅是浏览器实现的一种数据存储功能。特点就是同源限制,每次请求都携带

Session

session 从字面上讲,就是会话。是一种在服务端临时保存的用户身份数据的手段,或者说保存的用户信息数据就是 session。客户端请求的时候,带上自己的身份标识(由服务端下发的),服务端去在信息库去比对。客服端携带信息的手段可以是使用 cookie,当然也可以在 url 上携带。服务端的 session 是临时的,会话维度的,会话结束就被销毁。涉及负载均衡多台实例的时候需要做同步处理。

Token

Token 是无状态的验证,不会把 Token 数据保存在服务端。Token 的核心是算法。

  1. 用户通过用户名和密码发送请求。
  2. 服务端验证。
  3. 服务端返回一个签名的 token 给客户端。(token = 用户信息数据 + 用户信息数据的算法签名)
  4. 客户端储存 token,并且每次用于每次发送请求。(可以用 Cookie 携带信息)
  5. 服务端验证 token 并返回数据。(验证:对用户信息数据进行算法签名,结果是否等于 token 里的签名)