「查缺補(bǔ)漏」高頻考點(diǎn)瀏覽器面試題
前言
想要成為一名合格的前端工程師,掌握相關(guān)瀏覽器的工作原理是必備的,這樣子才會(huì)有一個(gè)完整知識(shí)體系,要是「能參透瀏覽器的工作原理,你就能解決80%的前端難題」。
這篇梳理的話(huà),更多的是對(duì)瀏覽器工作原理篇的查缺補(bǔ)漏,對(duì)于一些沒(méi)有涉及到的知識(shí)點(diǎn),準(zhǔn)備梳理梳理,也正好回顧之前梳理的內(nèi)容。
感謝掘友的鼓勵(lì)與支持???,往期文章都在最后梳理出來(lái)了(●'?'●)
「接下來(lái)以問(wèn)題形式展開(kāi)梳理」
1. 常見(jiàn)的瀏覽器內(nèi)核有哪些?
| 瀏覽器/RunTime | 內(nèi)核(渲染引擎) | 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 |
2. 瀏覽器的主要組成部分是什么?
「用戶(hù)界面」 - 包括地址欄、前進(jìn)/后退按鈕、書(shū)簽菜單等。 「瀏覽器引擎」 - 在用戶(hù)界面和呈現(xiàn)引擎之間傳送指令。 「呈現(xiàn)引擎」 - 負(fù)責(zé)顯示請(qǐng)求的內(nèi)容。如果請(qǐng)求的內(nèi)容是 HTML,它就負(fù)責(zé)解析 HTML 和 CSS 內(nèi)容,并將解析后的內(nèi)容顯示在屏幕上。 「網(wǎng)絡(luò)」 - 用于網(wǎng)絡(luò)調(diào)用,比如 HTTP 請(qǐng)求。 「用戶(hù)界面后端」 -用于繪制基本的窗口小部件,比如組合框和窗口。 「JavaScript 解釋器」- 用于解析和執(zhí)行 JavaScript 代碼。 「數(shù)據(jù)存儲(chǔ)」 - 這是持久層。瀏覽器需要在硬盤(pán)上保存各種數(shù)據(jù),例如 Cookie。新的 HTML 規(guī)范 (HTML5) 定義了“網(wǎng)絡(luò)數(shù)據(jù)庫(kù)”,這是一個(gè)完整(但是輕便)的瀏覽器內(nèi)數(shù)據(jù)庫(kù)。
值得注意的是,和大多數(shù)瀏覽器不同,Chrome 瀏覽器的每個(gè)標(biāo)簽頁(yè)都分別對(duì)應(yīng)一個(gè)呈現(xiàn)引擎實(shí)例。每個(gè)標(biāo)簽頁(yè)都是一個(gè)獨(dú)立的進(jìn)程。
3. 為什么JavaScript是單線(xiàn)程的,與異步?jīng)_突嗎
補(bǔ)充:JS中其實(shí)是沒(méi)有線(xiàn)程概念的,所謂的單線(xiàn)程也只是相對(duì)于多線(xiàn)程而言。JS的設(shè)計(jì)初衷就沒(méi)有考慮這些,針對(duì)JS這種不具備并行任務(wù)處理的特性,我們稱(chēng)之為“單線(xiàn)程”。
JS的單線(xiàn)程是指一個(gè)瀏覽器進(jìn)程中只有一個(gè)JS的執(zhí)行線(xiàn)程,同一時(shí)刻內(nèi)只會(huì)有一段代碼在執(zhí)行。
舉個(gè)通俗例子,假設(shè)JS支持多線(xiàn)程操作的話(huà),JS可以操作DOM,那么一個(gè)線(xiàn)程在刪除DOM,另外一個(gè)線(xiàn)程就在獲取DOM數(shù)據(jù),這樣子明顯不合理,這算是證明之一。
來(lái)看段代碼?
function foo() {console.log("first");setTimeout(( function(){console.log( 'second' );}),5);}for (var i = 0; i < 1000000; i++) {foo();}
打印結(jié)果就是首先是很多個(gè)first,然后再是second。
異步機(jī)制是瀏覽器的兩個(gè)或以上常駐線(xiàn)程共同完成的,舉個(gè)例子,比如異步請(qǐng)求由兩個(gè)常駐線(xiàn)程,JS執(zhí)行線(xiàn)程和事件觸發(fā)線(xiàn)程共同完成的。
JS執(zhí)行線(xiàn)程發(fā)起異步請(qǐng)求(瀏覽器會(huì)開(kāi)啟一個(gè)HTTP請(qǐng)求線(xiàn)程來(lái)執(zhí)行請(qǐng)求,這時(shí)JS的任務(wù)完成,繼續(xù)執(zhí)行線(xiàn)程隊(duì)列中剩下任務(wù)) 然后在未來(lái)的某一時(shí)刻事件觸發(fā)線(xiàn)程監(jiān)視到之前的發(fā)起的HTTP請(qǐng)求已完成,它就會(huì)把完成事件插入到JS執(zhí)行隊(duì)列的尾部等待JS處理
再比如定時(shí)器觸發(fā)(settimeout和setinterval) 是由「瀏覽器的定時(shí)器線(xiàn)程」執(zhí)行的定時(shí)計(jì)數(shù),然后在定時(shí)時(shí)間把定時(shí)處理函數(shù)的執(zhí)行請(qǐng)求插入到JS執(zhí)行隊(duì)列的尾端(所以用這兩個(gè)函數(shù)的時(shí)候,實(shí)際的執(zhí)行時(shí)間是大于或等于指定時(shí)間的,不保證能準(zhǔn)確定時(shí)的)。
所以這么說(shuō),JS單線(xiàn)程與異步更多是瀏覽器行為,之間不沖突。
4. CSS加載會(huì)造成阻塞嗎
先給出結(jié)論
CSS不會(huì)阻塞DOM解析,但會(huì)阻塞DOM渲染。CSS會(huì)阻塞JS執(zhí)行,并不會(huì)阻塞JS文件下載
先講一講CSSOM作用
第一個(gè)是提供給 JavaScript 操作樣式表的能力 第二個(gè)是為布局樹(shù)的合成提供基礎(chǔ)的樣式信息 這個(gè) CSSOM 體現(xiàn)在 DOM 中就是document.styleSheets。
由之前講過(guò)的瀏覽器渲染流程我們可以看出:
DOM 和 CSSOM通常是并行構(gòu)建的,所以「CSS 加載不會(huì)阻塞 DOM 的解析」。
然而由于Render Tree 是依賴(lài)DOM Tree和 CSSOM Tree的,所以它必須等到兩者都加載完畢后,完成相應(yīng)的構(gòu)建,才開(kāi)始渲染,因此,「CSS加載會(huì)阻塞DOM渲染」。
由于 JavaScript 是可操縱 DOM 和 css 樣式 的,如果在修改這些元素屬性同時(shí)渲染界面(即 JavaScript 線(xiàn)程和 UI 線(xiàn)程同時(shí)運(yùn)行),那么渲染線(xiàn)程前后獲得的元素?cái)?shù)據(jù)就可能不一致了。
因此為了防止渲染出現(xiàn)不可預(yù)期的結(jié)果,瀏覽器設(shè)置 「GUI 渲染線(xiàn)程與 JavaScript 引擎為互斥」的關(guān)系。
有個(gè)需要注意的點(diǎn)就是:
「有時(shí)候JS需要等到CSS的下載,這是為什么呢?」
仔細(xì)思考一下,其實(shí)這樣做是有道理的,如果腳本的內(nèi)容是獲取元素的樣式,寬高等CSS控制的屬性,瀏覽器是需要計(jì)算的,也就是依賴(lài)于CSS。瀏覽器也無(wú)法感知腳本內(nèi)容到底是什么,為避免樣式獲取,因而只好等前面所有的樣式下載完后,再執(zhí)行JS。
JS文件下載和CSS文件下載是并行的,有時(shí)候CSS文件很大,所以JS需要等待。
因此,樣式表會(huì)在后面的 js 執(zhí)行前先加載執(zhí)行完畢,所以「css 會(huì)阻塞后面 js 的執(zhí)行」。
5. 為什么JS會(huì)阻塞頁(yè)面加載
先給出結(jié)論?
「JS阻塞DOM解析」,也就會(huì)阻塞頁(yè)面
這也是為什么說(shuō)JS文件放在最下面的原因,那為什么會(huì)阻塞DOM解析呢
你可以這樣子理解:
由于 JavaScript 是可操縱 DOM 的,如果在修改這些元素屬性同時(shí)渲染界面(即 JavaScript 線(xiàn)程和 UI 線(xiàn)程同時(shí)運(yùn)行),那么渲染線(xiàn)程前后獲得的元素?cái)?shù)據(jù)就可能不一致了。
因此為了防止渲染出現(xiàn)不可預(yù)期的結(jié)果,瀏覽器設(shè)置 「GUI 渲染線(xiàn)程與 JavaScript 引擎為互斥」的關(guān)系。
當(dāng) JavaScript 引擎執(zhí)行時(shí) GUI 線(xiàn)程會(huì)被掛起,GUI 更新會(huì)被保存在一個(gè)隊(duì)列中等到引擎線(xiàn)程空閑時(shí)立即被執(zhí)行。
當(dāng)瀏覽器在執(zhí)行 JavaScript 程序的時(shí)候,GUI 渲染線(xiàn)程會(huì)被保存在一個(gè)隊(duì)列中,直到 JS 程序執(zhí)行完成,才會(huì)接著執(zhí)行。
因此如果 JS 執(zhí)行的時(shí)間過(guò)長(zhǎng),這樣就會(huì)造成頁(yè)面的渲染不連貫,導(dǎo)致頁(yè)面渲染加載阻塞的感覺(jué)。
另外,如果 JavaScript 文件中沒(méi)有操作 DOM 相關(guān)代碼,就可以將該 JavaScript 腳本設(shè)置為異步加載,通過(guò) async 或 defer 來(lái)標(biāo)記代碼
6. defer 和 async 的區(qū)別 ?
兩者都是異步去加載外部JS文件,不會(huì)阻塞DOM解析 Async是在外部JS加載完成后,瀏覽器空閑時(shí),Load事件觸發(fā)前執(zhí)行,標(biāo)記為async的腳本并不保證按照指定他們的先后順序執(zhí)行,該屬性對(duì)于內(nèi)聯(lián)腳本無(wú)作用 (即沒(méi)有「src」屬性的腳本)。 defer是在JS加載完成后,整個(gè)文檔解析完成后,觸發(fā) DOMContentLoaded事件前執(zhí)行,如果缺少src屬性(即內(nèi)嵌腳本),該屬性不應(yīng)被使用,因?yàn)檫@種情況下它不起作用
7. DOMContentLoaded 與 load 的區(qū)別 ?
DOMContentLoaded事件觸發(fā)時(shí):僅當(dāng)DOM解析完成后,不包括樣式表,圖片等資源。 onload 事件觸發(fā)時(shí),頁(yè)面上所有的 DOM,樣式表,腳本,圖片等資源已經(jīng)加載完畢。
那么也就是先DOMContentLoaded -> load,那么在Jquery中,使用(document).load(callback)監(jiān)聽(tīng)的就是load事件。
那我們可以聊一聊它們與async和defer區(qū)別
帶async的腳本一定會(huì)在load事件之前執(zhí)行,可能會(huì)在DOMContentLoaded之前或之后執(zhí)行。
情況1:HTML 還沒(méi)有被解析完的時(shí)候,async腳本已經(jīng)加載完了,那么 HTML 停止解析,去執(zhí)行腳本,腳本執(zhí)行完畢后觸發(fā)DOMContentLoaded事件 情況2:HTML 解析完了之后,async腳本才加載完,然后再執(zhí)行腳本,那么在HTML解析完畢、async腳本還沒(méi)加載完的時(shí)候就觸發(fā)DOMContentLoaded事件
如果 script 標(biāo)簽中包含 defer,那么這一塊腳本將不會(huì)影響 HTML 文檔的解析,而是等到HTML 解析完成后才會(huì)執(zhí)行。而 DOMContentLoaded 只有在 defer 腳本執(zhí)行結(jié)束后才會(huì)被觸發(fā)。
情況1:HTML還沒(méi)解析完成時(shí),defer腳本已經(jīng)加載完畢,那么defer腳本將等待HTML解析完成后再執(zhí)行。defer腳本執(zhí)行完畢后觸發(fā)DOMContentLoaded事件 情況2:HTML解析完成時(shí),defer腳本還沒(méi)加載完畢,那么defer腳本繼續(xù)加載,加載完成后直接執(zhí)行,執(zhí)行完畢后觸發(fā)DOMContentLoaded事件
8. 為什么CSS動(dòng)畫(huà)比JavaScript高效
我覺(jué)得這個(gè)題目說(shuō)法上可能就是行不通,不能這么說(shuō),如果了解的話(huà),都知道will-change只是一個(gè)優(yōu)化的手段,使用JS改變transform也可以享受這個(gè)屬性帶來(lái)的變化,所以這個(gè)說(shuō)法上有點(diǎn)不妥。
所以圍繞這個(gè)問(wèn)題展開(kāi)話(huà),更應(yīng)該說(shuō)建議推薦使用CSS動(dòng)畫(huà),至于為什么呢,涉及的知識(shí)點(diǎn)大概就是重排重繪,合成,這方面的點(diǎn),我在瀏覽器渲染流程中也提及了。
盡可能的避免重排和重繪,具體是哪些操作呢,如果非要去操作JS實(shí)現(xiàn)動(dòng)畫(huà)的話(huà),有哪些優(yōu)化的手段呢?
比如?
使用 createDocumentFragment進(jìn)行批量的 DOM 操作對(duì)于 resize、scroll 等進(jìn)行防抖/節(jié)流處理。 rAF優(yōu)化等等
剩下的東西就留給你們思考吧,希望我這是拋磚引玉吧(●'?'●)
9. 能不能實(shí)現(xiàn)事件防抖和節(jié)流
函數(shù)節(jié)流(throttle)
節(jié)流的意思是讓函數(shù)有節(jié)制地執(zhí)行,而不是毫無(wú)節(jié)制的觸發(fā)一次就執(zhí)行一次。什么叫有節(jié)制呢?就是在一段時(shí)間內(nèi),只執(zhí)行一次。
規(guī)定在一個(gè)單位時(shí)間內(nèi),只能觸發(fā)一次函數(shù)。如果這個(gè)單位時(shí)間內(nèi)觸發(fā)多次函數(shù),只有一次生效。
抓取一個(gè)關(guān)鍵的點(diǎn):就是執(zhí)行的時(shí)機(jī)。要做到控制執(zhí)行的時(shí)機(jī),我們可以通過(guò)「一個(gè)開(kāi)關(guān)」,與定時(shí)器setTimeout結(jié)合完成。
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);};};
函數(shù)防抖(debounce)
在事件被觸發(fā)n秒后再執(zhí)行回調(diào),如果在這n秒內(nèi)又被觸發(fā),則重新計(jì)時(shí)。
核心思想:每次事件觸發(fā)都會(huì)刪除原有定時(shí)器,建立新的定時(shí)器。通俗意思就是反復(fù)觸發(fā)函數(shù),只認(rèn)最后一次,從最后一次開(kāi)始計(jì)時(shí)。
代碼:
function debounce(fn, delay) {let timer = nullreturn function (...args) {let context = thisif(timer) clearTimeout(timer)timer = setTimeout(function() {fn.apply(context, args)},delay)}}
如何使用 debounce 和 throttle 以及常見(jiàn)的坑
自己造一個(gè) debounce / throttle 的輪子看起來(lái)多么誘人,或者隨便找個(gè)博文復(fù)制過(guò)來(lái)。「我是建議直接使用 underscore 或 Lodash」 。如果僅需要 _.debounce 和 _.throttle 方法,可以使用 Lodash 的自定義構(gòu)建工具,生成一個(gè) 2KB 的壓縮庫(kù)。使用以下的簡(jiǎn)單命令即可:
npm i -g lodash-clinpm i -g lodash-clilodash-cli include=debounce,throttle
常見(jiàn)的坑是,不止一次地調(diào)用 _.debounce 方法:
// 錯(cuò)誤$(window).on('scroll', function() {_.debounce(doSomething, 300);});// 正確$(window).on('scroll', _.debounce(doSomething, 200));
debounce 方法保存到一個(gè)變量以后,就可以用它的私有方法 debounced_version.cancel(),lodash 和 underscore.js 都有效。
let debounced_version = _.debounce(doSomething, 200);$(window).on('scroll', debounced_version);// 如果需要的話(huà)debounced_version.cancel();
適合應(yīng)用場(chǎng)景
防抖
search搜索,用戶(hù)不斷輸入值時(shí),用防抖來(lái)節(jié)約Ajax請(qǐng)求,也就是輸入框事件。 window觸發(fā)resize時(shí),不斷的調(diào)整瀏覽器窗口大小會(huì)不斷的觸發(fā)這個(gè)事件,用防抖來(lái)讓其只觸發(fā)一次
節(jié)流
鼠標(biāo)的點(diǎn)擊事件,比如mousedown只觸發(fā)一次 監(jiān)聽(tīng)滾動(dòng)事件,比如是否滑到底部自動(dòng)加載更多,用throttle判斷 比如游戲中發(fā)射子彈的頻率(1秒發(fā)射一顆)
10. 談一談你對(duì)requestAnimationFrame(rAF)理解
正好跟節(jié)流有點(diǎn)關(guān)系,有點(diǎn)相似處,就準(zhǔn)備梳理一下這個(gè)知識(shí)點(diǎn)。
「高性能動(dòng)畫(huà)是什么,那它衡量的標(biāo)準(zhǔn)是什么呢?」
動(dòng)畫(huà)幀率可以作為衡量標(biāo)準(zhǔn),一般來(lái)說(shuō)畫(huà)面在 60fps 的幀率下效果比較好。
換算一下就是,每一幀要在 16.7ms (16.7 = 1000/60) 內(nèi)完成渲染。
我們來(lái)看看MDN對(duì)它的解釋吧?
window.requestAnimationFrame() 方法告訴瀏覽器您希望執(zhí)行動(dòng)畫(huà)并請(qǐng)求瀏覽器在下一次重繪之前調(diào)用指定的函數(shù)來(lái)更新動(dòng)畫(huà)。該方法使用一個(gè)回調(diào)函數(shù)作為參數(shù),這個(gè)回調(diào)函數(shù)會(huì)在瀏覽器重繪之前調(diào)用。-- MDN
當(dāng)我們調(diào)用這個(gè)函數(shù)的時(shí)候,我們告訴它需要做兩件事:
我們需要新的一幀; 當(dāng)你渲染新的一幀時(shí)需要執(zhí)行我傳給你的回調(diào)函數(shù)
rAF與 setTimeout 相比
rAF(requestAnimationFrame) 最大的優(yōu)勢(shì)是「由系統(tǒng)來(lái)決定回調(diào)函數(shù)的執(zhí)行時(shí)機(jī)」。
具體一點(diǎn)講就是,系統(tǒng)每次繪制之前會(huì)主動(dòng)調(diào)用 rAF 中的回調(diào)函數(shù),如果系統(tǒng)繪制率是 60Hz,那么回調(diào)函數(shù)就每16.7ms 被執(zhí)行一次,如果繪制頻率是75Hz,那么這個(gè)間隔時(shí)間就變成了 1000/75=13.3ms。
換句話(huà)說(shuō)就是,rAF 的執(zhí)行步伐跟著系統(tǒng)的繪制頻率走。它能保證回調(diào)函數(shù)在屏幕每一次的繪制間隔中只被執(zhí)行一次(上一個(gè)知識(shí)點(diǎn)剛剛梳理完「函數(shù)節(jié)流」),這樣就不會(huì)引起丟幀現(xiàn)象,也不會(huì)導(dǎo)致動(dòng)畫(huà)出現(xiàn)卡頓的問(wèn)題。
另外它可以自動(dòng)調(diào)節(jié)頻率。如果callback工作太多無(wú)法在一幀內(nèi)完成會(huì)自動(dòng)降低為30fps。雖然降低了,但總比掉幀好。
與setTimeout動(dòng)畫(huà)對(duì)比的話(huà),有以下幾點(diǎn)優(yōu)勢(shì)
當(dāng)頁(yè)面隱藏或者最小化時(shí),setTimeout仍然在后臺(tái)執(zhí)行動(dòng)畫(huà),此時(shí)頁(yè)面不可見(jiàn)或者是不可用狀態(tài),動(dòng)畫(huà)刷新沒(méi)有意義,而言浪費(fèi)CPU。 rAF不一樣,當(dāng)頁(yè)面處理未激活的狀態(tài)時(shí),該頁(yè)面的屏幕繪制任務(wù)也會(huì)被系統(tǒng)暫停,因此跟著系統(tǒng)步伐走的rAF也會(huì)停止渲染,當(dāng)頁(yè)面被激活時(shí),動(dòng)畫(huà)就從上次停留的地方繼續(xù)執(zhí)行,有效節(jié)省了 CPU 開(kāi)銷(xiāo)。
什么時(shí)候調(diào)用呢
規(guī)范中似乎是這么去定義的:
在重新渲染前調(diào)用。 很可能在宏任務(wù)之后不去調(diào)用
這樣子分析的話(huà),似乎很合理嘛,為什么要在重新渲染前去調(diào)用呢?因?yàn)閞AF作為官方推薦的一種做流暢動(dòng)畫(huà)所應(yīng)該使用的API,做動(dòng)畫(huà)不可避免的去操作DOM,而如果是在渲染后去修改DOM的話(huà),那就只能等到下一輪渲染機(jī)會(huì)的時(shí)候才能去繪制出來(lái)了,這樣子似乎不合理。
rAF在瀏覽器決定渲染之前給你最后一個(gè)機(jī)會(huì)去改變 DOM 屬性,然后很快在接下來(lái)的繪制中幫你呈現(xiàn)出來(lái),所以這是做流暢動(dòng)畫(huà)的不二選擇。
至于宏任務(wù),微任務(wù),這可以說(shuō)起來(lái)就要展開(kāi)篇幅了,暫時(shí)不在這里梳理了。
rAF與節(jié)流相比
跟 _.throttle(dosomething, 16) 等價(jià)。它是高保真的,如果追求更好的精確度的話(huà),可以用瀏覽器原生的 API 。
可以使用 rAF API 替換 throttle 方法,考慮一下優(yōu)缺點(diǎn):
優(yōu)點(diǎn)
動(dòng)畫(huà)保持 60fps(每一幀 16 ms),瀏覽器內(nèi)部決定渲染的最佳時(shí)機(jī) 簡(jiǎn)潔標(biāo)準(zhǔn)的 API,后期維護(hù)成本低
缺點(diǎn)
動(dòng)畫(huà)的開(kāi)始/取消需要開(kāi)發(fā)者自己控制,不像 ‘.debounce’ 或 ‘.throttle’由函數(shù)內(nèi)部處理。 瀏覽器標(biāo)簽未激活時(shí),一切都不會(huì)執(zhí)行。 盡管所有的現(xiàn)代瀏覽器都支持 rAF ,IE9,Opera Mini 和 老的 Android 還是需要打補(bǔ)丁。 Node.js 不支持,無(wú)法在服務(wù)器端用于文件系統(tǒng)事件。
根據(jù)經(jīng)驗(yàn),如果 JavaScript 方法需要繪制或者直接改變屬性,我會(huì)選擇 requestAnimationFrame,只要涉及到重新計(jì)算元素位置,就可以使用它。
涉及到 AJAX 請(qǐng)求,添加/移除 class (可以觸發(fā) CSS 動(dòng)畫(huà)),我會(huì)選擇 _.debounce 或者 _.throttle ,可以設(shè)置更低的執(zhí)行頻率(例子中的200ms 換成16ms)。
11. 能不能實(shí)現(xiàn)圖片的懶加載
頁(yè)可見(jiàn)區(qū)域?qū)挘篸ocument.body.clientWidth;網(wǎng)頁(yè)可見(jiàn)區(qū)域高:document.body.clientHeight;網(wǎng)頁(yè)可見(jiàn)區(qū)域?qū)挘篸ocument.body.offsetWidth (包括邊線(xiàn)的寬);網(wǎng)頁(yè)可見(jiàn)區(qū)域高:document.body.offsetHeight (包括邊線(xiàn)的寬);網(wǎng)頁(yè)正文全文寬:document.body.scrollWidth;網(wǎng)頁(yè)正文全文高:document.body.scrollHeight;網(wǎng)頁(yè)被卷去的高:document.body.scrollTop;網(wǎng)頁(yè)被卷去的左:document.body.scrollLeft;網(wǎng)頁(yè)正文部分上:window.screenTop;網(wǎng)頁(yè)正文部分左:window.screenLeft;屏幕分辨率的高:window.screen.height;屏幕分辨率的寬:window.screen.width;屏幕可用工作區(qū)高度:window.screen.availHeight;
關(guān)于scrollTop,offsetTop,scrollLeft,offsetLeft用法介紹,點(diǎn)這里
「原理思路」
拿到所以的圖片 img dom重點(diǎn)是第二步,判斷當(dāng)前圖片是否到了可視區(qū)范圍內(nèi) 到了可視區(qū)的高度以后,就將img的src屬性設(shè)置給src 綁定window的 scroll事件
當(dāng)然了,為了用戶(hù)的體驗(yàn)更加,默認(rèn)的情況下,設(shè)置一個(gè)「占位圖」
本次測(cè)試代碼
CSS代碼?
img{display: block;height: 320px;margin-top: 20px;margin: 10px auto;}
HTML?
第一種方式
「clientHeight-scrollTop-offsetTop」
直接上我運(yùn)行的代碼?
let Img = document.getElementsByTagName("img"),len = Img.length,count = 0;function lazyLoad () {let viewH = document.body.clientHeight, //可見(jiàn)區(qū)域高度scrollTop = document.body.scrollTop; //滾動(dòng)條距離頂部高度for(let i = count; i < len; i++) {if(Img[i].offsetTop < scrollTop + viewH ){if(Img[i].getAttribute('src') === ''){Img[i].src = Img[i].getAttribute('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, //可見(jiàn)區(qū)域高度scrollTop = document.body.scrollTop; //滾動(dòng)條距離頂部高度for(let i = count; i < len; i++) {if(Img[i].getBoundingClientRect().top < scrollTop + viewH ){if(Img[i].getAttribute('src') === ''){Img[i].src = Img[i].getAttribute('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(); // 首次加載
好像也差不多,不知道是不是我寫(xiě)的方式有問(wèn)題(●'?'●),感覺(jué)差不多
來(lái)看看效果吧,我給這個(gè)事件加了一個(gè)節(jié)流,這樣子操作看起來(lái)就更好了。
12. 說(shuō)一說(shuō)你對(duì)Cookie localStorage sessionStorage
Cookie
得扯一下HTTP是一個(gè)無(wú)狀態(tài)的協(xié)議,這里主要指的是HTTP1.x版本,簡(jiǎn)單的可以理解為即使同一個(gè)客戶(hù)端連續(xù)兩次發(fā)送請(qǐng)求給服務(wù)器,服務(wù)器也無(wú)法識(shí)別這個(gè)同一個(gè)客戶(hù)端發(fā)的請(qǐng)求,導(dǎo)致的問(wèn)題,比如現(xiàn)實(shí)生活中你加入一個(gè)商品到購(gòu)物車(chē),但是因?yàn)闊o(wú)法識(shí)別同一個(gè)客戶(hù)端,你刷新頁(yè)面的話(huà)就?
為了解決 HTTP 無(wú)狀態(tài)導(dǎo)致的問(wèn)題(HTTP1.x),后來(lái)出現(xiàn)了 Cookie。
Cookie 的存在也不是為了解決通訊協(xié)議無(wú)狀態(tài)的問(wèn)題,只是為了解決客戶(hù)端與服務(wù)端會(huì)話(huà)狀態(tài)的問(wèn)題,這個(gè)狀態(tài)是指后端服務(wù)的狀態(tài)而非通訊協(xié)議的狀態(tài)。
Cookie存放在本地的好處就在于即使你關(guān)閉了瀏覽器,Cookie 依然可以生效。
Cookie設(shè)置
怎么去設(shè)置呢?簡(jiǎn)單來(lái)說(shuō)就是?
客戶(hù)端發(fā)送 HTTP 請(qǐng)求到服務(wù)器 當(dāng)服務(wù)器收到 HTTP 請(qǐng)求時(shí),在響應(yīng)頭里面添加一個(gè) Set-Cookie 字段 瀏覽器收到響應(yīng)后保存下 Cookie 之后對(duì)該服務(wù)器每一次請(qǐng)求中都通過(guò) Cookie 字段將 Cookie 信息發(fā)送給服務(wù)器。
Cookie指令
在下面這張圖里我們可以看到 Cookies 相關(guān)的一些屬性?
這里主要說(shuō)一些大家可能沒(méi)有注意的點(diǎn):
「Name/Value」
用 JavaScript 操作 Cookie 的時(shí)候注意對(duì) Value 進(jìn)行編碼處理。
Expires/Max-Age
Expires 用于設(shè)置 Cookie 的過(guò)期時(shí)間。比如:
Set-Cookie: id=aad3fWa; Expires=Wed, 21 May 2020 07:28:00 GMT;
當(dāng) Expires 屬性缺省時(shí),表示是會(huì)話(huà)性 Cookie。 像上圖 Expires 的值為 Session,表示的就是會(huì)話(huà)性 Cookie。 會(huì)話(huà)性 Cookie 的時(shí)候,值保存在客戶(hù)端內(nèi)存中,并在用戶(hù)關(guān)閉瀏覽器時(shí)失效。 需要注意的是,有些瀏覽器提供了會(huì)話(huà)恢復(fù)功能,關(guān)閉瀏覽器,會(huì)話(huà)期Cookie會(huì)保留下來(lái)。 與會(huì)話(huà)性 Cookie 相對(duì)的是持久性 Cookie,持久性 Cookies 會(huì)保存在用戶(hù)的硬盤(pán)中,直至過(guò)期或者清除 Cookie。
Max-Age 用于設(shè)置在 Cookie 失效之前需要經(jīng)過(guò)的秒數(shù)。比如:
Set-Cookie: id=a3fWa; Max-Age=604800;
假如 Expires 和 Max-Age 都存在,Max-Age 優(yōu)先級(jí)更高。
Domain
Domain 指定了 Cookie 可以送達(dá)的主機(jī)名。假如沒(méi)有指定,那么默認(rèn)值為當(dāng)前文檔訪(fǎng)問(wèn)地址中的主機(jī)部分(但是不包含子域名)。
在這里注意的是,不能跨域設(shè)置 Cookie
Path
Path 指定了一個(gè) URL 路徑,這個(gè)路徑必須出現(xiàn)在要請(qǐng)求的資源的路徑中才可以發(fā)送 Cookie 首部。比如設(shè)置 Path=/docs,/docs/Web/ 下的資源會(huì)帶 Cookie 首部,/test 則不會(huì)攜帶 Cookie 首部。
「Domain 和 Path 標(biāo)識(shí)共同定義了 Cookie 的作用域:即 Cookie 應(yīng)該發(fā)送給哪些 URL?!?/strong>
Secure屬性
標(biāo)記為 Secure 的 Cookie 只應(yīng)通過(guò)被HTTPS協(xié)議加密過(guò)的請(qǐng)求發(fā)送給服務(wù)端。使用 HTTPS 安全協(xié)議,可以保護(hù) Cookie 在瀏覽器和 Web 服務(wù)器間的傳輸過(guò)程中不被竊取和篡改。
HTTPOnly
設(shè)置 HTTPOnly 屬性可以防止客戶(hù)端腳本通過(guò) document.cookie 等方式訪(fǎng)問(wèn) Cookie,有助于避免 XSS 攻擊。
SameSite
SameSite 屬性可以讓 Cookie 在跨站請(qǐng)求時(shí)不會(huì)被發(fā)送,從而可以阻止跨站請(qǐng)求偽造攻擊(CSRF)。
后面講CSRF攻擊會(huì)將講到,這里過(guò)。
這個(gè)屬性值修改有什么影響呢?
從上圖可以看出,對(duì)大部分 web 應(yīng)用而言,Post 表單,iframe,AJAX,Image 這四種情況從以前的跨站會(huì)發(fā)送三方 Cookie,變成了不發(fā)送。
Cookie 的作用
Cookie 主要用于以下三個(gè)方面:
會(huì)話(huà)狀態(tài)管理(如用戶(hù)登錄狀態(tài)、購(gòu)物車(chē)、游戲分?jǐn)?shù)或其它需要記錄的信息) 個(gè)性化設(shè)置(如用戶(hù)自定義設(shè)置、主題等) 瀏覽器行為跟蹤(如跟蹤分析用戶(hù)行為等)
Cookie 的缺點(diǎn)
從大小,安全,增加請(qǐng)求大小。
容量缺陷。Cookie 的體積上限只有 4KB,只能用來(lái)存儲(chǔ)少量的信息。降低性能,Cookie緊跟著域名,不管域名下的某個(gè)地址是否需要這個(gè)Cookie,請(qǐng)求都會(huì)帶上完整的Cookie,請(qǐng)求數(shù)量增加,會(huì)造成巨大的浪費(fèi)。 安全缺陷,Cookie是以純文本的形式在瀏覽器和服務(wù)器中傳遞,很容易被非法用戶(hù)獲取,當(dāng)HTTPOnly為false時(shí),Cookie信息還可以直接通過(guò)JS腳本讀取。
localStorage 和 ?sessionStorage
在 web 本地存儲(chǔ)場(chǎng)景上,cookie 的使用受到種種限制,最關(guān)鍵的就是存儲(chǔ)容量太小和數(shù)據(jù)無(wú)法持久化存儲(chǔ)。
在 HTML 5 的標(biāo)準(zhǔn)下,出現(xiàn)了 localStorage 和 sessionStorage 供我們使用。
異同點(diǎn)
| 分類(lèi) | 生命周期 | 存儲(chǔ)容量 | 存儲(chǔ)位置 |
|---|---|---|---|
| cookie | 默認(rèn)保存在內(nèi)存中,隨瀏覽器關(guān)閉失效(如果設(shè)置過(guò)期時(shí)間,在到過(guò)期時(shí)間后失效) | 4KB | 保存在客戶(hù)端,每次請(qǐng)求時(shí)都會(huì)帶上 |
| localStorage | 理論上永久有效的,除非主動(dòng)清除。 | 4.98MB(不同瀏覽器情況不同,safari 2.49M) | 保存在客戶(hù)端,不與服務(wù)端交互。節(jié)省網(wǎng)絡(luò)流量 |
| sessionStorage | 僅在當(dāng)前網(wǎng)頁(yè)會(huì)話(huà)下有效,關(guān)閉頁(yè)面或?yàn)g覽器后會(huì)被清除。 | 4.98MB(部分瀏覽器沒(méi)有限制) | 同上 |
操作方式
接下來(lái)我們來(lái)具體看看如何來(lái)操作localStorage和sessionStorage
let obj = { name: "TianTianUp", age: 18 };localStorage.setItem("name", "TianTianUp");localStorage.setItem("info", JSON.stringify(obj));
接著進(jìn)入相同的域名時(shí)就能拿到相應(yīng)的值?
let name = localStorage.getItem("name");let info = JSON.parse(localStorage.getItem("info"));
從這里可以看出,localStorage其實(shí)存儲(chǔ)的都是字符串,如果是存儲(chǔ)對(duì)象需要調(diào)用JSON的stringify方法,并且用JSON.parse來(lái)解析成對(duì)象。
應(yīng)用場(chǎng)景
localStorage 適合持久化緩存數(shù)據(jù),比如頁(yè)面的默認(rèn)偏好配置,如官網(wǎng)的 logo,存儲(chǔ)Base64格式的圖片資源等;sessionStorage 適合一次性臨時(shí)數(shù)據(jù)保存,存儲(chǔ)本次瀏覽信息記錄,這樣子頁(yè)面關(guān)閉的話(huà),就不需要這些記錄了,還有對(duì)表單信息進(jìn)行維護(hù),這樣子頁(yè)面刷新的話(huà),也不會(huì)讓表單信息丟失。
13. 聊一聊瀏覽器緩存
瀏覽器緩存是性能優(yōu)化的一個(gè)重要手段,對(duì)于理解緩存機(jī)制而言也是很重要的,我們來(lái)梳理一下吧?
強(qiáng)緩存
強(qiáng)緩存兩個(gè)相關(guān)字段,「Expires」,「Cache-Control」。
「強(qiáng)緩存分為兩種情況,一種是發(fā)送HTTP請(qǐng)求,一種不需要發(fā)送。」
首先檢查強(qiáng)緩存,這個(gè)階段**不需要發(fā)送HTTP請(qǐng)求。**通過(guò)查找不同的字段來(lái)進(jìn)行,不同的HTTP版本所以不同。
HTTP1.0版本,使用的是Expires,HTTP1.1使用的是Cache-Control
Expires
Expires即過(guò)期時(shí)間,時(shí)間是相對(duì)于服務(wù)器的時(shí)間而言的,存在于服務(wù)端返回的響應(yīng)頭中,在這個(gè)過(guò)期時(shí)間之前可以直接從緩存里面獲取數(shù)據(jù),無(wú)需再次請(qǐng)求。比如下面這樣:
Expires:Mon, 29 Jun 2020 11:10:23 GMT
表示該資源在2020年7月29日11:10:23過(guò)期,過(guò)期時(shí)就會(huì)重新向服務(wù)器發(fā)起請(qǐng)求。
這個(gè)方式有一個(gè)問(wèn)題:「服務(wù)器的時(shí)間和瀏覽器的時(shí)間可能并不一致」,所以HTTP1.1提出新的字段代替它。
Cache-Control
HTTP1.1版本中,使用的就是該字段,這個(gè)字段采用的時(shí)間是過(guò)期時(shí)長(zhǎng),對(duì)應(yīng)的是max-age。
Cache-Control:max-age=6000
上面代表該資源返回后6000秒,可以直接使用緩存。
當(dāng)然了,它還有其他很多關(guān)鍵的指令,梳理了幾個(gè)重要的?
注意點(diǎn):
當(dāng)Expires和Cache-Control同時(shí)存在時(shí),優(yōu)先考慮Cache-Control。 當(dāng)然了,當(dāng)緩存資源失效了,也就是沒(méi)有命中強(qiáng)緩存,接下來(lái)就進(jìn)入?yún)f(xié)商緩存?
協(xié)商緩存
強(qiáng)緩存失效后,瀏覽器在請(qǐng)求頭中攜帶響應(yīng)的緩存Tag來(lái)向服務(wù)器發(fā)送請(qǐng)求,服務(wù)器根據(jù)對(duì)應(yīng)的tag,來(lái)決定是否使用緩存。
緩存分為兩種,「Last-Modified」 和 「ETag」。兩者各有優(yōu)勢(shì),并不存在誰(shuí)對(duì)誰(shuí)有絕對(duì)的優(yōu)勢(shì),與上面所講的強(qiáng)緩存兩個(gè)Tag所不同。
Last-Modified
這個(gè)字段表示的是「最后修改時(shí)間」。在瀏覽器第一次給服務(wù)器發(fā)送請(qǐng)求后,服務(wù)器會(huì)在響應(yīng)頭中加上這個(gè)字段。
瀏覽器接收到后,「如果再次請(qǐng)求」,會(huì)在請(qǐng)求頭中攜帶If-Modified-Since字段,這個(gè)字段的值也就是服務(wù)器傳來(lái)的最后修改時(shí)間。
服務(wù)器拿到請(qǐng)求頭中的If-Modified-Since的字段后,其實(shí)會(huì)和這個(gè)服務(wù)器中該資源的最后修改時(shí)間對(duì)比:
如果請(qǐng)求頭中的這個(gè)值小于最后修改時(shí)間,說(shuō)明是時(shí)候更新了。返回新的資源,跟常規(guī)的HTTP請(qǐng)求響應(yīng)的流程一樣。 否則返回304,告訴瀏覽器直接使用緩存。
ETag
ETag是服務(wù)器根據(jù)當(dāng)前文件的內(nèi)容,對(duì)文件生成唯一的標(biāo)識(shí),比如MD5算法,只要里面的內(nèi)容有改動(dòng),這個(gè)值就會(huì)修改,服務(wù)器通過(guò)把響應(yīng)頭把該字段給瀏覽器。
瀏覽器接受到ETag值,會(huì)在下次請(qǐng)求的時(shí)候,將這個(gè)值作為「If-None-Match」這個(gè)字段的內(nèi)容,發(fā)給服務(wù)器。
服務(wù)器接收到「If-None-Match」后,會(huì)跟服務(wù)器上該資源的「ETag」進(jìn)行比對(duì)?
如果兩者一樣的話(huà),直接返回304,告訴瀏覽器直接使用緩存 如果不一樣的話(huà),說(shuō)明內(nèi)容更新了,返回新的資源,跟常規(guī)的HTTP請(qǐng)求響應(yīng)的流程一樣
兩者對(duì)比
性能上, Last-Modified優(yōu)于ETag,Last-Modified記錄的是時(shí)間點(diǎn),而Etag需要根據(jù)文件的MD5算法生成對(duì)應(yīng)的hash值。精度上, ETag優(yōu)于Last-Modified。ETag按照內(nèi)容給資源帶上標(biāo)識(shí),能準(zhǔn)確感知資源變化,Last-Modified在某些場(chǎng)景并不能準(zhǔn)確感知變化,比如?編輯了資源文件,但是文件內(nèi)容并沒(méi)有更改,這樣也會(huì)造成緩存失效。 Last-Modified 能夠感知的單位時(shí)間是秒,如果文件在 1 秒內(nèi)改變了多次,那么這時(shí)候的 Last-Modified 并沒(méi)有體現(xiàn)出修改了。
最后,「如果兩種方式都支持的話(huà),服務(wù)器會(huì)優(yōu)先考慮ETag」。
緩存位置
接下來(lái)我們考慮使用緩存的話(huà),緩存的位置在哪里呢?
瀏覽器緩存的位置的話(huà),可以分為四種,優(yōu)先級(jí)從高到低排列分別?
Service Worker Memory Cache Disk Cache Push Cache
Service Worker
這個(gè)應(yīng)用場(chǎng)景比如PWA,它借鑒了Web Worker思路,由于它脫離了瀏覽器的窗體,因此無(wú)法直接訪(fǎng)問(wèn)DOM。它能完成的功能比如:離線(xiàn)緩存、消息推送和網(wǎng)絡(luò)代理,其中離線(xiàn)緩存就是「Service Worker Cache」。
Memory Cache
指的是內(nèi)存緩存,從效率上講它是最快的,從存活時(shí)間來(lái)講又是最短的,當(dāng)渲染進(jìn)程結(jié)束后,內(nèi)存緩存也就不存在了。
Disk Cache
存儲(chǔ)在磁盤(pán)中的緩存,從存取效率上講是比內(nèi)存緩存慢的,優(yōu)勢(shì)在于存儲(chǔ)容量和存儲(chǔ)時(shí)長(zhǎng)。
Disk Cache VS Memory Cache
兩者對(duì)比,主要的策略?
內(nèi)容使用率高的話(huà),文件優(yōu)先進(jìn)入磁盤(pán)
比較大的JS,CSS文件會(huì)直接放入磁盤(pán),反之放入內(nèi)存。
Push Cache
推送緩存,這算是瀏覽器中最后一道防線(xiàn)吧,它是HTTP/2的內(nèi)容。具體我也不是很清楚,有興趣的可以去了解。
總結(jié)
首先檢查 Cache-Control, 嘗鮮,看強(qiáng)緩存是否可用如果可用的話(huà),直接使用 否則進(jìn)入?yún)f(xié)商緩存,發(fā)送HTTP請(qǐng)求,服務(wù)器通過(guò)請(qǐng)求頭中的 If-Modified-Since或者If-None-Match字段檢查資源是否更新資源更新,返回資源和200狀態(tài)碼。 否則,返回304,直接告訴瀏覽器直接從緩存中去資源。
14. 說(shuō)一說(shuō)從輸入U(xiǎn)RL到頁(yè)面呈現(xiàn)發(fā)生了什么?
一旦問(wèn)這個(gè)問(wèn)題的話(huà),我覺(jué)得肯定是一個(gè)非常深的問(wèn)題了,無(wú)論從深度還是廣度上,要真的答好這個(gè)題目,或者梳理清楚的話(huà),挺難的,畢竟一個(gè)非常綜合性的問(wèn)題,我作為一個(gè)剛剛?cè)腴T(mén)的小白,只能梳理部分知識(shí),更深的知識(shí)可以去看看參考鏈接。
那么我們就開(kāi)始吧,假設(shè)你輸入的內(nèi)容是?
https://juejin.im/???
網(wǎng)絡(luò)請(qǐng)求
1. 構(gòu)建請(qǐng)求
首先,瀏覽器構(gòu)建「請(qǐng)求行」信息(如下所示),構(gòu)建好后,瀏覽器準(zhǔn)備發(fā)起網(wǎng)絡(luò)請(qǐng)求?
GET / HTTP1.1GET是請(qǐng)求方法,路徑就是根路徑,HTTP協(xié)議版本1.1
2. 查找緩存
在真正發(fā)起網(wǎng)絡(luò)請(qǐng)求之前,瀏覽器會(huì)先在瀏覽器緩存中查詢(xún)是否有要請(qǐng)求的文件。
先檢查強(qiáng)緩存,如果命中的話(huà)直接使用,否則進(jìn)入下一步,強(qiáng)緩存的知識(shí)點(diǎn),上面?梳理過(guò)了。
3. DNS解析
輸入的域名的話(huà),我們需要根據(jù)域名去獲取對(duì)應(yīng)的ip地址。這個(gè)過(guò)程需要依賴(lài)一個(gè)服務(wù)系統(tǒng),叫做是DNS域名解析, 從查找到獲取到具體IP的過(guò)程叫做是DNS解析。
關(guān)于DNS篇,可以看看阮一峰的網(wǎng)絡(luò)日志
首先,瀏覽器提供了DNS數(shù)據(jù)緩存功能,如果一個(gè)域名已經(jīng)解析過(guò)了,那么就會(huì)把解析的結(jié)果緩存下來(lái),下次查找的話(huà),直接去緩存中找,不需要結(jié)果DNS解析。
「解析過(guò)程總結(jié)如下」?
「首先查看是否有對(duì)應(yīng)的域名緩存,有的話(huà)直接用緩存的ip訪(fǎng)問(wèn)」
ipconfig /displaydns// 輸入這個(gè)命令就可以查看對(duì)應(yīng)的電腦中是否有緩存「如果緩存中沒(méi)有,則去查找hosts文件」 一般在
c:\windows\system32\drivers\etc\hosts如果hosts文件里沒(méi)找到想解析的域名,則將「域名發(fā)往自己配置的dns服務(wù)器」,也叫「本地dns服務(wù)器」。
ipconfig/all通過(guò)這個(gè)命令可以查看自己的本地dns服務(wù)器如果「本地dns服務(wù)器有相應(yīng)域名的記錄」,則返回記錄。
電腦的dns服務(wù)器一般是各大運(yùn)營(yíng)商如電信聯(lián)通提供的,或者像180.76.76.76,223.5.5.5,4個(gè)114等知名dns服務(wù)商提供的,本身緩存了大量的常見(jiàn)域名的ip,所以常見(jiàn)的網(wǎng)站,都是有記錄的。不需要找根服務(wù)器。
如果電腦自己的服務(wù)器沒(méi)有記錄,會(huì)去找根服務(wù)器。根服務(wù)器全球只要13臺(tái),回去找其中之一,找了根服務(wù)器后,「根服務(wù)器會(huì)根據(jù)請(qǐng)求的域名,返回對(duì)應(yīng)的“頂級(jí)域名服務(wù)器”」,如:
如果請(qǐng)求的域名是http://xxx.com,則返回負(fù)責(zé)com域的服務(wù)器 如果是http://xxx.cn,則發(fā)給負(fù)責(zé)cn域的服務(wù)器 如果是http://xxx.ca,則發(fā)給負(fù)責(zé)ca域的服務(wù)器 「頂級(jí)域服務(wù)器收到請(qǐng)求,會(huì)返回二級(jí)域服務(wù)器的地址」
比如一個(gè)網(wǎng)址是 www.xxx.edu.cn,則頂級(jí)域名服務(wù)器再轉(zhuǎn)發(fā)給負(fù)責(zé).edu.cn域的二級(jí)服務(wù)器「以此類(lèi)推,最終會(huì)發(fā)到負(fù)責(zé)鎖查詢(xún)域名的,最精確的那臺(tái)dns,可以得到查詢(xún)結(jié)果?!?/strong>
最后一步,「本地dns服務(wù)器,把最終的解析結(jié)果,返回給客戶(hù)端,對(duì)客戶(hù)端來(lái)講,只是一去一回的事,客戶(hù)端并不知道本地dns服務(wù)器經(jīng)過(guò)了千山萬(wàn)水?!?/strong>
以上就是大概的過(guò)程了,有興趣的話(huà),可以仔細(xì)去看看。
建立TCP鏈接
我們所了解的就是?Chrome 在同一個(gè)域名下要求同時(shí)最多只能有 6 個(gè) TCP 連接,超過(guò) 6 個(gè)的話(huà)剩下的請(qǐng)求就得等待。
那么我們假設(shè)不需要等待,我們進(jìn)入了TCP連接的建立階段。
建立TCP連接經(jīng)歷下面三個(gè)階段:
通過(guò)「三次握手」建立客戶(hù)端和服務(wù)器之間的連接。 進(jìn)行數(shù)據(jù)傳輸。 斷開(kāi)連接的階段。數(shù)據(jù)傳輸完成,現(xiàn)在要斷開(kāi)連接了,通過(guò)「四次揮手」來(lái)斷開(kāi)連接。
從上面看得出來(lái),TCP 連接通過(guò)什么手段來(lái)保證數(shù)據(jù)傳輸?shù)目煽啃裕皇?code style="overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(145, 109, 213);font-weight: bolder;background-image: none;background-position: initial;background-size: initial;background-repeat: initial;background-attachment: initial;background-origin: initial;background-clip: initial;">三次握手確認(rèn)連接,二是數(shù)據(jù)包校驗(yàn)保證數(shù)據(jù)到達(dá)接收方,三是通過(guò)四次揮手斷開(kāi)連接。
深入理解的話(huà),可以看看對(duì)應(yīng)的文章,掘金上面很多文章都有深入了解,這里就不梳理了。
發(fā)送HTTP請(qǐng)求
TCP連接完成后,接下來(lái)就可以與服務(wù)器通信了,也就是我們經(jīng)常說(shuō)的發(fā)送HTTP請(qǐng)求。
發(fā)送HTTP請(qǐng)求的話(huà),需要攜帶三樣?xùn)|西:「請(qǐng)求行」,「請(qǐng)求頭」,「請(qǐng)求體」。
我們看看大概是是什么樣子的吧?
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3Accept-Encoding: gzip, deflate, brAccept-Language: zh-CN,zh;q=0.9Cache-Control: no-cacheConnection: keep-aliveCookie: /* 省略cookie信息 */Host: juejin.imPragma: no-cacheUpgrade-Insecure-Requests: 1User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36
最后就是請(qǐng)求體,請(qǐng)求體的話(huà)只有在POST請(qǐng)求場(chǎng)景下存在,常見(jiàn)的就是表單提交
網(wǎng)絡(luò)響應(yīng)
HTTP 請(qǐng)求到達(dá)服務(wù)器,服務(wù)器進(jìn)行對(duì)應(yīng)的處理。最后要把數(shù)據(jù)傳給瀏覽器,也就是通常我們說(shuō)的返回網(wǎng)絡(luò)響應(yīng)。
跟請(qǐng)求部分類(lèi)似,網(wǎng)絡(luò)響應(yīng)具有三個(gè)部分:「響應(yīng)行」、「響應(yīng)頭」和「響應(yīng)體」。
響應(yīng)行類(lèi)似下面這樣?
HTTP/1.1 200 OK
對(duì)應(yīng)的響應(yīng)頭數(shù)據(jù)是怎么樣的呢?我們來(lái)舉個(gè)例子看看?
Access-Control-Max-Age: 86400Cache-control: privateConnection: closeContent-Encoding: gzipContent-Type: text/html;charset=utf-8Date: Wed, 22 Jul 2020 13:24:49 GMTVary: Accept-EncodingSet-Cookie: ab={}; path=/; expires=Thu, 22 Jul 2021 13:24:49 GMT; secure; httponlyTransfer-Encoding: chunked
接下來(lái),我們數(shù)據(jù)拿到了,你認(rèn)為就會(huì)斷開(kāi)TCP連接嗎?
這個(gè)的看響應(yīng)頭中的Connection字段。上面的字段值為close,那么就會(huì)斷開(kāi),一般情況下,HTTP1.1版本的話(huà),通常請(qǐng)求頭會(huì)包含「Connection: Keep-Alive」表示建立了持久連接,這樣TCP連接會(huì)一直保持,之后請(qǐng)求統(tǒng)一站點(diǎn)的資源會(huì)復(fù)用這個(gè)連接。
上面的情況就會(huì)斷開(kāi)TCP連接,請(qǐng)求-響應(yīng)流程結(jié)束。
到這里的話(huà),網(wǎng)絡(luò)請(qǐng)求就告一段落了,接下來(lái)的內(nèi)容就是渲染流程了?
渲染階段
較為專(zhuān)業(yè)的術(shù)語(yǔ)總結(jié)為以下階段:
構(gòu)建DOM樹(shù) 樣式計(jì)算 布局階段 分層 繪制 分塊 光柵化 合成
關(guān)于渲染流程的話(huà),可以看我之前總結(jié)的一篇???
[1.1W字]寫(xiě)給女友的秘籍-瀏覽器工作原理(渲染流程)篇
15. 談一談你對(duì)重排和重繪理解
關(guān)于重排和重繪,可以上面的知識(shí)點(diǎn)去梳理,也就是渲染階段,里面也梳理了部分的點(diǎn),(●'?'●)
偷個(gè)懶,看下面的文章噢?
[1.1W字]寫(xiě)給女友的秘籍-瀏覽器工作原理(渲染流程)篇
16. 談一談跨域,同源策略,以及跨域解決方案
什么是跨域
跨域,是指瀏覽器不能執(zhí)行其他網(wǎng)站的腳本。它是由瀏覽器的同源策略造成的,是瀏覽器對(duì)JavaScript實(shí)施的安全限制。
同源策略
同源策略是一個(gè)安全策略。所謂的同源,指的是協(xié)議,域名,端口相同。
瀏覽器處于安全方面的考慮,只允許本域名下的接口交互,不同源的客戶(hù)端腳本,在沒(méi)有明確授權(quán)的情況下,不能讀寫(xiě)對(duì)方的資源。
限制了一下行為:
Cookie、LocalStorage 和 IndexDB 無(wú)法讀取 DOM 和 JS 對(duì)象無(wú)法獲取 Ajax請(qǐng)求發(fā)送不出去
解決方案
當(dāng)然了,我梳理了幾個(gè)我覺(jué)得工作中常用的,其他的自行去了解。
jsonp跨域
利用script標(biāo)簽沒(méi)有跨域限制的漏洞,網(wǎng)頁(yè)可以拿到從其他來(lái)源產(chǎn)生動(dòng)態(tài)JSON數(shù)據(jù),當(dāng)然了JSONP請(qǐng)求一定要對(duì)方的服務(wù)器做支持才可以。
「與AJAX對(duì)比」
JSONP和AJAX相同,都是客戶(hù)端向服務(wù)器發(fā)送請(qǐng)求,從服務(wù)器獲取數(shù)據(jù)的方式。但是AJAX屬于同源策略,JSONP屬于非同源策略(跨域請(qǐng)求)
「JSONP優(yōu)點(diǎn)」
兼容性比較好,可用于解決主流瀏覽器的跨域數(shù)據(jù)訪(fǎng)問(wèn)的問(wèn)題。缺點(diǎn)就是僅支持get請(qǐng)求,具有局限性,不安全,可能會(huì)受到XSS攻擊。
「思路?」
創(chuàng)建script標(biāo)簽 設(shè)置script標(biāo)簽的src屬性,以問(wèn)號(hào)傳遞參數(shù),設(shè)置好回調(diào)函數(shù)callback名稱(chēng) 插入html文本中 調(diào)用回調(diào)函數(shù),res參數(shù)就是獲取的數(shù)據(jù)
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);}
當(dāng)然,jquery也支持jsonp的實(shí)現(xiàn)方式
$.ajax({url: 'http://www.baidu.cn/login',type: 'GET',dataType: 'jsonp', //請(qǐng)求方式為jsonpjsonpCallback: 'callback',data: {"username": "Nealyang"}})
「JSONP優(yōu)點(diǎn)」
它不像XMLHttpRequest對(duì)象實(shí)現(xiàn)的Ajax請(qǐng)求那樣受到同源策略的限制 它的兼容性更好,在更加古老的瀏覽器中都可以運(yùn)行,不需要XMLHttpRequest或ActiveX的支持 并且在請(qǐng)求完畢后可以通過(guò)調(diào)用callback的方式回傳結(jié)果。
「JSONP缺點(diǎn)」
它只支持GET請(qǐng)求而不支持POST等其它類(lèi)型的HTTP請(qǐng)求 它只支持跨域HTTP請(qǐng)求這種情況,不能解決不同域的兩個(gè)頁(yè)面之間如何進(jìn)行JavaScript調(diào)用的問(wèn)題
跨域資源共享 CORS
CORS(Cross-Origin Resource Sharing)跨域資源共享,定義了必須在訪(fǎng)問(wèn)跨域資源時(shí),瀏覽器與服務(wù)器應(yīng)該如何溝通。CORS背后的基本思想就是使用自定義的HTTP頭部讓瀏覽器與服務(wù)器進(jìn)行溝通,從而決定請(qǐng)求或響應(yīng)是應(yīng)該成功還是失敗。目前,所有瀏覽器都支持該功能,IE瀏覽器不能低于IE10。整個(gè)CORS通信過(guò)程,都是瀏覽器自動(dòng)完成,不需要用戶(hù)參與。對(duì)于開(kāi)發(fā)者來(lái)說(shuō),CORS通信與同源的AJAX通信沒(méi)有差別,代碼完全一樣。瀏覽器一旦發(fā)現(xiàn)AJAX請(qǐng)求跨源,就會(huì)自動(dòng)添加一些附加的頭信息,有時(shí)還會(huì)多出一次附加的請(qǐng)求,但用戶(hù)不會(huì)有感覺(jué)。
上面是引用,你要記住的關(guān)鍵點(diǎn)?
「CORS 需要瀏覽器和后端同時(shí)支持。IE 8 和 9 需要通過(guò) XDomainRequest 來(lái)實(shí)現(xiàn)」。
「瀏覽器會(huì)自動(dòng)進(jìn)行 CORS 通信,實(shí)現(xiàn) CORS 通信的關(guān)鍵是后端。只要后端實(shí)現(xiàn)了 CORS,就實(shí)現(xiàn)了跨域?!?/strong> 服務(wù)端設(shè)置 Access-Control-Allow-Origin 就可以開(kāi)啟 CORS。該屬性表示哪些域名可以訪(fǎng)問(wèn)資源,如果設(shè)置通配符則表示所有網(wǎng)站都可以訪(fǎng)問(wèn)資源。
請(qǐng)求分為「簡(jiǎn)單請(qǐng)求」和「非簡(jiǎn)單請(qǐng)求」,所以我們的了解這兩種情況。
「簡(jiǎn)單請(qǐng)求」
滿(mǎn)足下面兩個(gè)條件,就屬于簡(jiǎn)單請(qǐng)求?
條件1:使用下列方法之一:
GET HEAD POST
條件2:Content-Type 的值僅限于下列三者之一?
text/plain multipart/form-data application/x-www-form-urlencoded
請(qǐng)求中的任意 XMLHttpRequestUpload 對(duì)象均沒(méi)有注冊(cè)任何事件監(jiān)聽(tīng)器;
XMLHttpRequestUpload 對(duì)象可以使用 XMLHttpRequest.upload 屬性訪(fǎng)問(wèn)。
「復(fù)雜請(qǐng)求」
不符合以上條件的請(qǐng)求就肯定是復(fù)雜請(qǐng)求了。復(fù)雜請(qǐng)求的CORS請(qǐng)求,會(huì)在正式通信之前,增加一次HTTP查詢(xún)請(qǐng)求,稱(chēng)為"預(yù)檢"請(qǐng)求,該請(qǐng)求是 option 方法的,通過(guò)該請(qǐng)求來(lái)知道服務(wù)端是否允許跨域請(qǐng)求。
直接上一個(gè)例子吧? 看看一個(gè)完整的復(fù)雜請(qǐng)求吧,并且介紹一下CORS請(qǐng)求的字段。
//server2.jslet express = require('express')let app = express()let whitList = ['http://localhost:3000'] //設(shè)置白名單app.use(function(req, res, next) {let origin = req.headers.originif (whitList.includes(origin)) {// 設(shè)置哪個(gè)源可以訪(fǎng)問(wèn)我res.setHeader('Access-Control-Allow-Origin', origin)// 允許攜帶哪個(gè)頭訪(fǎng)問(wèn)我res.setHeader('Access-Control-Allow-Headers', 'name')// 允許哪個(gè)方法訪(fǎng)問(wèn)我res.setHeader('Access-Control-Allow-Methods', 'PUT')// 允許攜帶cookieres.setHeader('Access-Control-Allow-Credentials', true)// 預(yù)檢的存活時(shí)間res.setHeader('Access-Control-Max-Age', 6)// 允許返回的頭res.setHeader('Access-Control-Expose-Headers', 'name')if (req.method === 'OPTIONS') {res.end() // OPTIONS請(qǐng)求不做任何處理}}next()})app.put('/getData', function(req, res) {console.log(req.headers)res.setHeader('name', 'jw') //返回一個(gè)響應(yīng)頭,后臺(tái)需設(shè)置res.end('我不愛(ài)你')})app.get('/getData', function(req, res) {console.log(req.headers)res.end('我不愛(ài)你')})app.use(express.static(__dirname))app.listen(4000)
上述代碼由http://localhost:3000/index.html向http://localhost:4000/跨域請(qǐng)求,正如我們上面所說(shuō)的,后端是實(shí)現(xiàn) CORS 通信的關(guān)鍵。
上述的例子,一定對(duì)你會(huì)有所幫助的,這塊代碼,是跟著浪里行舟代碼來(lái)的,參考處注明了出處。
「與JSONP對(duì)比」
JSONP只能實(shí)現(xiàn)GET請(qǐng)求,而CORS支持所有類(lèi)型的HTTP請(qǐng)求。 使用CORS,開(kāi)發(fā)者可以使用普通的XMLHttpRequest發(fā)起請(qǐng)求和獲得數(shù)據(jù),比起JSONP有更好的錯(cuò)誤處理。 JSONP主要被老的瀏覽器支持,它們往往不支持CORS,而絕大多數(shù)現(xiàn)代瀏覽器都已經(jīng)支持了CORS)
WebSocket協(xié)議跨域
Websocket是HTML5的一個(gè)持久化的協(xié)議,它實(shí)現(xiàn)了瀏覽器與服務(wù)器的全雙工通信,同時(shí)也是跨域的一種解決方案。
WebSocket和HTTP都是應(yīng)用層協(xié)議,都基于 TCP 協(xié)議。但是 「WebSocket 是一種雙向通信協(xié)議,在建立連接之后,WebSocket 的 server 與 client 都能主動(dòng)向?qū)Ψ桨l(fā)送或接收數(shù)據(jù)」。同時(shí),WebSocket 在建立連接時(shí)需要借助 HTTP 協(xié)議,連接建立好了之后 client 與 server 之間的雙向通信就與 HTTP 無(wú)關(guān)了。
我們先來(lái)看個(gè)例子?
本地文件socket.html向localhost:3000發(fā)生數(shù)據(jù)和接受數(shù)據(jù)?
// socket.html
后端部分?
// server.jslet WebSocket = require('ws'); //記得安裝wslet wss = new WebSocket.Server({port:3000});wss.on('connection',function(ws) {ws.on('message', function (data) {console.log(data);ws.send('我不愛(ài)你')});})
如果 你想去嘗試的話(huà),建議可以去玩一玩Socket.io,
這是因?yàn)樵鶺ebSocket API使用起來(lái)不太方便,它很好地封裝了webSocket接口 提供了更簡(jiǎn)單、靈活的接口,也對(duì)不支持webSocket的瀏覽器提供了向下兼容。
nginx代理跨域
17. 談一談你對(duì)XSS攻擊理解
什么是 XSS 攻擊
XSS 全稱(chēng)是 Cross Site Scripting ,為了與CSS區(qū)分開(kāi)來(lái),故簡(jiǎn)稱(chēng) XSS,翻譯過(guò)來(lái)就是“跨站腳本”。
XSS是指黑客往 HTML 文件中或者 DOM 中注入惡意腳本,從而在用戶(hù)瀏覽頁(yè)面時(shí)利用注入的惡意腳本對(duì)用戶(hù)實(shí)施攻擊的一種手段。
最開(kāi)始的時(shí)候,這種攻擊是通過(guò)跨域來(lái)實(shí)現(xiàn)的,所以叫“跨域腳本”。發(fā)展到現(xiàn)在,往HTML文件中中插入惡意代碼方式越來(lái)越多,所以是否跨域注入腳本已經(jīng)不是唯一的注入手段了,但是 XSS 這個(gè)名字卻一直保留至今。
注入惡意腳本可以完成這些事情:
竊取Cookie 監(jiān)聽(tīng)用戶(hù)行為,比如輸入賬號(hào)密碼后之間發(fā)給黑客服務(wù)器 在網(wǎng)頁(yè)中生成浮窗廣告 修改DOM偽造登入表單
一般的情況下,XSS攻擊有三種實(shí)現(xiàn)方式
存儲(chǔ)型 XSS 攻擊 反射型 XSS 攻擊 基于 DOM 的 XSS 攻擊
存儲(chǔ)型 XSS 攻擊
從圖上看,存儲(chǔ)型 XSS 攻擊大致步驟如下:
首先黑客利用站點(diǎn)漏洞將一段惡意 JavaScript 代碼提交到網(wǎng)站的數(shù)據(jù)庫(kù)中; 然后用戶(hù)向網(wǎng)站請(qǐng)求包含了惡意 JavaScript 腳本的頁(yè)面; 當(dāng)用戶(hù)瀏覽該頁(yè)面的時(shí)候,惡意腳本就會(huì)將用戶(hù)的 Cookie 信息等數(shù)據(jù)上傳到服務(wù)器。
比如常見(jiàn)的場(chǎng)景:
在評(píng)論區(qū)提交一份腳本代碼,假設(shè)前后端沒(méi)有做好轉(zhuǎn)義工作,那內(nèi)容上傳到服務(wù)器,在頁(yè)面渲染的時(shí)候就會(huì)直接執(zhí)行,相當(dāng)于執(zhí)行一段未知的JS代碼。這就是存儲(chǔ)型 XSS 攻擊。
反射型 XSS 攻擊
反射型 XSS 攻擊指的就是惡意腳本作為「網(wǎng)絡(luò)請(qǐng)求的一部分」,隨后網(wǎng)站又把惡意的JavaScript腳本返回給用戶(hù),當(dāng)惡意 JavaScript 腳本在用戶(hù)頁(yè)面中被執(zhí)行時(shí),黑客就可以利用該腳本做一些惡意操作。
舉個(gè)例子:
http://TianTianUp.com?query=
如上,服務(wù)器拿到后解析參數(shù)query,最后將內(nèi)容返回給瀏覽器,瀏覽器將這些內(nèi)容作為HTML的一部分解析,發(fā)現(xiàn)是Javascript腳本,直接執(zhí)行,這樣子被XSS攻擊了。
這也就是反射型名字的由來(lái),將惡意腳本作為參數(shù),通過(guò)網(wǎng)絡(luò)請(qǐng)求,最后經(jīng)過(guò)服務(wù)器,在反射到HTML文檔中,執(zhí)行解析。
主要注意的就是,「服務(wù)器不會(huì)存儲(chǔ)這些惡意的腳本,這也算是和存儲(chǔ)型XSS攻擊的區(qū)別吧」。
基于 DOM 的 XSS 攻擊
基于 DOM 的 XSS 攻擊是不牽涉到頁(yè)面 Web 服務(wù)器的。具體來(lái)講,黑客通過(guò)各種手段將惡意腳本注入用戶(hù)的頁(yè)面中,在數(shù)據(jù)傳輸?shù)臅r(shí)候劫持網(wǎng)絡(luò)數(shù)據(jù)包
常見(jiàn)的劫持手段有:
WIFI路由器劫持 本地惡意軟件
阻止 XSS 攻擊的策略
以上講述的XSS攻擊原理,都有一個(gè)共同點(diǎn):讓惡意腳本直接在瀏覽器執(zhí)行。
針對(duì)三種不同形式的XSS攻擊,有以下三種解決辦法
對(duì)輸入腳本進(jìn)行過(guò)濾或轉(zhuǎn)碼
對(duì)用戶(hù)輸入的信息過(guò)濾或者是轉(zhuǎn)碼
舉個(gè)例子?
轉(zhuǎn)碼后?
這樣的代碼在 html 解析的過(guò)程中是無(wú)法執(zhí)行的。
當(dāng)然了對(duì)于
訪(fǎng)問(wèn)該頁(yè)面后,表單會(huì)自動(dòng)提交,相當(dāng)于模擬用戶(hù)完成了一次POST操作。
同樣也會(huì)攜帶相應(yīng)的用戶(hù) cookie 信息,讓服務(wù)器誤以為是一個(gè)正常的用戶(hù)在操作,讓各種惡意的操作變?yōu)榭赡堋?/p>
3. 引誘用戶(hù)點(diǎn)擊鏈接
這種需要誘導(dǎo)用戶(hù)去點(diǎn)擊鏈接才會(huì)觸發(fā),這類(lèi)的情況比如在論壇中發(fā)布照片,照片中嵌入了惡意鏈接,或者是以廣告的形式去誘導(dǎo),比如:
重磅消息?。?!
點(diǎn)擊后,自動(dòng)發(fā)送 get 請(qǐng)求,接下來(lái)和自動(dòng)發(fā) GET 請(qǐng)求部分同理。
以上三種情況,就是CSRF攻擊原理,跟XSS對(duì)比的話(huà),CSRF攻擊并不需要將惡意代碼注入HTML中,而是跳轉(zhuǎn)新的頁(yè)面,利用「服務(wù)器的驗(yàn)證漏洞」和「用戶(hù)之前的登錄狀態(tài)」來(lái)模擬用戶(hù)進(jìn)行操作
「防護(hù)策略」
其實(shí)我們可以想到,黑客只能借助受害者的**cookie**騙取服務(wù)器的信任,但是黑客并不能憑借拿到「cookie」,也看不到 「cookie」的內(nèi)容。另外,對(duì)于服務(wù)器返回的結(jié)果,由于瀏覽器「同源策略」的限制,黑客也無(wú)法進(jìn)行解析。
這就告訴我們,我們要保護(hù)的對(duì)象是那些可以直接產(chǎn)生數(shù)據(jù)改變的服務(wù),而對(duì)于讀取數(shù)據(jù)的服務(wù),則不需要進(jìn)行
**CSRF**的保護(hù)。而保護(hù)的關(guān)鍵,是 「在請(qǐng)求中放入黑客所不能偽造的信息」
用戶(hù)操作限制——驗(yàn)證碼機(jī)制
方法:添加驗(yàn)證碼來(lái)識(shí)別是不是用戶(hù)主動(dòng)去發(fā)起這個(gè)請(qǐng)求,由于一定強(qiáng)度的驗(yàn)證碼機(jī)器無(wú)法識(shí)別,因此危險(xiǎn)網(wǎng)站不能偽造一個(gè)完整的請(qǐng)求。
1. 驗(yàn)證來(lái)源站點(diǎn)
在服務(wù)器端驗(yàn)證請(qǐng)求來(lái)源的站點(diǎn),由于大量的CSRF攻擊來(lái)自第三方站點(diǎn),因此服務(wù)器跨域禁止來(lái)自第三方站點(diǎn)的請(qǐng)求,主要通過(guò)HTTP請(qǐng)求頭中的兩個(gè)Header
Origin Header Referer Header
這兩個(gè)Header在瀏覽器發(fā)起請(qǐng)求時(shí),大多數(shù)情況會(huì)自動(dòng)帶上,并且不能由前端自定義內(nèi)容。
服務(wù)器可以通過(guò)解析這兩個(gè)Header中的域名,確定請(qǐng)求的來(lái)源域。
其中,「Origin」只包含域名信息,而「Referer」包含了具體的 URL 路徑。
在某些情況下,這兩者都是可以偽造的,通過(guò)AJax中自定義請(qǐng)求頭即可,安全性略差。
2. 利用Cookie的SameSite屬性
可以看看MDN對(duì)此的解釋
SameSite可以設(shè)置為三個(gè)值,Strict、Lax和None。
在 Strict模式下,瀏覽器完全禁止第三方請(qǐng)求攜帶Cookie。比如請(qǐng)求sanyuan.com網(wǎng)站只能在sanyuan.com域名當(dāng)中請(qǐng)求才能攜帶 Cookie,在其他網(wǎng)站請(qǐng)求都不能。在 Lax模式,就寬松一點(diǎn)了,但是只能在get 方法提交表單況或者a 標(biāo)簽發(fā)送 get 請(qǐng)求的情況下可以攜帶 Cookie,其他情況均不能。在None模式下,Cookie將在所有上下文中發(fā)送,即允許跨域發(fā)送。
3. 「CSRF Token」
前面講到CSRF的另一個(gè)特征是,攻擊者無(wú)法直接竊取到用戶(hù)的信息(Cookie,Header,網(wǎng)站內(nèi)容等),僅僅是冒用Cookie中的信息。
那么我們可以使用Token,在不涉及XSS的前提下,一般黑客很難拿到Token。
可以看看這篇文章,將了Token是怎么操作的?徹底理解cookie,session,token
Token(令牌)做為Web領(lǐng)域驗(yàn)證身份是一個(gè)不錯(cuò)的選擇,當(dāng)然了,JWT有興趣的也可以去了解一下。
Token步驟如下:
「第一步:將CSRF Token輸出到頁(yè)面中」
首先,用戶(hù)打開(kāi)頁(yè)面的時(shí)候,服務(wù)器需要給這個(gè)用戶(hù)生成一個(gè)Token,該Token通過(guò)加密算法對(duì)數(shù)據(jù)進(jìn)行加密,一般Token都包括隨機(jī)字符串和時(shí)間戳的組合,顯然在提交時(shí)Token不能再放在Cookie中了(XSS可能會(huì)獲取Cookie),否則又會(huì)被攻擊者冒用。因此,為了安全起見(jiàn)Token最好還是存在服務(wù)器的Session中,之后在每次頁(yè)面加載時(shí),使用JS遍歷整個(gè)DOM樹(shù),對(duì)于DOM中所有的a和form標(biāo)簽后加入Token。這樣可以解決大部分的請(qǐng)求,但是對(duì)于在頁(yè)面加載之后動(dòng)態(tài)生成的HTML代碼,這種方法就沒(méi)有作用,還需要程序員在編碼時(shí)手動(dòng)添加Token。
「第二步:頁(yè)面提交的請(qǐng)求攜帶這個(gè)Token」
對(duì)于GET請(qǐng)求,Token將附在請(qǐng)求地址之后,這樣URL 就變成 http://url?csrftoken=tokenvalue。而對(duì)于 POST 請(qǐng)求來(lái)說(shuō),要在 form 的最后加上:
這樣,就把Token以參數(shù)的形式加入請(qǐng)求了。
「第三步:服務(wù)器驗(yàn)證Token是否正確」
當(dāng)用戶(hù)從客戶(hù)端得到了Token,再次提交給服務(wù)器的時(shí)候,服務(wù)器需要判斷Token的有效性,驗(yàn)證過(guò)程是先解密Token,對(duì)比加密字符串以及時(shí)間戳,如果加密字符串一致且時(shí)間未過(guò)期,那么這個(gè)Token就是有效的。
非常感興趣的,可以仔細(xì)去閱讀一下相關(guān)的文章,Token是如何加密的,又是如何保證不被攻擊者獲取道。
總結(jié)
CSRF(Cross-site request forgery), 即跨站請(qǐng)求偽造,本質(zhì)是沖著瀏覽器分不清發(fā)起請(qǐng)求是不是真正的用戶(hù)本人,所以防范的關(guān)鍵在于在請(qǐng)求中放入黑客所不能偽造的信息。從而防止黑客偽造一個(gè)完整的請(qǐng)求欺騙服務(wù)器。
「防范措施」:驗(yàn)證碼機(jī)制,驗(yàn)證來(lái)源站點(diǎn),利用Cookie的SameSite屬性,CSRF Token
參考
還在看那些老掉牙的性能優(yōu)化文章么?這些最新性能指標(biāo)了解下 原來(lái) CSS 與 JS 是這樣阻塞 DOM 解析和渲染的 從瀏覽器多進(jìn)程到JS單線(xiàn)程,JS運(yùn)行機(jī)制最全面的一次梳理 實(shí)現(xiàn)圖片懶加載的幾種方案比較 九種跨域方式實(shí)現(xiàn)原理(完整版) 極客時(shí)間專(zhuān)欄 還分不清 Cookie、Session、Token、JWT?
最后
如果你覺(jué)得這篇內(nèi)容對(duì)你挺有啟發(fā),我想邀請(qǐng)你幫我三個(gè)小忙:
點(diǎn)個(gè)「在看」,讓更多的人也能看到這篇內(nèi)容(喜歡不點(diǎn)在看,都是耍流氓 -_-)
歡迎加我微信「qianyu443033099」拉你進(jìn)技術(shù)群,長(zhǎng)期交流學(xué)習(xí)...
關(guān)注公眾號(hào)「前端下午茶」,持續(xù)為你推送精選好文,也可以加我為好友,隨時(shí)聊騷。

