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

