想要成爲一名合格的前端工程師,掌握相關瀏覽器的工做原理是必備的,這樣子纔會有一個完整知識體系,要是「能參透瀏覽器的工做原理,你就能解決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 排版