想要成为一名合格的前端工程师,掌握相关浏览器的工做原理是必备的,这样子才会有一个完整知识体系,要是「能参透浏览器的工做原理,你就能解决80%的前端难题」。php
这篇梳理的话,更多的是对浏览器工做原理篇的查缺补漏,对于一些没有涉及到的知识点,准备梳理梳理,也正好回顾以前梳理的内容。css
感谢掘友的鼓励与支持🌹🌹🌹,往期文章都在最后梳理出来了(●'◡'●)html
「接下来以问题形式展开梳理」前端
浏览器/RunTime | 内核(渲染引擎) | JavaScript 引擎 |
---|---|---|
Chrome | webkit->blink | V8 |
FireFox | Gecko | SpiderMonkey |
Safari | Webkit | JavaScriptCore |
Edge | EdgeHTML | Chakra(for JavaScript) |
IE | Trident | JScript(IE3.0-IE8.0) |
Opera | Presto->blink | Linear A(4.0-6.1)/ Linear B(7.0-9.2)/ Futhark(9.5-10.2)/ Carakan(10.5-) |
Node.js | - | V8 |
值得注意的是,和大多数浏览器不一样,Chrome 浏览器的每一个标签页都分别对应一个呈现引擎实例。每一个标签页都是一个独立的进程。jquery
补充:JS中实际上是没有线程概念的,所谓的单线程也只是相对于多线程而言。JS的设计初衷就没有考虑这些,针对JS这种不具有并行任务处理的特性,咱们称之为“单线程”。nginx
JS的单线程是指一个浏览器进程中只有一个JS的执行线程,同一时刻内只会有一段代码在执行。程序员
举个通俗例子,假设JS支持多线程操做的话,JS能够操做DOM,那么一个线程在删除DOM,另一个线程就在获取DOM数据,这样子明显不合理,这算是证实之一。web
来看段代码👇面试
function foo() { console.log("first"); setTimeout(( function(){ console.log( 'second' ); }),5); } for (var i = 0; i < 1000000; i++) { foo(); } 复制代码
打印结果就是首先是不少个first,而后再是second。ajax
异步机制是浏览器的两个或以上常驻线程共同完成的,举个例子,好比异步请求由两个常驻线程,JS执行线程和事件触发线程共同完成的。
再好比定时器触发(settimeout和setinterval) 是由「浏览器的定时器线程」执行的定时计数,而后在定时时间把定时处理函数的执行请求插入到JS执行队列的尾端(因此用这两个函数的时候,实际的执行时间是大于或等于指定时间的,不保证能准肯定时的)。
因此这么说,JS单线程与异步更可能是浏览器行为,之间不冲突。
先给出结论
CSS
不会阻塞DOM
解析,但会阻塞DOM
渲染。CSS
会阻塞JS执行,并不会阻塞JS文件下载先讲一讲CSSOM做用
由以前讲过的浏览器渲染流程咱们能够看出:
DOM 和 CSSOM一般是并行构建的,因此「CSS 加载不会阻塞 DOM 的解析」。
然而因为Render Tree 是依赖DOM Tree和 CSSOM Tree的,因此它必须等到二者都加载完毕后,完成相应的构建,才开始渲染,所以,「CSS加载会阻塞DOM渲染」。
因为 JavaScript 是可操纵 DOM 和 css 样式 的,若是在修改这些元素属性同时渲染界面(即 JavaScript 线程和 UI 线程同时运行),那么渲染线程先后得到的元素数据就可能不一致了。
所以为了防止渲染出现不可预期的结果,浏览器设置 「GUI 渲染线程与 JavaScript 引擎为互斥」的关系。
有个须要注意的点就是:
「有时候JS须要等到CSS的下载,这是为何呢?」
仔细思考一下,其实这样作是有道理的,若是脚本的内容是获取元素的样式,宽高等CSS
控制的属性,浏览器是须要计算的,也就是依赖于CSS
。浏览器也没法感知脚本内容究竟是什么,为避免样式获取,于是只好等前面全部的样式下载完后,再执行JS
。
JS文件下载和CSS文件下载是并行的,有时候CSS文件很大,因此JS须要等待。
所以,样式表会在后面的 js 执行前先加载执行完毕,因此「css 会阻塞后面 js 的执行」。
先给出结论👇
这也是为何说JS文件放在最下面的缘由,那为何会阻塞DOM解析呢
你能够这样子理解:
因为 JavaScript 是可操纵 DOM 的,若是在修改这些元素属性同时渲染界面(即 JavaScript 线程和 UI 线程同时运行),那么渲染线程先后得到的元素数据就可能不一致了。
所以为了防止渲染出现不可预期的结果,浏览器设置 「GUI 渲染线程与 JavaScript 引擎为互斥」的关系。
当 JavaScript 引擎执行时 GUI 线程会被挂起,GUI 更新会被保存在一个队列中等到引擎线程空闲时当即被执行。
当浏览器在执行 JavaScript 程序的时候,GUI 渲染线程会被保存在一个队列中,直到 JS 程序执行完成,才会接着执行。
所以若是 JS 执行的时间过长,这样就会形成页面的渲染不连贯,致使页面渲染加载阻塞的感受。
另外,若是 JavaScript 文件中没有操做 DOM 相关代码,就能够将该 JavaScript 脚本设置为异步加载,经过 async 或 defer 来标记代码
DOMContentLoaded
事件前执行,若是缺乏 src
属性(即内嵌脚本),该属性不该被使用,由于这种状况下它不起做用那么也就是先DOMContentLoaded -> load,那么在Jquery中,使用(document).load(callback)监听的就是load事件。
那咱们能够聊一聊它们与async和defer区别
带async的脚本必定会在load事件以前执行,可能会在DOMContentLoaded以前或以后执行。
若是 script 标签中包含 defer,那么这一块脚本将不会影响 HTML 文档的解析,而是等到HTML 解析完成后才会执行。而 DOMContentLoaded 只有在 defer 脚本执行结束后才会被触发。
我以为这个题目说法上可能就是行不通,不能这么说,若是了解的话,都知道will-change只是一个优化的手段,使用JS改变transform也能够享受这个属性带来的变化,因此这个说法上有点不妥。
因此围绕这个问题展开话,更应该说建议推荐使用CSS动画,至于为何呢,涉及的知识点大概就是重排重绘,合成,这方面的点,我在浏览器渲染流程中也说起了。
尽量的避免重排和重绘,具体是哪些操做呢,若是非要去操做JS实现动画的话,有哪些优化的手段呢?
好比👇
createDocumentFragment
进行批量的 DOM 操做剩下的东西就留给大家思考吧,但愿我这是抛砖引玉吧(●'◡'●)
节流的意思是让函数有节制地执行,而不是毫无节制的触发一次就执行一次。什么叫有节制呢?就是在一段时间内,只执行一次。
规定在一个单位时间内,只能触发一次函数。若是这个单位时间内触发屡次函数,只有一次生效。
抓取一个关键的点:就是执行的时机。要作到控制执行的时机,咱们能够经过「一个开关」,与定时器setTimeout结合完成。
function throttle(fn, delay) { let flag = true, timer = null; return function (...args) { let context = this; if (!flag) return; flag = false; clearTimeout(timer) timer = setTimeout(() => { fn.apply(context, args); flag = true; }, delay); }; }; 复制代码
在事件被触发n秒后再执行回调,若是在这n秒内又被触发,则从新计时。
核心思想:每次事件触发都会删除原有定时器,创建新的定时器。通俗意思就是反复触发函数,只认最后一次,从最后一次开始计时。
代码:
function debounce(fn, delay) { let timer = null return function (...args) { let context = this if(timer) clearTimeout(timer) timer = setTimeout(function() { fn.apply(context, args) },delay) } } 复制代码
本身造一个 debounce / throttle 的轮子看起来多么诱人,或者随便找个博文复制过来。「我是建议直接使用 underscore 或 Lodash」 。若是仅须要 _.debounce
和 _.throttle
方法,可使用 Lodash 的自定义构建工具,生成一个 2KB 的压缩库。使用如下的简单命令便可:
npm i -g lodash-cli npm i -g lodash-clilodash-cli include=debounce,throttle 复制代码
常见的坑是,不止一次地调用 _.debounce
方法:
// 错误 $(window).on('scroll', function() { _.debounce(doSomething, 300); }); // 正确 $(window).on('scroll', _.debounce(doSomething, 200)); 复制代码
debounce 方法保存到一个变量之后,就能够用它的私有方法 debounced_version.cancel()
,lodash 和 underscore.js 都有效。
let debounced_version = _.debounce(doSomething, 200); $(window).on('scroll', debounced_version); // 若是须要的话debounced_version.cancel(); 复制代码
防抖
节流
正好跟节流有点关系,有点类似处,就准备梳理一下这个知识点。
动画帧率能够做为衡量标准,通常来讲画面在 60fps 的帧率下效果比较好。
换算一下就是,每一帧要在 16.7ms (16.7 = 1000/60) 内完成渲染。
咱们来看看MDN对它的解释吧👇
window.requestAnimationFrame() 方法告诉浏览器您但愿执行动画并请求浏览器在下一次重绘以前调用指定的函数来更新动画。该方法使用一个回调函数做为参数,这个回调函数会在浏览器重绘以前调用。-- MDN
当咱们调用这个函数的时候,咱们告诉它须要作两件事:
rAF(requestAnimationFrame) 最大的优点是「由系统来决定回调函数的执行时机」。
具体一点讲就是,系统每次绘制以前会主动调用 rAF 中的回调函数,若是系统绘制率是 60Hz,那么回调函数就每16.7ms 被执行一次,若是绘制频率是75Hz,那么这个间隔时间就变成了 1000/75=13.3ms。
换句话说就是,rAF 的执行步伐跟着系统的绘制频率走。它能保证回调函数在屏幕每一次的绘制间隔中只被执行一次(上一个知识点刚刚梳理完「函数节流」),这样就不会引发丢帧现象,也不会致使动画出现卡顿的问题。
另外它能够自动调节频率。若是callback工做太多没法在一帧内完成会自动下降为30fps。虽然下降了,但总比掉帧好。
与setTimeout动画对比的话,有如下几点优点
规范中彷佛是这么去定义的:
这样子分析的话,彷佛很合理嘛,为何要在从新渲染前去调用呢?由于rAF做为官方推荐的一种作流畅动画所应该使用的API,作动画不可避免的去操做DOM,而若是是在渲染后去修改DOM的话,那就只能等到下一轮渲染机会的时候才能去绘制出来了,这样子彷佛不合理。
rAF
在浏览器决定渲染以前给你最后一个机会去改变 DOM 属性,而后很快在接下来的绘制中帮你呈现出来,因此这是作流畅动画的不二选择。
至于宏任务,微任务,这能够提及来就要展开篇幅了,暂时不在这里梳理了。
跟 _.throttle(dosomething, 16)
等价。它是高保真的,若是追求更好的精确度的话,能够用浏览器原生的 API 。
可使用 rAF API 替换 throttle 方法,考虑一下优缺点:
优势
缺点
根据经验,若是 JavaScript 方法须要绘制或者直接改变属性,我会选择 requestAnimationFrame
,只要涉及到从新计算元素位置,就可使用它。
涉及到 AJAX 请求,添加/移除 class (能够触发 CSS 动画),我会选择 _.debounce
或者 _.throttle
,能够设置更低的执行频率(例子中的200ms 换成16ms)。
页可见区域宽: document.body.clientWidth; 网页可见区域高: document.body.clientHeight; 网页可见区域宽: document.body.offsetWidth (包括边线的宽); 网页可见区域高: document.body.offsetHeight (包括边线的宽); 网页正文全文宽: document.body.scrollWidth; 网页正文全文高: document.body.scrollHeight; 网页被卷去的高: document.body.scrollTop; 网页被卷去的左: document.body.scrollLeft; 网页正文部分上: window.screenTop; 网页正文部分左: window.screenLeft; 屏幕分辨率的高: window.screen.height; 屏幕分辨率的宽: window.screen.width; 屏幕可用工做区高度: window.screen.availHeight; 复制代码
关于scrollTop,offsetTop,scrollLeft,offsetLeft用法介绍,点这里
img dom
scroll
事件固然了,为了用户的体验更加,默认的状况下,设置一个「占位图」
本次测试代码
CSS代码👇
<style> img{ display: block; height: 320px; margin-top: 20px; margin: 10px auto; } </style> 复制代码
HTML👇
<img src="default.png" data-src="https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1595328889118&di=1665d7e122bc96be92d0f3e1b2f5e302&imgtype=0&src=http%3A%2F%2Fwork.361ser.com%2FContent%2Fueditor%2Fnet%2Fupload%2Fimage%2F20171014%2F6364359407281350179759303.jpg" /> 复制代码
「clientHeight-scrollTop-offsetTop」
直接上我运行的代码👇
let Img = document.getElementsByTagName("img"), len = Img.length, count = 0; function lazyLoad () { let viewH = document.body.clientHeight, //可见区域高度 scrollTop = document.body.scrollTop; //滚动条距离顶部高度 for(let i = count; i < len; i++) { if(Img[i].offsetTop < scrollTop + viewH ){ if(Img[i].getAttribute('src') === 'default.png'){ Img[i].src = Img[i].getAttribute('data-src') count++; } } } } function throttle(fn, delay) { let flag = true, timer = null; return function (...args) { let context = this; if (!flag) return; flag = false; clearTimeout(timer) timer = setTimeout(() => { fn.apply(context, args); flag = true; }, delay); }; }; window.addEventListener('scroll', throttle(lazyLoad,1000)) lazyLoad(); // 首次加载 复制代码
使用 element.getBoundingClientRect()
API 直接获得 top 值。
代码👇
let Img = document.getElementsByTagName("img"), len = Img.length, count = 0; function lazyLoad () { let viewH = document.body.clientHeight, //可见区域高度 scrollTop = document.body.scrollTop; //滚动条距离顶部高度 for(let i = count; i < len; i++) { if(Img[i].getBoundingClientRect().top < scrollTop + viewH ){ if(Img[i].getAttribute('src') === 'default.png'){ Img[i].src = Img[i].getAttribute('data-src') count++; } } } } function throttle(fn, delay) { let flag = true, timer = null; return function (...args) { let context = this; if (!flag) return; flag = false; clearTimeout(timer) timer = setTimeout(() => { fn.apply(context, args); flag = true; }, delay); }; }; window.addEventListener('scroll', throttle(lazyLoad,1000)) lazyLoad(); // 首次加载 复制代码
好像也差很少,不知道是否是我写的方式有问题(●'◡'●),感受差很少
来看看效果吧,我给这个事件加了一个节流,这样子操做看起来就更好了。
得扯一下HTTP是一个无状态的协议
,这里主要指的是HTTP1.x版本,简单的能够理解为即便同一个客户端连续两次发送请求给服务器,服务器也没法识别这个同一个客户端发的请求,致使的问题,好比现实生活中你加入一个商品到购物车,可是由于没法识别同一个客户端,你刷新页面的话就🤭
为了解决 HTTP 无状态致使的问题(HTTP1.x),后来出现了 Cookie。
Cookie 的存在也不是为了解决通信协议无状态的问题,只是为了解决客户端与服务端会话状态的问题,这个状态是指后端服务的状态而非通信协议的状态。
Cookie存放在本地的好处就在于即便你关闭了浏览器,Cookie 依然能够生效。
怎么去设置呢?简单来讲就是👇
在下面这张图里咱们能够看到 Cookies 相关的一些属性👇
这里主要说一些你们可能没有注意的点:
用 JavaScript 操做 Cookie 的时候注意对 Value 进行编码处理。
Expires 用于设置 Cookie 的过时时间。好比:
Set-Cookie: id=aad3fWa; Expires=Wed, 21 May 2020 07:28:00 GMT;
复制代码
Max-Age 用于设置在 Cookie 失效以前须要通过的秒数。好比:
Set-Cookie: id=a3fWa; Max-Age=604800;
复制代码
假如 Expires 和 Max-Age 都存在,Max-Age 优先级更高。
Domain 指定了 Cookie 能够送达的主机名。假如没有指定,那么默认值为当前文档访问地址中的主机部分(可是不包含子域名)。
在这里注意的是,不能跨域设置 Cookie
Path 指定了一个 URL 路径,这个路径必须出如今要请求的资源的路径中才能够发送 Cookie 首部。好比设置 Path=/docs
,/docs/Web/
下的资源会带 Cookie 首部,/test
则不会携带 Cookie 首部。
「Domain 和 Path 标识共同定义了 Cookie 的做用域:即 Cookie 应该发送给哪些 URL。」
标记为 Secure 的 Cookie 只应经过被HTTPS协议加密过的请求发送给服务端。使用 HTTPS 安全协议,能够保护 Cookie 在浏览器和 Web 服务器间的传输过程当中不被窃取和篡改。
设置 HTTPOnly 属性能够防止客户端脚本经过 document.cookie 等方式访问 Cookie,有助于避免 XSS 攻击。
SameSite 属性可让 Cookie 在跨站请求时不会被发送,从而能够阻止跨站请求伪造攻击(CSRF)。
后面讲CSRF攻击会将讲到,这里过。
这个属性值修改有什么影响呢?
从上图能够看出,对大部分 web 应用而言,Post 表单,iframe,AJAX,Image 这四种状况从之前的跨站会发送三方 Cookie,变成了不发送。
Cookie 主要用于如下三个方面:
从大小,安全,增长请求大小。
4KB
,只能用来存储少许的信息。在 web 本地存储场景上,cookie 的使用受到种种限制,最关键的就是存储容量过小和数据没法持久化存储。
在 HTML 5 的标准下,出现了 localStorage 和 sessionStorage 供咱们使用。
分类 | 生命周期 | 存储容量 | 存储位置 |
---|---|---|---|
cookie | 默认保存在内存中,随浏览器关闭失效(若是设置过时时间,在到过时时间后失效) | 4KB | 保存在客户端,每次请求时都会带上 |
localStorage | 理论上永久有效的,除非主动清除。 | 4.98MB(不一样浏览器状况不一样,safari 2.49M) | 保存在客户端,不与服务端交互。节省网络流量 |
sessionStorage | 仅在当前网页会话下有效,关闭页面或浏览器后会被清除。 | 4.98MB(部分浏览器没有限制) | 同上 |
接下来咱们来具体看看如何来操做localStorage
和sessionStorage
let obj = { name: "TianTianUp", age: 18 }; localStorage.setItem("name", "TianTianUp"); localStorage.setItem("info", JSON.stringify(obj)); 复制代码
接着进入相同的域名时就能拿到相应的值👇
let name = localStorage.getItem("name"); let info = JSON.parse(localStorage.getItem("info")); 复制代码
从这里能够看出,localStorage
其实存储的都是字符串,若是是存储对象须要调用JSON
的stringify
方法,而且用JSON.parse
来解析成对象。
logo
,存储Base64
格式的图片资源等;浏览器缓存是性能优化的一个重要手段,对于理解缓存机制而言也是很重要的,咱们来梳理一下吧👇
强缓存两个相关字段,「Expires」,「Cache-Control」。
「强缓存分为两种状况,一种是发送HTTP请求,一种不须要发送。」
首先检查强缓存,这个阶段**不须要发送HTTP请求。**经过查找不一样的字段来进行,不一样的HTTP版本因此不一样。
Expires
即过时时间,时间是相对于服务器的时间而言的,存在于服务端返回的响应头中,在这个过时时间以前能够直接从缓存里面获取数据,无需再次请求。好比下面这样:
Expires:Mon, 29 Jun 2020 11:10:23 GMT
复制代码
表示该资源在2020年7月29日11:10:23
过时,过时时就会从新向服务器发起请求。
这个方式有一个问题:「服务器的时间和浏览器的时间可能并不一致」,因此HTTP1.1提出新的字段代替它。
HTTP1.1版本中,使用的就是该字段,这个字段采用的时间是过时时长,对应的是max-age。
Cache-Control:max-age=6000
复制代码
上面表明该资源返回后6000秒,能够直接使用缓存。
固然了,它还有其余不少关键的指令,梳理了几个重要的👇
注意点:
强缓存失效后,浏览器在请求头中携带响应的缓存Tag
来向服务器发送请求,服务器根据对应的tag,来决定是否使用缓存。
缓存分为两种,「Last-Modified」 和 「ETag」。二者各有优点,并不存在谁对谁有绝对的优点
,与上面所讲的强缓存两个Tag所不一样。
这个字段表示的是「最后修改时间」。在浏览器第一次给服务器发送请求后,服务器会在响应头中加上这个字段。
浏览器接收到后,「若是再次请求」,会在请求头中携带If-Modified-Since
字段,这个字段的值也就是服务器传来的最后修改时间。
服务器拿到请求头中的If-Modified-Since
的字段后,其实会和这个服务器中该资源的最后修改时间
对比:
ETag是服务器根据当前文件的内容,对文件生成惟一的标识,好比MD5算法,只要里面的内容有改动,这个值就会修改,服务器经过把响应头把该字段给浏览器。
浏览器接受到ETag值,会在下次请求的时候,将这个值做为「If-None-Match」这个字段的内容,发给服务器。
服务器接收到「If-None-Match」后,会跟服务器上该资源的「ETag」进行比对👇
Last-Modified
优于ETag
,Last-Modified
记录的是时间点,而Etag
须要根据文件的MD5算法生成对应的hash值。ETag
优于Last-Modified
。ETag
按照内容给资源带上标识,能准确感知资源变化,Last-Modified
在某些场景并不能准确感知变化,好比👇
最后,「若是两种方式都支持的话,服务器会优先考虑ETag
」。
接下来咱们考虑使用缓存的话,缓存的位置在哪里呢?
浏览器缓存的位置的话,能够分为四种,优先级从高到低排列分别👇
这个应用场景好比PWA,它借鉴了Web Worker思路,因为它脱离了浏览器的窗体,所以没法直接访问DOM。它能完成的功能好比:离线缓存
、消息推送
和网络代理
,其中离线缓存
就是「Service Worker Cache」。
指的是内存缓存,从效率上讲它是最快的,从存活时间来说又是最短的,当渲染进程结束后,内存缓存也就不存在了。
存储在磁盘中的缓存,从存取效率上讲是比内存缓存慢的,优点在于存储容量和存储时长。
二者对比,主要的策略👇
内容使用率高的话,文件优先进入磁盘
比较大的JS,CSS文件会直接放入磁盘,反之放入内存。
推送缓存,这算是浏览器中最后一道防线吧,它是HTTP/2
的内容。具体我也不是很清楚,有兴趣的能够去了解。
Cache-Control
, 尝鲜,看强缓存是否可用If-Modified-Since
或者If-None-Match
字段检查资源是否更新一旦问这个问题的话,我以为确定是一个很是深的问题了,不管从深度仍是广度上,要真的答好这个题目,或者梳理清楚的话,挺难的,毕竟一个很是综合性的问题,我做为一个刚刚入门的小白,只能梳理部分知识,更深的知识能够去看看参考连接。
那么咱们就开始吧,假设你输入的内容是👇
https://juejin.cn
复制代码
👇👇👇
首先,浏览器构建「请求行」信息(以下所示),构建好后,浏览器准备发起网络请求👇
GET / HTTP1.1 GET是请求方法,路径就是根路径,HTTP协议版本1.1 复制代码
在真正发起网络请求以前,浏览器会先在浏览器缓存中查询是否有要请求的文件。
先检查强缓存,若是命中的话直接使用,不然进入下一步,强缓存的知识点,上面👆梳理过了。
输入的域名的话,咱们须要根据域名去获取对应的ip地址
。 这个过程须要依赖一个服务系统,叫作是DNS域名解析
, 从查找到获取到具体IP的过程叫作是DNS解析
。
关于DNS篇,能够看看阮一峰的网络日志
首先,浏览器提供了DNS数据缓存功能,若是一个域名已经解析过了,那么就会把解析的结果缓存下来,下次查找的话,直接去缓存中找,不须要结果DNS解析。
「解析过程总结以下」👇
「首先查看是否有对应的域名缓存,有的话直接用缓存的ip访问」
ipconfig /displaydns // 输入这个命令就能够查看对应的电脑中是否有缓存 复制代码
「若是缓存中没有,则去查找hosts文件」 通常在 c:\windows\system32\drivers\etc\hosts
若是hosts文件里没找到想解析的域名,则将「域名发往本身配置的dns服务器」,也叫「本地dns服务器」。
ipconfig/all 经过这个命令能够查看本身的本地dns服务器 复制代码
若是「本地dns服务器有相应域名的记录」,则返回记录。
电脑的dns服务器通常是各大运营商如电信联通提供的,或者像180.76.76.76,223.5.5.5,4个114等知名dns服务商提供的,自己缓存了大量的常见域名的ip,因此常见的网站,都是有记录的。不须要找根服务器。
若是电脑本身的服务器没有记录,会去找根服务器。根服务器全球只要13组,回去找其中之一,找了根服务器后,「根服务器会根据请求的域名,返回对应的“顶级域名服务器”」,如:
「顶级域服务器收到请求,会返回二级域服务器的地址」
www.xxx.edu.cn
,则顶级域名服务器再转发给负责.edu.cn
域的二级服务器「以此类推,最终会发到负责锁查询域名的,最精确的那台dns,能够获得查询结果。」
最后一步,「本地dns服务器,把最终的解析结果,返回给客户端,对客户端来说,只是一去一回的事,客户端并不知道本地dns服务器通过了千山万水。」
以上就是大概的过程了,有兴趣的话,能够仔细去看看。
咱们所了解的就是👉Chrome 在同一个域名下要求同时最多只能有 6 个 TCP 链接,超过 6 个的话剩下的请求就得等待。
那么咱们假设不须要等待,咱们进入了TCP链接的创建阶段。
创建TCP链接
经历下面三个阶段:
从上面看得出来,TCP 链接经过什么手段来保证数据传输的可靠性,一是三次握手
确认链接,二是数据包校验
保证数据到达接收方,三是经过四次挥手
断开链接。
深刻理解的话,能够看看对应的文章,掘金上面不少文章都有深刻了解,这里就不梳理了。
TCP链接完成后,接下来就能够与服务器通讯了,也就是咱们常常说的发送HTTP请求。
发送HTTP请求的话,须要携带三样东西:「请求行」,「请求头」,「请求体」。
咱们看看大概是是什么样子的吧👇
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3 Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9 Cache-Control: no-cache Connection: keep-alive Cookie: /* 省略cookie信息 */ Host: juejin.im Pragma: no-cache Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36 复制代码
最后就是请求体,请求体的话只有在POST
请求场景下存在,常见的就是表单提交
HTTP 请求到达服务器,服务器进行对应的处理。最后要把数据传给浏览器,也就是一般咱们说的返回网络响应。
跟请求部分相似,网络响应具备三个部分:「响应行」、「响应头」和「响应体」。
响应行相似下面这样👇
HTTP/1.1 200 OK
复制代码
对应的响应头数据是怎么样的呢?咱们来举个例子看看👇
Access-Control-Max-Age: 86400 Cache-control: private Connection: close Content-Encoding: gzip Content-Type: text/html;charset=utf-8 Date: Wed, 22 Jul 2020 13:24:49 GMT Vary: Accept-Encoding Set-Cookie: ab={}; path=/; expires=Thu, 22 Jul 2021 13:24:49 GMT; secure; httponly Transfer-Encoding: chunked 复制代码
接下来,咱们数据拿到了,你认为就会断开TCP链接吗?
这个的看响应头中的Connection字段。上面的字段值为close,那么就会断开,通常状况下,HTTP1.1版本的话,一般请求头会包含「Connection: Keep-Alive」表示创建了持久链接,这样TCP
链接会一直保持,以后请求统一站点的资源会复用这个链接。
上面的状况就会断开TCP
链接,请求-响应流程结束。
到这里的话,网络请求就告一段落了,接下来的内容就是渲染流程了👇
较为专业的术语总结为如下阶段:
关于渲染流程的话,能够看我以前总结的一篇✅✅✅
[1.1W字]写给女朋友的秘籍-浏览器工做原理(渲染流程)篇
关于重排和重绘,能够上面的知识点去梳理,也就是渲染阶段,里面也梳理了部分的点,(●'◡'●)
偷个懒,看下面的文章噢👇
[1.1W字]写给女朋友的秘籍-浏览器工做原理(渲染流程)篇
跨域,是指浏览器不能执行其余网站的脚本。它是由浏览器的同源策略形成的,是浏览器对JavaScript实施的安全限制。
同源策略是一个安全策略。所谓的同源,指的是协议,域名,端口相同。
浏览器处于安全方面的考虑,只容许本域名下的接口交互,不一样源的客户端脚本,在没有明确受权的状况下,不能读写对方的资源。
限制了一下行为:
固然了,我梳理了几个我以为工做中经常使用的,其余的自行去了解。
利用script标签没有跨域限制的漏洞,网页能够拿到从其余来源产生动态JSON数据,固然了JSONP请求必定要对方的服务器作支持才能够。
「与AJAX对比」
JSONP和AJAX相同,都是客户端向服务器发送请求,从服务器获取数据的方式。可是AJAX属于同源策略,JSONP属于非同源策略(跨域请求)
「JSONP优势」
兼容性比较好,可用于解决主流浏览器的跨域数据访问的问题。缺点就是仅支持get请求,具备局限性,不安全,可能会受到XSS攻击。
「思路👇」
let script = document.createElement('script'); script.src = 'http://www.baidu.cn/login?username=TianTianUp&callback=callback'; document.body.appendChild(script); function callback(res) { console.log(res); } 复制代码
固然,jquery也支持jsonp的实现方式
$.ajax({ url: 'http://www.baidu.cn/login', type: 'GET', dataType: 'jsonp', //请求方式为jsonp jsonpCallback: 'callback', data: { "username": "Nealyang" } }) 复制代码
「JSONP优势」
「JSONP缺点」
CORS(Cross-Origin Resource Sharing)跨域资源共享,定义了必须在访问跨域资源时,浏览器与服务器应该如何沟通。CORS背后的基本思想就是使用自定义的HTTP头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功仍是失败。目前,全部浏览器都支持该功能,IE浏览器不能低于IE10。整个CORS通讯过程,都是浏览器自动完成,不须要用户参与。对于开发者来讲,CORS通讯与同源的AJAX通讯没有差异,代码彻底同样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感受。
上面是引用,你要记住的关键点👇
「CORS 须要浏览器和后端同时支持。IE 8 和 9 须要经过 XDomainRequest 来实现」。
请求分为「简单请求」和「非简单请求」,因此咱们的了解这两种状况。
「简单请求」
知足下面两个条件,就属于简单请求👇
条件1:使用下列方法之一:
条件2:Content-Type 的值仅限于下列三者之一👇
请求中的任意 XMLHttpRequestUpload 对象均没有注册任何事件监听器;
XMLHttpRequestUpload 对象可使用 XMLHttpRequest.upload 属性访问。
「复杂请求」
不符合以上条件的请求就确定是复杂请求了。 复杂请求的CORS请求,会在正式通讯以前,增长一次HTTP查询请求,称为"预检"请求,该请求是 option 方法的,经过该请求来知道服务端是否容许跨域请求。
直接上一个例子吧👇 看看一个完整的复杂请求吧,而且介绍一下CORS请求的字段。
//server2.js let express = require('express') let app = express() let whitList = ['http://localhost:3000'] //设置白名单 app.use(function(req, res, next) { let origin = req.headers.origin if (whitList.includes(origin)) { // 设置哪一个源能够访问我 res.setHeader('Access-Control-Allow-Origin', origin) // 容许携带哪一个头访问我 res.setHeader('Access-Control-Allow-Headers', 'name') // 容许哪一个方法访问我 res.setHeader('Access-Control-Allow-Methods', 'PUT') // 容许携带cookie res.setHeader('Access-Control-Allow-Credentials', true) // 预检的存活时间 res.setHeader('Access-Control-Max-Age', 6) // 容许返回的头 res.setHeader('Access-Control-Expose-Headers', 'name') if (req.method === 'OPTIONS') { res.end() // OPTIONS请求不作任何处理 } } next() }) app.put('/getData', function(req, res) { console.log(req.headers) res.setHeader('name', 'jw') //返回一个响应头,后台需设置 res.end('我不爱你') }) app.get('/getData', function(req, res) { console.log(req.headers) res.end('我不爱你') }) app.use(express.static(__dirname)) app.listen(4000) 复制代码
上述代码由http://localhost:3000/index.html
向http://localhost:4000/
跨域请求,正如咱们上面所说的,后端是实现 CORS 通讯的关键。
上述的例子,必定对你会有所帮助的,这块代码,是跟着浪里行舟
代码来的,参考处注明了出处。
「与JSONP对比」
Websocket是HTML5的一个持久化的协议,它实现了浏览器与服务器的全双工通讯,同时也是跨域的一种解决方案。
WebSocket和HTTP都是应用层协议,都基于 TCP 协议。可是 「WebSocket 是一种双向通讯协议,在创建链接以后,WebSocket 的 server 与 client 都能主动向对方发送或接收数据」。同时,WebSocket 在创建链接时须要借助 HTTP 协议,链接创建好了以后 client 与 server 之间的双向通讯就与 HTTP 无关了。
咱们先来看个例子👇
本地文件socket.html向localhost:3000
发生数据和接受数据👇
// socket.html <script> let socket = new WebSocket('ws://localhost:3000'); socket.onopen = function () { socket.send('我爱你');//向服务器发送数据 } socket.onmessage = function (e) { console.log(e.data);//接收服务器返回的数据 } </script> 复制代码
后端部分👇
// server.js let WebSocket = require('ws'); //记得安装ws let wss = new WebSocket.Server({port:3000}); wss.on('connection',function(ws) { ws.on('message', function (data) { console.log(data); ws.send('我不爱你') }); }) 复制代码
若是 你想去尝试的话,建议能够去玩一玩Socket.io
,
XSS 全称是 Cross Site Scripting
,为了与CSS
区分开来,故简称 XSS
,翻译过来就是“跨站脚本”。
XSS是指黑客往 HTML 文件中或者 DOM 中注入恶意脚本,从而在用户浏览页面时利用注入的恶意脚本对用户实施攻击的一种手段。
最开始的时候,这种攻击是经过跨域来实现的,因此叫“跨域脚本”。发展到如今,往HTML文件中中插入恶意代码方式愈来愈多,因此是否跨域注入脚本已经不是惟一的注入手段了,可是 XSS 这个名字却一直保留至今。
注入恶意脚本能够完成这些事情:
通常的状况下,XSS攻击有三种实现方式
从图上看,存储型 XSS 攻击大体步骤以下:
好比常见的场景:
在评论区提交一份脚本代码,假设先后端没有作好转义工做,那内容上传到服务器,在页面渲染的时候就会直接执行
,至关于执行一段未知的JS代码。这就是存储型 XSS 攻击。
反射型 XSS 攻击指的就是恶意脚本做为「网络请求的一部分」,随后网站又把恶意的JavaScript脚本返回给用户,当恶意 JavaScript 脚本在用户页面中被执行时,黑客就能够利用该脚本作一些恶意操做。
举个例子:
http://TianTianUp.com?query=<script>alert("你受到了XSS攻击")</script> 复制代码
如上,服务器拿到后解析参数query,最后将内容返回给浏览器,浏览器将这些内容做为HTML的一部分解析,发现是Javascript脚本,直接执行,这样子被XSS攻击了。
这也就是反射型名字的由来,将恶意脚本做为参数,经过网络请求,最后通过服务器,在反射到HTML文档中,执行解析。
主要注意的就是,「服务器不会存储这些恶意的脚本,这也算是和存储型XSS攻击的区别吧」。
基于 DOM 的 XSS 攻击是不牵涉到页面 Web 服务器的。具体来说,黑客经过各类手段将恶意脚本注入用户的页面中,在数据传输的时候劫持网络数据包
常见的劫持手段有:
以上讲述的XSS攻击原理,都有一个共同点:让恶意脚本直接在浏览器执行。
针对三种不一样形式的XSS攻击,有如下三种解决办法
对用户输入的信息过滤或者是转码
举个例子👇
转码后👇
<script>alert('你受到XSS攻击了')</script>
复制代码
这样的代码在 html 解析的过程当中是没法执行的。
固然了对于<script>
、<img>
、<a>
等关键字标签也是能够过来的,效果以下👇
复制代码
最后什么都没有剩下了
该安全策略的实现基于一个称做 Content-Security-Policy
的 HTTP 首部。
能够移步MDN,有更加规范的解释。我在这里就是梳理一下吧。
CSP,即浏览器中的内容安全策略,它的核心思想大概就是服务器决定浏览器加载哪些资源,具体来讲有几个功能👇
因为不少 XSS 攻击都是来盗用 Cookie 的,所以还能够经过使用 HttpOnly 属性来保护咱们 Cookie 的安全。这样子的话,JavaScript 便没法读取 Cookie 的值。这样也能很好的防范 XSS 攻击。
一般服务器能够将某些 Cookie 设置为 HttpOnly 标志,HttpOnly 是服务器经过 HTTP 响应头来设置的,下面是打开 Google 时,HTTP 响应头中的一段:
set-cookie: NID=189=M8l6-z41asXtm2uEwcOC5oh9djkffOMhWqQrlnCtOI; expires=Sat, 18-Apr-2020 06:52:22 GMT; path=/; domain=.google.com; HttpOnly 复制代码
XSS
攻击是指浏览器中执行恶意脚本, 而后拿到用户的信息进行操做。主要分为存储型
、反射型
和文档型
。防范的措施包括:
<script>
、<img>
、<a>
标签除了以上策略以外,咱们还能够经过添加验证码防止脚本冒充用户提交危险操做。而对于一些不受信任的输入,还能够限制其输入长度,这样能够增大 XSS 攻击的难度。
CSRF 英文全称是 Cross-site request forgery
,因此又称为“跨站请求伪造”,是指黑客引诱用户打开黑客的网站,在黑客的网站中,利用用户的登陆状态发起的跨站请求。简单来说,「CSRF 攻击就是黑客利用了用户的登陆状态,并经过第三方的站点来作一些坏事。」
通常的状况下,点开一个诱导你的连接,黑客会在你不知情的时候作哪些事情呢
黑客网页里面可能有一段这样的代码👇
<img src="http://bank.example/withdraw?amount=10000&for=hacker" >
复制代码
在受害者访问含有这个img的页面后,浏览器会自动向http://bank.example/withdraw?account=xiaoming&amount=10000&for=hacker
发出一次HTTP请求。
bank.example
就会收到包含受害者登陆信息的一次跨域请求。
黑客网页中有一个表单,自动提交的表单👇
<form action="http://bank.example/withdraw" method=POST> <input type="hidden" name="account" value="xiaoming" /> <input type="hidden" name="amount" value="10000" /> <input type="hidden" name="for" value="hacker" /> </form> <script> document.forms[0].submit(); </script> 复制代码
访问该页面后,表单会自动提交,至关于模拟用户完成了一次POST操做。
一样也会携带相应的用户 cookie 信息,让服务器误觉得是一个正常的用户在操做,让各类恶意的操做变为可能。
这种须要诱导用户去点击连接才会触发,这类的状况好比在论坛中发布照片,照片中嵌入了恶意连接,或者是以广告的形式去诱导,好比:
<a href="http://test.com/csrf/withdraw.php?amount=1000&for=hacker" taget="_blank">
重磅消息!!!
<a/>
复制代码
点击后,自动发送 get 请求,接下来和自动发 GET 请求
部分同理。
以上三种状况,就是CSRF攻击原理,跟XSS对比的话,CSRF攻击并不须要将恶意代码注入HTML中,而是跳转新的页面,利用「服务器的验证漏洞」和「用户以前的登陆状态」来模拟用户进行操做
其实咱们能够想到,黑客只能借助受害者的**cookie**
骗取服务器的信任,可是黑客并不能凭借拿到「cookie」,也看不到 「cookie」的内容。另外,对于服务器返回的结果,因为浏览器「同源策略」的限制,黑客也没法进行解析。
这就告诉咱们,咱们要保护的对象是那些能够直接产生数据改变的服务,而对于读取数据的服务,则不须要进行
**CSRF**
的保护。而保护的关键,是 「在请求中放入黑客所不能伪造的信息」
方法:添加验证码来识别是否是用户主动去发起这个请求,因为必定强度的验证码机器没法识别,所以危险网站不能伪造一个完整的请求。
在服务器端验证请求来源的站点,因为大量的CSRF攻击来自第三方站点,所以服务器跨域禁止来自第三方站点的请求,主要经过HTTP请求头中的两个Header
这两个Header在浏览器发起请求时,大多数状况会自动带上,而且不能由前端自定义内容。
服务器能够经过解析这两个Header中的域名,肯定请求的来源域。
其中,「Origin」只包含域名信息,而「Referer」包含了具体
的 URL 路径。
在某些状况下,这二者都是能够伪造的,经过AJax
中自定义请求头便可,安全性略差。
SameSite
能够设置为三个值,Strict
、Lax
和None
。
Strict
模式下,浏览器彻底禁止第三方请求携带Cookie。好比请求sanyuan.com
网站只能在sanyuan.com
域名当中请求才能携带 Cookie,在其余网站请求都不能。Lax
模式,就宽松一点了,可是只能在 get 方法提交表单
况或者a 标签发送 get 请求
的状况下能够携带 Cookie,其余状况均不能。前面讲到CSRF的另外一个特征是,攻击者没法直接窃取到用户的信息(Cookie,Header,网站内容等),仅仅是冒用Cookie中的信息。
那么咱们可使用Token,在不涉及XSS的前提下,通常黑客很难拿到Token。
能够看看这篇文章,将了Token是怎么操做的👉完全理解cookie,session,token
Token(令牌)作为Web领域验证身份是一个不错的选择,固然了,JWT有兴趣的也能够去了解一下。
Token步骤以下:
「第一步:将CSRF Token输出到页面中」
首先,用户打开页面的时候,服务器须要给这个用户生成一个Token,该Token经过加密算法对数据进行加密,通常Token都包括随机字符串和时间戳的组合,显然在提交时Token不能再放在Cookie中了(XSS可能会获取Cookie),不然又会被攻击者冒用。所以,为了安全起见Token最好仍是存在服务器的Session中,以后在每次页面加载时,使用JS遍历整个DOM树,对于DOM中全部的a和form标签后加入Token。这样能够解决大部分的请求,可是对于在页面加载以后动态生成的HTML代码,这种方法就没有做用,还须要程序员在编码时手动添加Token。
「第二步:页面提交的请求携带这个Token」
对于GET请求,Token将附在请求地址以后,这样URL 就变成 http://url?csrftoken=tokenvalue。 而对于 POST 请求来讲,要在 form 的最后加上:
<input type=”hidden” name=”csrftoken” value=”tokenvalue”/>
这样,就把Token以参数的形式加入请求了。
「第三步:服务器验证Token是否正确」
当用户从客户端获得了Token,再次提交给服务器的时候,服务器须要判断Token的有效性,验证过程是先解密Token,对比加密字符串以及时间戳,若是加密字符串一致且时间未过时,那么这个Token就是有效的。
很是感兴趣的,能够仔细去阅读一下相关的文章,Token是如何加密的,又是如何保证不被攻击者获取道。
CSRF(Cross-site request forgery), 即跨站请求伪造,本质是冲着浏览器分不清发起请求是否是真正的用户本人,因此防范的关键在于在请求中放入黑客所不能伪造的信息。从而防止黑客伪造一个完整的请求欺骗服务器。
「防范措施」:验证码机制,验证来源站点,利用Cookie的SameSite属性,CSRF Token
若是你以为这篇内容对你挺有有帮助的话:
点赞支持下吧,让更多的人也能看到这篇内容(收藏不点赞,都是耍流氓 -_-)
欢迎在留言区与我分享你的想法,也欢迎你在留言区记录你的思考过程。
以为不错的话,也能够阅读TianTian近期梳理的文章(感谢掘友的鼓励与支持🌹🌹🌹):
本文使用 mdnice 排版