1. 瀏覽器渲染機(jī)制
- 瀏覽器采用流式布局模型(
Flow Based Layout
) - 瀏覽器會(huì)把
HTML
解析成DOM
乘盖,把CSS
解析成CSSOM
,DOM
和CSSOM
合并就產(chǎn)生了渲染樹(Render Tree
)。 - 有了
RenderTree
,我們就知道了所有節(jié)點(diǎn)的樣式,然后計(jì)算他們?cè)陧?yè)面上的大小和位置摇锋,最后把節(jié)點(diǎn)繪制到頁(yè)面上。 - 由于瀏覽器使用流式布局站超,對(duì)
Render Tree
的計(jì)算通常只需要遍歷一次就可以完成荸恕,但table
及其內(nèi)部元素除外,他們可能需要多次計(jì)算死相,通常要花3倍于同等元素的時(shí)間融求,這也是為什么要避免使用table
布局的原因之一。
2. 重繪
由于節(jié)點(diǎn)的幾何屬性發(fā)生改變或者由于樣式發(fā)生改變而不會(huì)影響布局的算撮,稱為重繪生宛,例如outline
, visibility
, color
、background-color
等肮柜,重繪的代價(jià)陷舅,因?yàn)闉g覽器必須驗(yàn)證DOM樹上其他節(jié)點(diǎn)元素的可見性。
3. 回流
回流是布局或者幾何屬性需要改變就稱為回流审洞±痴觯回流是影響瀏覽器性能的關(guān)鍵因素,因?yàn)槠渥兓婕暗讲糠猪?yè)面(或是整個(gè)頁(yè)面)的布局更新。一個(gè)元素的回流可能會(huì)導(dǎo)致了其所有子元素以及DOM中緊隨其后的節(jié)點(diǎn)仰剿、祖先節(jié)點(diǎn)元素的隨后的回流耙箍。
<body>
<div class="error">
<h4>我的組件</h4>
<p><strong>錯(cuò)誤:</strong>錯(cuò)誤的描述…</p>
<h5>錯(cuò)誤糾正</h5>
<ol>
<li>第一步</li>
<li>第二步</li>
</ol>
</div>
</body>
在上面的HTML片段中,對(duì)該段落(標(biāo)簽)回流將會(huì)引發(fā)強(qiáng)烈的回流酥馍,因?yàn)樗且粋€(gè)子節(jié)點(diǎn)。這也導(dǎo)致了祖先的回流(`div.error`和`body` – 視瀏覽器而定)阅酪。此外旨袒,
和``也會(huì)有簡(jiǎn)單的回流,因?yàn)槠湓贒OM中在回流元素之后术辐。大部分的回流將導(dǎo)致頁(yè)面的重新渲染砚尽。
回流必定會(huì)發(fā)生重繪,重繪不一定會(huì)引發(fā)回流辉词。
4. 瀏覽器優(yōu)化
現(xiàn)代瀏覽器大多都是通過隊(duì)列機(jī)制來批量更新布局必孤,瀏覽器會(huì)把修改操作放在隊(duì)列中,至少一個(gè)瀏覽器刷新(即16.6ms)才會(huì)清空隊(duì)列瑞躺,但當(dāng)你獲取布局信息的時(shí)候敷搪,隊(duì)列中可能有會(huì)影響這些屬性或方法返回值的操作,即使沒有幢哨,瀏覽器也會(huì)強(qiáng)制清空隊(duì)列赡勘,觸發(fā)回流與重繪來確保返回正確的值。
主要包括以下屬性或方法:
-
offsetTop
捞镰、offsetLeft
闸与、offsetWidth
、offsetHeight
-
scrollTop
岸售、scrollLeft
践樱、scrollWidth
、scrollHeight
-
clientTop
凸丸、clientLeft
拷邢、clientWidth
、clientHeight
-
width
甲雅、height
getComputedStyle()
getBoundingClientRect()
所以解孙,我們應(yīng)該避免頻繁的使用上述的屬性,他們都會(huì)強(qiáng)制渲染刷新隊(duì)列抛人。
5. 減少重繪與回流
-
CSS
使用
transform
替代top
使用
visibility
替換display: none
弛姜,因?yàn)榍罢咧粫?huì)引起重繪,后者會(huì)引發(fā)回流(改變了布局避免使用
table
布局妖枚,可能很小的一個(gè)小改動(dòng)會(huì)造成整個(gè)table
的重新布局廷臼。盡可能在
DOM
樹的最末端改變class
,回流是不可避免的,但可以減少其影響荠商。盡可能在DOM樹的最末端改變class寂恬,可以限制了回流的范圍,使其影響盡可能少的節(jié)點(diǎn)莱没。-
避免設(shè)置多層內(nèi)聯(lián)樣式初肉,CSS 選擇符從右往左匹配查找,避免節(jié)點(diǎn)層級(jí)過多饰躲。
<div> <a> <span></span> </a> </div> <style> span { color: red; } div > a > span { color: red; } </style>
對(duì)于第一種設(shè)置樣式的方式來說牙咏,瀏覽器只需要找到頁(yè)面中所有的
span
標(biāo)簽然后設(shè)置顏色,但是對(duì)于第二種設(shè)置樣式的方式來說嘹裂,瀏覽器首先需要找到所有的span
標(biāo)簽妄壶,然后找到span
標(biāo)簽上的a
標(biāo)簽,最后再去找到div
標(biāo)簽寄狼,然后給符合這種條件的span
標(biāo)簽設(shè)置顏色丁寄,這樣的遞歸過程就很復(fù)雜。所以我們應(yīng)該盡可能的避免寫過于具體的 CSS 選擇器泊愧,然后對(duì)于 HTML 來說也盡量少的添加無意義標(biāo)簽伊磺,保證層級(jí)扁平。 將動(dòng)畫效果應(yīng)用到
position
屬性為absolute
或fixed
的元素上拼卵,避免影響其他元素的布局奢浑,這樣只是一個(gè)重繪,而不是回流腋腮,同時(shí)雀彼,控制動(dòng)畫速度可以選擇requestAnimationFrame
,詳見探討 requestAnimationFrame即寡。避免使用
CSS
表達(dá)式徊哑,可能會(huì)引發(fā)回流。將頻繁重繪或者回流的節(jié)點(diǎn)設(shè)置為圖層聪富,圖層能夠阻止該節(jié)點(diǎn)的渲染行為影響別的節(jié)點(diǎn)莺丑,例如
will-change
、video
墩蔓、iframe
等標(biāo)簽梢莽,瀏覽器會(huì)自動(dòng)將該節(jié)點(diǎn)變?yōu)閳D層。CSS3 硬件加速(GPU加速)奸披,使用css3硬件加速昏名,可以讓
transform
、opacity
阵面、filters
這些動(dòng)畫不會(huì)引起回流重繪 轻局。但是對(duì)于動(dòng)畫的其它屬性洪鸭,比如background-color
這些,還是會(huì)引起回流重繪的仑扑,不過它還是可以提升這些動(dòng)畫的性能览爵。
-
JavaScript
-
避免頻繁操作樣式,最好一次性重寫
style
屬性镇饮,或者將樣式列表定義為class
并一次性更改class
屬性蜓竹。 -
避免頻繁操作
DOM
,創(chuàng)建一個(gè)documentFragment
储藐,在它上面應(yīng)用所有DOM操作
梅肤,最后再把它添加到文檔中。 - 避免頻繁讀取會(huì)引發(fā)回流/重繪的屬性邑茄,如果確實(shí)需要多次使用,就用一個(gè)變量緩存起來俊啼。
- 對(duì)具有復(fù)雜動(dòng)畫的元素使用絕對(duì)定位肺缕,使它脫離文檔流,否則會(huì)引起父元素及后續(xù)元素頻繁回流授帕。
-
避免頻繁操作樣式,最好一次性重寫
瀏覽器與Node的事件循環(huán)有何區(qū)別?
(Event Loop)
瀏覽器
關(guān)于微任務(wù)和宏任務(wù)在瀏覽器的執(zhí)行順序是這樣的:
- 執(zhí)行一只task(宏任務(wù))
- 執(zhí)行完micro-task隊(duì)列 (微任務(wù))
如此循環(huán)往復(fù)下去
瀏覽器的task(宏任務(wù))執(zhí)行順序在 html#event-loops 里面有講就不翻譯了 常見的 task(宏任務(wù)) 比如:setTimeout同木、setInterval、script(整體代碼)跛十、 I/O 操作彤路、UI 渲染等。 常見的 micro-task 比如: new Promise().then(回調(diào))芥映、MutationObserver(html5新特性) 等洲尊。
Node
Node的事件循環(huán)是libuv實(shí)現(xiàn)的,引用一張官網(wǎng)的圖:
大體的task(宏任務(wù))執(zhí)行順序是這樣的:
- timers定時(shí)器:本階段執(zhí)行已經(jīng)安排的 setTimeout() 和 setInterval() 的回調(diào)函數(shù)奈偏。
- pending callbacks待定回調(diào):執(zhí)行延遲到下一個(gè)循環(huán)迭代的 I/O 回調(diào)坞嘀。
- idle, prepare:僅系統(tǒng)內(nèi)部使用。
- poll 輪詢:檢索新的 I/O 事件;執(zhí)行與 I/O 相關(guān)的回調(diào)(幾乎所有情況下惊来,除了關(guān)閉的回調(diào)函數(shù)丽涩,它們由計(jì)時(shí)器和 setImmediate() 排定的之外),其余情況 node 將在此處阻塞裁蚁。
- check 檢測(cè):setImmediate() 回調(diào)函數(shù)在這里執(zhí)行矢渊。
- close callbacks 關(guān)閉的回調(diào)函數(shù):一些準(zhǔn)備關(guān)閉的回調(diào)函數(shù),如:socket.on('close', ...)枉证。
微任務(wù)和宏任務(wù)在Node的執(zhí)行順序
Node 10以前:
- 執(zhí)行完一個(gè)階段的所有任務(wù)
- 執(zhí)行完nextTick隊(duì)列里面的內(nèi)容
- 然后執(zhí)行完微任務(wù)隊(duì)列的內(nèi)容
Node 11以后: 和瀏覽器的行為統(tǒng)一了矮男,都是每執(zhí)行一個(gè)宏任務(wù)就執(zhí)行完微任務(wù)隊(duì)列。
一刽严、線程與進(jìn)程
1.概念
我們經(jīng)常說JS 是單線程執(zhí)行的昂灵,指的是一個(gè)進(jìn)程里只有一個(gè)主線程避凝,那到底什么是線程?什么是進(jìn)程眨补?
官方的說法是:進(jìn)程是 CPU資源分配的最小單位管削;線程是 CPU調(diào)度的最小單位。這兩句話并不好理解撑螺,我們先來看張圖:
- 進(jìn)程好比圖中的工廠含思,有單獨(dú)的專屬自己的工廠資源。
- 線程好比圖中的工人甘晤,多個(gè)工人在一個(gè)工廠中協(xié)作工作含潘,工廠與工人是 1:n的關(guān)系。也就是說一個(gè)進(jìn)程由一個(gè)或多個(gè)線程組成线婚,線程是一個(gè)進(jìn)程中代碼的不同執(zhí)行路線遏弱;
- 工廠的空間是工人們共享的,這象征一個(gè)進(jìn)程的內(nèi)存空間是共享的塞弊,每個(gè)線程都可用這些共享內(nèi)存漱逸。
- 多個(gè)工廠之間獨(dú)立存在。
2.多進(jìn)程與多線程
- 多進(jìn)程:在同一個(gè)時(shí)間里游沿,同一個(gè)計(jì)算機(jī)系統(tǒng)中如果允許兩個(gè)或兩個(gè)以上的進(jìn)程處于運(yùn)行狀態(tài)叭首。多進(jìn)程帶來的好處是明顯的第步,比如你可以聽歌的同時(shí),打開編輯器敲代碼,編輯器和聽歌軟件的進(jìn)程之間絲毫不會(huì)相互干擾蛤虐。
- 多線程:程序中包含多個(gè)執(zhí)行流曲饱,即在一個(gè)程序中可以同時(shí)運(yùn)行多個(gè)不同的線程來執(zhí)行不同的任務(wù)湃番,也就是說允許單個(gè)程序創(chuàng)建多個(gè)并行執(zhí)行的線程來完成各自的任務(wù)褪猛。
以Chrome瀏覽器中為例,當(dāng)你打開一個(gè) Tab 頁(yè)時(shí)吃环,其實(shí)就是創(chuàng)建了一個(gè)進(jìn)程镶柱,一個(gè)進(jìn)程中可以有多個(gè)線程(下文會(huì)詳細(xì)介紹),比如渲染線程模叙、JS 引擎線程歇拆、HTTP 請(qǐng)求線程等等。當(dāng)你發(fā)起一個(gè)請(qǐng)求時(shí)范咨,其實(shí)就是創(chuàng)建了一個(gè)線程故觅,當(dāng)請(qǐng)求結(jié)束后,該線程可能就會(huì)被銷毀渠啊。
二输吏、瀏覽器內(nèi)核
簡(jiǎn)單來說瀏覽器內(nèi)核是通過取得頁(yè)面內(nèi)容、整理信息(應(yīng)用CSS)替蛉、計(jì)算和組合最終輸出可視化的圖像結(jié)果贯溅,通常也被稱為渲染引擎拄氯。
瀏覽器內(nèi)核是多線程,在內(nèi)核控制下各線程相互配合以保持同步它浅,一個(gè)瀏覽器通常由以下常駐線程組成:
- GUI 渲染線程
- JavaScript引擎線程
- 定時(shí)觸發(fā)器線程
- 事件觸發(fā)線程
- 異步http請(qǐng)求線程
1.GUI渲染線程
- 主要負(fù)責(zé)頁(yè)面的渲染译柏,解析HTML、CSS姐霍,構(gòu)建DOM樹鄙麦,布局和繪制等。
- 當(dāng)界面需要重繪或者由于某種操作引發(fā)回流時(shí)镊折,將執(zhí)行該線程胯府。
- 該線程與JS引擎線程互斥,當(dāng)執(zhí)行JS引擎線程時(shí)恨胚,GUI渲染會(huì)被掛起骂因,當(dāng)任務(wù)隊(duì)列空閑時(shí),主線程才會(huì)去執(zhí)行GUI渲染赃泡。
2.JS引擎線程
- 該線程當(dāng)然是主要負(fù)責(zé)處理 JavaScript腳本侣签,執(zhí)行代碼。
- 也是主要負(fù)責(zé)執(zhí)行準(zhǔn)備好待執(zhí)行的事件急迂,即定時(shí)器計(jì)數(shù)結(jié)束,或者異步請(qǐng)求成功并正確返回時(shí)蹦肴,將依次進(jìn)入任務(wù)隊(duì)列僚碎,等待 JS引擎線程的執(zhí)行。
- 當(dāng)然阴幌,該線程與 GUI渲染線程互斥勺阐,當(dāng) JS引擎線程執(zhí)行 JavaScript腳本時(shí)間過長(zhǎng),將導(dǎo)致頁(yè)面渲染的阻塞矛双。
3.定時(shí)器觸發(fā)線程
- 負(fù)責(zé)執(zhí)行異步定時(shí)器一類的函數(shù)的線程渊抽,如: setTimeout,setInterval议忽。
- 主線程依次執(zhí)行代碼時(shí)懒闷,遇到定時(shí)器,會(huì)將定時(shí)器交給該線程處理栈幸,當(dāng)計(jì)數(shù)完畢后愤估,事件觸發(fā)線程會(huì)將計(jì)數(shù)完畢后的事件加入到任務(wù)隊(duì)列的尾部,等待JS引擎線程執(zhí)行速址。
4.事件觸發(fā)線程
- 主要負(fù)責(zé)將準(zhǔn)備好的事件交給 JS引擎線程執(zhí)行玩焰。
比如 setTimeout定時(shí)器計(jì)數(shù)結(jié)束, ajax等異步請(qǐng)求成功并觸發(fā)回調(diào)函數(shù)芍锚,或者用戶觸發(fā)點(diǎn)擊事件時(shí)昔园,該線程會(huì)將整裝待發(fā)的事件依次加入到任務(wù)隊(duì)列的隊(duì)尾蔓榄,等待 JS引擎線程的執(zhí)行。
5.異步http請(qǐng)求線程
- 負(fù)責(zé)執(zhí)行異步請(qǐng)求一類的函數(shù)的線程默刚,如: Promise甥郑,axios,ajax等羡棵。
- 主線程依次執(zhí)行代碼時(shí)壹若,遇到異步請(qǐng)求,會(huì)將函數(shù)交給該線程處理皂冰,當(dāng)監(jiān)聽到狀態(tài)碼變更店展,如果有回調(diào)函數(shù),事件觸發(fā)線程會(huì)將回調(diào)函數(shù)加入到任務(wù)隊(duì)列的尾部秃流,等待JS引擎線程執(zhí)行赂蕴。
三、瀏覽器中的 Event Loop
1.Micro-Task 與 Macro-Task
瀏覽器端事件循環(huán)中的異步隊(duì)列有兩種:macro(宏任務(wù))隊(duì)列和 micro(微任務(wù))隊(duì)列舶胀。
- 常見的 macro-task 比如:setTimeout概说、setInterval、script(整體代碼)嚣伐、 I/O 操作糖赔、UI 渲染等。
- 常見的 micro-task 比如: new Promise().then(回調(diào))轩端、MutationObserver(html5新特性) 等放典。
2.Event Loop 過程解析
一個(gè)完整的 Event Loop 過程,可以概括為以下階段:
- 一開始執(zhí)行椈穑空,我們可以把執(zhí)行棧認(rèn)為是一個(gè)存儲(chǔ)函數(shù)調(diào)用的棧結(jié)構(gòu)奋构,遵循先進(jìn)后出的原則。micro 隊(duì)列空拱层,macro 隊(duì)列里有且只有一個(gè) script 腳本(整體代碼)弥臼。
- 全局上下文(script 標(biāo)簽)被推入執(zhí)行棧,同步代碼執(zhí)行根灯。在執(zhí)行的過程中径缅,會(huì)判斷是同步任務(wù)還是異步任務(wù),通過對(duì)一些接口的調(diào)用烙肺,可以產(chǎn)生新的 macro-task 與 micro-task芥驳,它們會(huì)分別被推入各自的任務(wù)隊(duì)列里。同步代碼執(zhí)行完了茬高,script 腳本會(huì)被移出 macro 隊(duì)列兆旬,這個(gè)過程本質(zhì)上是隊(duì)列的 macro-task 的執(zhí)行和出隊(duì)的過程。
- 上一步我們出隊(duì)的是一個(gè) macro-task怎栽,這一步我們處理的是 micro-task丽猬。但需要注意的是:當(dāng) macro-task 出隊(duì)時(shí)宿饱,任務(wù)是一個(gè)一個(gè)執(zhí)行的;而 micro-task 出隊(duì)時(shí)脚祟,任務(wù)是一隊(duì)一隊(duì)執(zhí)行的谬以。因此,我們處理 micro 隊(duì)列這一步由桌,會(huì)逐個(gè)執(zhí)行隊(duì)列中的任務(wù)并把它出隊(duì)为黎,直到隊(duì)列被清空。
- 執(zhí)行渲染操作行您,更新界面
- 檢查是否存在 Web worker 任務(wù)铭乾,如果有,則對(duì)其進(jìn)行處理
- 上述過程循環(huán)往復(fù)娃循,直到兩個(gè)隊(duì)列都清空
我們總結(jié)一下炕檩,每一次循環(huán)都是一個(gè)這樣的過程:
當(dāng)某個(gè)宏任務(wù)執(zhí)行完后,會(huì)查看是否有微任務(wù)隊(duì)列。如果有捌斧,先執(zhí)行微任務(wù)隊(duì)列中的所有任務(wù)笛质,如果沒有,會(huì)讀取宏任務(wù)隊(duì)列中排在最前的任務(wù)捞蚂,執(zhí)行宏任務(wù)的過程中妇押,遇到微任務(wù),依次加入微任務(wù)隊(duì)列姓迅。椙没簦空后,再次讀取微任務(wù)隊(duì)列里的任務(wù)队贱,依次類推。
接下來我們看道例子來介紹上面流程:
Promise.resolve().then(()=>{
console.log('Promise1')
setTimeout(()=>{
console.log('setTimeout2')
},0)
})
setTimeout(()=>{
console.log('setTimeout1')
Promise.resolve().then(()=>{
console.log('Promise2')
})
},0)
復(fù)制代碼
最后輸出結(jié)果是Promise1潭袱,setTimeout1柱嫌,Promise2,setTimeout2
- 一開始執(zhí)行棧的同步任務(wù)(這屬于宏任務(wù))執(zhí)行完畢屯换,會(huì)去查看是否有微任務(wù)隊(duì)列编丘,上題中存在(有且只有一個(gè)),然后執(zhí)行微任務(wù)隊(duì)列中的所有任務(wù)輸出Promise1彤悔,同時(shí)會(huì)生成一個(gè)宏任務(wù) setTimeout2
- 然后去查看宏任務(wù)隊(duì)列嘉抓,宏任務(wù) setTimeout1 在 setTimeout2 之前,先執(zhí)行宏任務(wù) setTimeout1晕窑,輸出 setTimeout1
- 在執(zhí)行宏任務(wù)setTimeout1時(shí)會(huì)生成微任務(wù)Promise2 抑片,放入微任務(wù)隊(duì)列中,接著先去清空微任務(wù)隊(duì)列中的所有任務(wù)杨赤,輸出 Promise2
- 清空完微任務(wù)隊(duì)列中的所有任務(wù)后敞斋,就又會(huì)去宏任務(wù)隊(duì)列取一個(gè)截汪,這回執(zhí)行的是 setTimeout2
四、Node 中的 Event Loop
1.Node簡(jiǎn)介
Node 中的 Event Loop 和瀏覽器中的是完全不相同的東西植捎。Node.js采用V8作為js的解析引擎衙解,而I/O處理方面使用了自己設(shè)計(jì)的libuv,libuv是一個(gè)基于事件驅(qū)動(dòng)的跨平臺(tái)抽象層焰枢,封裝了不同操作系統(tǒng)一些底層特性蚓峦,對(duì)外提供統(tǒng)一的API,事件循環(huán)機(jī)制也是它里面的實(shí)現(xiàn)(下文會(huì)詳細(xì)介紹)济锄。
Node.js的運(yùn)行機(jī)制如下:
- V8引擎解析JavaScript腳本暑椰。
- 解析后的代碼,調(diào)用Node API拟淮。
- libuv庫(kù)負(fù)責(zé)Node API的執(zhí)行干茉。它將不同的任務(wù)分配給不同的線程,形成一個(gè)Event Loop(事件循環(huán))很泊,以異步的方式將任務(wù)的執(zhí)行結(jié)果返回給V8引擎角虫。
- V8引擎再將結(jié)果返回給用戶。
2.六個(gè)階段
其中l(wèi)ibuv引擎中的事件循環(huán)分為 6 個(gè)階段委造,它們會(huì)按照順序反復(fù)運(yùn)行戳鹅。每當(dāng)進(jìn)入某一個(gè)階段的時(shí)候,都會(huì)從對(duì)應(yīng)的回調(diào)隊(duì)列中取出函數(shù)去執(zhí)行昏兆。當(dāng)隊(duì)列為空或者執(zhí)行的回調(diào)函數(shù)數(shù)量到達(dá)系統(tǒng)設(shè)定的閾值枫虏,就會(huì)進(jìn)入下一階段。
從上圖中爬虱,大致看出node中的事件循環(huán)的順序:
外部輸入數(shù)據(jù)-->輪詢階段(poll)-->檢查階段(check)-->關(guān)閉事件回調(diào)階段(close callback)-->定時(shí)器檢測(cè)階段(timer)-->I/O事件回調(diào)階段(I/O callbacks)-->閑置階段(idle, prepare)-->輪詢階段(按照該順序反復(fù)運(yùn)行)...
- timers 階段:這個(gè)階段執(zhí)行timer(setTimeout隶债、setInterval)的回調(diào)
- I/O callbacks 階段:處理一些上一輪循環(huán)中的少數(shù)未執(zhí)行的 I/O 回調(diào)
- idle, prepare 階段:僅node內(nèi)部使用
- poll 階段:獲取新的I/O事件, 適當(dāng)?shù)臈l件下node將阻塞在這里
- check 階段:執(zhí)行 setImmediate() 的回調(diào)
- close callbacks 階段:執(zhí)行 socket 的 close 事件回調(diào)
注意:上面六個(gè)階段都不包括 process.nextTick()(下文會(huì)介紹)
接下去我們?cè)敿?xì)介紹timers
、poll
跑筝、check
這3個(gè)階段死讹,因?yàn)槿粘i_發(fā)中的絕大部分異步任務(wù)都是在這3個(gè)階段處理的。
(1) timer
timers 階段會(huì)執(zhí)行 setTimeout 和 setInterval 回調(diào)曲梗,并且是由 poll 階段控制的赞警。 同樣,在 Node 中定時(shí)器指定的時(shí)間也不是準(zhǔn)確時(shí)間虏两,只能是盡快執(zhí)行愧旦。
(2) poll
poll 是一個(gè)至關(guān)重要的階段,這一階段中定罢,系統(tǒng)會(huì)做兩件事情
1.回到 timer 階段執(zhí)行回調(diào)
2.執(zhí)行 I/O 回調(diào)
并且在進(jìn)入該階段時(shí)如果沒有設(shè)定了 timer 的話笤虫,會(huì)發(fā)生以下兩件事情
- 如果 poll 隊(duì)列不為空,會(huì)遍歷回調(diào)隊(duì)列并同步執(zhí)行,直到隊(duì)列為空或者達(dá)到系統(tǒng)限制
- 如果 poll 隊(duì)列為空時(shí)耕皮,會(huì)有兩件事發(fā)生
- 如果有 setImmediate 回調(diào)需要執(zhí)行境蜕,poll 階段會(huì)停止并且進(jìn)入到 check 階段執(zhí)行回調(diào)
- 如果沒有 setImmediate 回調(diào)需要執(zhí)行,會(huì)等待回調(diào)被加入到隊(duì)列中并立即執(zhí)行回調(diào)凌停,這里同樣會(huì)有個(gè)超時(shí)時(shí)間設(shè)置防止一直等待下去
當(dāng)然設(shè)定了 timer 的話且 poll 隊(duì)列為空粱年,則會(huì)判斷是否有 timer 超時(shí),如果有的話會(huì)回到 timer 階段執(zhí)行回調(diào)罚拟。
(3) check階段
setImmediate()的回調(diào)會(huì)被加入check隊(duì)列中台诗,從event loop的階段圖可以知道,check階段的執(zhí)行順序在poll階段之后赐俗。 我們先來看個(gè)例子:
console.log('start')
setTimeout(() => {
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
setTimeout(() => {
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
})
}, 0)
Promise.resolve().then(function() {
console.log('promise3')
})
console.log('end')
//start=>end=>promise3=>timer1=>timer2=>promise1=>promise2
復(fù)制代碼
- 一開始執(zhí)行棧的同步任務(wù)(這屬于宏任務(wù))執(zhí)行完畢后(依次打印出start end拉队,并將2個(gè)timer依次放入timer隊(duì)列),會(huì)先去執(zhí)行微任務(wù)(這點(diǎn)跟瀏覽器端的一樣),所以打印出promise3
- 然后進(jìn)入timers階段阻逮,執(zhí)行timer1的回調(diào)函數(shù)粱快,打印timer1,并將promise.then回調(diào)放入microtask隊(duì)列叔扼,同樣的步驟執(zhí)行timer2事哭,打印timer2;這點(diǎn)跟瀏覽器端相差比較大瓜富,timers階段有幾個(gè)setTimeout/setInterval都會(huì)依次執(zhí)行鳍咱,并不像瀏覽器端,每執(zhí)行一個(gè)宏任務(wù)后就去執(zhí)行一個(gè)微任務(wù)(關(guān)于Node與瀏覽器的 Event Loop 差異与柑,下文還會(huì)詳細(xì)介紹)谤辜。
3.Micro-Task 與 Macro-Task
Node端事件循環(huán)中的異步隊(duì)列也是這兩種:macro(宏任務(wù))隊(duì)列和 micro(微任務(wù))隊(duì)列。
- 常見的 macro-task 比如:setTimeout价捧、setInterval丑念、 setImmediate、script(整體代碼)结蟋、 I/O 操作等脯倚。
- 常見的 micro-task 比如: process.nextTick、new Promise().then(回調(diào))等椎眯。
4.注意點(diǎn)
(1) setTimeout 和 setImmediate
二者非常相似挠将,區(qū)別主要在于調(diào)用時(shí)機(jī)不同胳岂。
- setImmediate 設(shè)計(jì)在poll階段完成時(shí)執(zhí)行编整,即check階段;
- setTimeout 設(shè)計(jì)在poll階段為空閑時(shí)乳丰,且設(shè)定時(shí)間到達(dá)后執(zhí)行掌测,但它在timer階段執(zhí)行
setTimeout(function timeout () {
console.log('timeout');
},0);
setImmediate(function immediate () {
console.log('immediate');
});
復(fù)制代碼
- 對(duì)于以上代碼來說,setTimeout 可能執(zhí)行在前,也可能執(zhí)行在后汞斧。
- 首先 setTimeout(fn, 0) === setTimeout(fn, 1)夜郁,這是由源碼決定的 進(jìn)入事件循環(huán)也是需要成本的,如果在準(zhǔn)備時(shí)候花費(fèi)了大于 1ms 的時(shí)間粘勒,那么在 timer 階段就會(huì)直接執(zhí)行 setTimeout 回調(diào)
- 如果準(zhǔn)備時(shí)間花費(fèi)小于 1ms竞端,那么就是 setImmediate 回調(diào)先執(zhí)行了
但當(dāng)二者在異步i/o callback內(nèi)部調(diào)用時(shí),總是先執(zhí)行setImmediate庙睡,再執(zhí)行setTimeout
const fs = require('fs')
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0)
setImmediate(() => {
console.log('immediate')
})
})
// immediate
// timeout
復(fù)制代碼
在上述代碼中事富,setImmediate 永遠(yuǎn)先執(zhí)行。因?yàn)閮蓚€(gè)代碼寫在 IO 回調(diào)中乘陪,IO 回調(diào)是在 poll 階段執(zhí)行统台,當(dāng)回調(diào)執(zhí)行完畢后隊(duì)列為空,發(fā)現(xiàn)存在 setImmediate 回調(diào)啡邑,所以就直接跳轉(zhuǎn)到 check 階段去執(zhí)行回調(diào)了贱勃。
(2) process.nextTick
這個(gè)函數(shù)其實(shí)是獨(dú)立于 Event Loop 之外的,它有一個(gè)自己的隊(duì)列谤逼,當(dāng)每個(gè)階段完成后贵扰,如果存在 nextTick 隊(duì)列,就會(huì)清空隊(duì)列中的所有回調(diào)函數(shù)森缠,并且優(yōu)先于其他 microtask 執(zhí)行拔鹰。
setTimeout(() => {
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
process.nextTick(() => {
console.log('nextTick')
process.nextTick(() => {
console.log('nextTick')
process.nextTick(() => {
console.log('nextTick')
process.nextTick(() => {
console.log('nextTick')
})
})
})
})
// nextTick=>nextTick=>nextTick=>nextTick=>timer1=>promise1
復(fù)制代碼
五、Node與瀏覽器的 Event Loop 差異
瀏覽器環(huán)境下贵涵,microtask的任務(wù)隊(duì)列是每個(gè)macrotask執(zhí)行完之后執(zhí)行列肢。而在Node.js中,microtask會(huì)在事件循環(huán)的各個(gè)階段之間執(zhí)行宾茂,也就是一個(gè)階段執(zhí)行完畢瓷马,就會(huì)去執(zhí)行microtask隊(duì)列的任務(wù)。
接下我們通過一個(gè)例子來說明兩者區(qū)別:
setTimeout(()=>{
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
setTimeout(()=>{
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
})
}, 0)
復(fù)制代碼
瀏覽器端運(yùn)行結(jié)果:timer1=>promise1=>timer2=>promise2
瀏覽器端的處理過程如下:
Node端運(yùn)行結(jié)果分兩種情況:
- 如果是node11版本一旦執(zhí)行一個(gè)階段里的一個(gè)宏任務(wù)(setTimeout,setInterval和setImmediate)就立刻執(zhí)行微任務(wù)隊(duì)列跨晴,這就跟瀏覽器端運(yùn)行一致欧聘,最后的結(jié)果為
timer1=>promise1=>timer2=>promise2
- 如果是node10及其之前版本:要看第一個(gè)定時(shí)器執(zhí)行完,第二個(gè)定時(shí)器是否在完成隊(duì)列中端盆。
- 如果是第二個(gè)定時(shí)器還未在完成隊(duì)列中腌紧,最后的結(jié)果為
timer1=>promise1=>timer2=>promise2
- 如果是第二個(gè)定時(shí)器已經(jīng)在完成隊(duì)列中,則最后的結(jié)果為
timer1=>timer2=>promise1=>promise2
(下文過程解釋基于這種情況下)
- 如果是第二個(gè)定時(shí)器還未在完成隊(duì)列中腌紧,最后的結(jié)果為
1.全局腳本(main())執(zhí)行星压,將2個(gè)timer依次放入timer隊(duì)列沃斤,main()執(zhí)行完畢,調(diào)用椃偃担空閑痕届,任務(wù)隊(duì)列開始執(zhí)行;
2.首先進(jìn)入timers階段,執(zhí)行timer1的回調(diào)函數(shù)研叫,打印timer1锤窑,并將promise1.then回調(diào)放入microtask隊(duì)列,同樣的步驟執(zhí)行timer2嚷炉,打印timer2渊啰;
3.至此,timer階段執(zhí)行結(jié)束申屹,event loop進(jìn)入下一個(gè)階段之前虽抄,執(zhí)行microtask隊(duì)列的所有任務(wù),依次打印promise1独柑、promise2
Node端的處理過程如下:
六迈窟、總結(jié)
瀏覽器和Node 環(huán)境下,microtask 任務(wù)隊(duì)列的執(zhí)行時(shí)機(jī)不同
- Node端忌栅,microtask 在事件循環(huán)的各個(gè)階段之間執(zhí)行
- 瀏覽器端车酣,microtask 在事件循環(huán)的 macrotask 執(zhí)行完之后執(zhí)行
搞懂前端緩存
總的來說:
- 如果開啟了Service Worker首先會(huì)從Service Worker中拿
- 如果新開一個(gè)以前打開過的頁(yè)面緩存會(huì)從Disk Cache中拿(前提是命中強(qiáng)緩存)
- 刷新當(dāng)前頁(yè)面時(shí)瀏覽器會(huì)根據(jù)當(dāng)前運(yùn)行環(huán)境內(nèi)存來決定是從 Memory Cache 還是 從Disk Cache中拿(可以看到下圖最后幾個(gè)文件有時(shí)候是從 Memory Cache中拿有時(shí)候是從Disk Cache中拿)
注意:以上回答全部基于chrome瀏覽器
搞懂前端緩存
前端緩存/后端緩存
扯了些沒用的,我們先進(jìn)入定義環(huán)節(jié):什么是前端緩存索绪?與之相對(duì)的什么又是后端緩存湖员?
基本的網(wǎng)絡(luò)請(qǐng)求就是三個(gè)步驟:請(qǐng)求,處理瑞驱,響應(yīng)娘摔。
后端緩存主要集中于“處理”步驟,通過保留數(shù)據(jù)庫(kù)連接唤反,存儲(chǔ)處理結(jié)果等方式縮短處理時(shí)間凳寺,盡快進(jìn)入“響應(yīng)”步驟。當(dāng)然這不在本文的討論范圍之內(nèi)彤侍。
而前端緩存則可以在剩下的兩步:“請(qǐng)求”和“響應(yīng)”中進(jìn)行肠缨。在“請(qǐng)求”步驟中,瀏覽器也可以通過存儲(chǔ)結(jié)果的方式直接使用資源盏阶,直接省去了發(fā)送請(qǐng)求晒奕;而“響應(yīng)”步驟需要瀏覽器和服務(wù)器共同配合,通過減少響應(yīng)內(nèi)容來縮短傳輸時(shí)間名斟。這些都會(huì)在下面進(jìn)行討論脑慧。
本文主要包含
- 按緩存位置分類 (memory cache, disk cache, Service Worker 等)
- 按失效策略分類 (
Cache-Control
,ETag
等) - 幫助理解原理的一些案例
- 緩存的應(yīng)用模式
按緩存位置分類
我看過的大部分討論緩存的文章會(huì)直接從 HTTP 協(xié)議頭中的緩存字段開始,例如 Cache-Control
, ETag
, max-age
等砰盐。但偶爾也會(huì)聽到別人討論 memory cache, disk cache 等闷袒。那這兩種分類體系究竟有何關(guān)聯(lián)?是否有交叉楞卡?(我個(gè)人認(rèn)為這是本文的最大價(jià)值所在霜运,因?yàn)樵趯懼拔易约阂彩潜粌煞N分類體系搞的一團(tuán)糟)
實(shí)際上,HTTP 協(xié)議頭的那些字段蒋腮,都屬于 disk cache 的范疇淘捡,是幾個(gè)緩存位置的其中之一。因此本著從全局到局部的原則池摧,我們應(yīng)當(dāng)先從緩存位置開始討論焦除。等講到 disk cache 時(shí),才會(huì)詳細(xì)講述這些協(xié)議頭的字段及其作用作彤。
我們可以在 Chrome 的開發(fā)者工具中膘魄,Network -> Size 一列看到一個(gè)請(qǐng)求最終的處理方式:如果是大小 (多少 K, 多少 M 等) 就表示是網(wǎng)絡(luò)請(qǐng)求竭讳,否則會(huì)列出 from memory cache
, from disk cache
和 from ServiceWorker
创葡。
它們的優(yōu)先級(jí)是:(由上到下尋找,找到即返回绢慢;找不到則繼續(xù))
- Service Worker
- Memory Cache
- Disk Cache
- 網(wǎng)絡(luò)請(qǐng)求
memory cache
memory cache 是內(nèi)存中的緩存灿渴,(與之相對(duì) disk cache 就是硬盤上的緩存)。按照操作系統(tǒng)的常理:先讀內(nèi)存胰舆,再讀硬盤骚露。disk cache 將在后面介紹 (因?yàn)樗膬?yōu)先級(jí)更低一些),這里先討論 memory cache缚窿。
幾乎所有的網(wǎng)絡(luò)請(qǐng)求資源都會(huì)被瀏覽器自動(dòng)加入到 memory cache 中棘幸。但是也正因?yàn)閿?shù)量很大但是瀏覽器占用的內(nèi)存不能無限擴(kuò)大這樣兩個(gè)因素,memory cache 注定只能是個(gè)“短期存儲(chǔ)”倦零。常規(guī)情況下误续,瀏覽器的 TAB 關(guān)閉后該次瀏覽的 memory cache 便告失效 (為了給其他 TAB 騰出位置)。而如果極端情況下 (例如一個(gè)頁(yè)面的緩存就占用了超級(jí)多的內(nèi)存)扫茅,那可能在 TAB 沒關(guān)閉之前女嘲,排在前面的緩存就已經(jīng)失效了。
剛才提過诞帐,幾乎所有的請(qǐng)求資源 都能進(jìn)入 memory cache欣尼,這里細(xì)分一下主要有兩塊:
-
preloader。如果你對(duì)這個(gè)機(jī)制不太了解停蕉,這里做一個(gè)簡(jiǎn)單的介紹愕鼓,詳情可以參閱這篇文章。
熟悉瀏覽器處理流程的同學(xué)們應(yīng)該了解慧起,在瀏覽器打開網(wǎng)頁(yè)的過程中菇晃,會(huì)先請(qǐng)求 HTML 然后解析。之后如果瀏覽器發(fā)現(xiàn)了 js, css 等需要解析和執(zhí)行的資源時(shí)蚓挤,它會(huì)使用 CPU 資源對(duì)它們進(jìn)行解析和執(zhí)行磺送。在古老的年代(大約 2007 年以前)驻子,“請(qǐng)求 js/css - 解析執(zhí)行 - 請(qǐng)求下一個(gè) js/css - 解析執(zhí)行下一個(gè) js/css” 這樣的“串行”操作模式在每次打開頁(yè)面之前進(jìn)行著。很明顯在解析執(zhí)行的時(shí)候估灿,網(wǎng)絡(luò)請(qǐng)求是空閑的崇呵,這就有了發(fā)揮的空間:我們能不能一邊解析執(zhí)行 js/css,一邊去請(qǐng)求下一個(gè)(或下一批)資源呢馅袁?
這就是 preloader 要做的事情域慷。不過 preloader 沒有一個(gè)官方標(biāo)準(zhǔn),所以每個(gè)瀏覽器的處理都略有區(qū)別汗销。例如有些瀏覽器還會(huì)下載 css 中的
@import
內(nèi)容poster
等犹褒。而這些被 preloader 請(qǐng)求夠來的資源就會(huì)被放入 memory cache 中,供之后的解析執(zhí)行操作使用弛针。
preload (雖然看上去和剛才的 preloader 就差了倆字母)叠骑。實(shí)際上這個(gè)大家應(yīng)該更加熟悉一些。這些顯式指定的預(yù)加載資源削茁,也會(huì)被放入 memory cache 中座云。
memory cache 機(jī)制保證了一個(gè)頁(yè)面中如果有兩個(gè)相同的請(qǐng)求 (例如兩個(gè) src
相同的 ,兩個(gè) `href` 相同的
)都實(shí)際只會(huì)被請(qǐng)求最多一次付材,避免浪費(fèi)朦拖。
不過在匹配緩存時(shí),除了匹配完全相同的 URL 之外厌衔,還會(huì)比對(duì)他們的類型璧帝,CORS 中的域名規(guī)則等。因此一個(gè)作為腳本 (script) 類型被緩存的資源是不能用在圖片 (image) 類型的請(qǐng)求中的富寿,即便他們 src
相等睬隶。
在從 memory cache 獲取緩存內(nèi)容時(shí),瀏覽器會(huì)忽視例如 max-age=0
, no-cache
等頭部配置页徐。例如頁(yè)面上存在幾個(gè)相同 src
的圖片苏潜,即便它們可能被設(shè)置為不緩存,但依然會(huì)從 memory cache 中讀取变勇。這是因?yàn)?memory cache 只是短期使用恤左,大部分情況生命周期只有一次瀏覽而已。而 max-age=0
在語(yǔ)義上普遍被解讀為“不要在下次瀏覽時(shí)使用”搀绣,所以和 memory cache 并不沖突飞袋。
但如果站長(zhǎng)是真心不想讓一個(gè)資源進(jìn)入緩存,就連短期也不行链患,那就需要使用 no-store
巧鸭。存在這個(gè)頭部配置的話,即便是 memory cache 也不會(huì)存儲(chǔ)麻捻,自然也不會(huì)從中讀取了纲仍。(后面的第二個(gè)示例有關(guān)于這點(diǎn)的體現(xiàn))
disk cache
disk cache 也叫 HTTP cache呀袱,顧名思義是存儲(chǔ)在硬盤上的緩存,因此它是持久存儲(chǔ)的郑叠,是實(shí)際存在于文件系統(tǒng)中的夜赵。而且它允許相同的資源在跨會(huì)話,甚至跨站點(diǎn)的情況下使用锻拘,例如兩個(gè)站點(diǎn)都使用了同一張圖片。
disk cache 會(huì)嚴(yán)格根據(jù) HTTP 頭信息中的各類字段來判定哪些資源可以緩存击蹲,哪些資源不可以緩存署拟;哪些資源是仍然可用的,哪些資源是過時(shí)需要重新請(qǐng)求的歌豺。當(dāng)命中緩存之后推穷,瀏覽器會(huì)從硬盤中讀取資源,雖然比起從內(nèi)存中讀取慢了一些类咧,但比起網(wǎng)絡(luò)請(qǐng)求還是快了不少的馒铃。絕大部分的緩存都來自 disk cache。
關(guān)于 HTTP 的協(xié)議頭中的緩存字段痕惋,我們會(huì)在稍后進(jìn)行詳細(xì)討論区宇。
凡是持久性存儲(chǔ)都會(huì)面臨容量增長(zhǎng)的問題,disk cache 也不例外值戳。在瀏覽器自動(dòng)清理時(shí)议谷,會(huì)有神秘的算法去把“最老的”或者“最可能過時(shí)的”資源刪除,因此是一個(gè)一個(gè)刪除的堕虹。不過每個(gè)瀏覽器識(shí)別“最老的”和“最可能過時(shí)的”資源的算法不盡相同卧晓,可能也是它們差異性的體現(xiàn)。
Service Worker
上述的緩存策略以及緩存/讀取/失效的動(dòng)作都是由瀏覽器內(nèi)部判斷 & 進(jìn)行的赴捞,我們只能設(shè)置響應(yīng)頭的某些字段來告訴瀏覽器逼裆,而不能自己操作。舉個(gè)生活中去銀行存/取錢的例子來說赦政,你只能告訴銀行職員胜宇,我要存/取多少錢,然后把由他們會(huì)經(jīng)過一系列的記錄和手續(xù)之后恢着,把錢放到金庫(kù)中去掸屡,或者從金庫(kù)中取出錢來交給你。
但 Service Worker 的出現(xiàn)然评,給予了我們另外一種更加靈活仅财,更加直接的操作方式。依然以存/取錢為例碗淌,我們現(xiàn)在可以繞開銀行職員盏求,自己走到金庫(kù)前(當(dāng)然是有別于上述金庫(kù)的一個(gè)單獨(dú)的小金庫(kù))抖锥,自己把錢放進(jìn)去或者取出來。因此我們可以選擇放哪些錢(緩存哪些文件)碎罚,什么情況把錢取出來(路由匹配規(guī)則)磅废,取哪些錢出來(緩存匹配并返回)。當(dāng)然現(xiàn)實(shí)中銀行沒有給我們開放這樣的服務(wù)荆烈。
Service Worker 能夠操作的緩存是有別于瀏覽器內(nèi)部的 memory cache 或者 disk cache 的拯勉。我們可以從 Chrome 的 F12 中,Application -> Cache Storage 找到這個(gè)單獨(dú)的“小金庫(kù)”憔购。除了位置不同之外宫峦,這個(gè)緩存是永久性的,即關(guān)閉 TAB 或者瀏覽器玫鸟,下次打開依然還在(而 memory cache 不是)导绷。有兩種情況會(huì)導(dǎo)致這個(gè)緩存中的資源被清除:手動(dòng)調(diào)用 API cache.delete(resource)
或者容量超過限制,被瀏覽器全部清空屎飘。
如果 Service Worker 沒能命中緩存妥曲,一般情況會(huì)使用 fetch()
方法繼續(xù)獲取資源。這時(shí)候钦购,瀏覽器就去 memory cache 或者 disk cache 進(jìn)行下一次找緩存的工作了檐盟。注意:經(jīng)過 Service Worker 的 fetch()
方法獲取的資源,即便它并沒有命中 Service Worker 緩存押桃,甚至實(shí)際走了網(wǎng)絡(luò)請(qǐng)求遵堵,也會(huì)標(biāo)注為 from ServiceWorker
。這個(gè)情況在后面的第三個(gè)示例中有所體現(xiàn)怨规。
請(qǐng)求網(wǎng)絡(luò)
如果一個(gè)請(qǐng)求在上述 3 個(gè)位置都沒有找到緩存陌宿,那么瀏覽器會(huì)正式發(fā)送網(wǎng)絡(luò)請(qǐng)求去獲取內(nèi)容。之后容易想到波丰,為了提升之后請(qǐng)求的緩存命中率壳坪,自然要把這個(gè)資源添加到緩存中去。具體來說:
- 根據(jù) Service Worker 中的 handler 決定是否存入 Cache Storage (額外的緩存位置)掰烟。
- 根據(jù) HTTP 頭部的相關(guān)字段(
Cache-control
,Pragma
等)決定是否存入 disk cache - memory cache 保存一份資源 的引用爽蝴,以備下次使用。
按失效策略分類
memory cache 是瀏覽器為了加快讀取緩存速度而進(jìn)行的自身的優(yōu)化行為纫骑,不受開發(fā)者控制蝎亚,也不受 HTTP 協(xié)議頭的約束,算是一個(gè)黑盒先馆。Service Worker 是由開發(fā)者編寫的額外的腳本发框,且緩存位置獨(dú)立,出現(xiàn)也較晚煤墙,使用還不算太廣泛梅惯。所以我們平時(shí)最為熟悉的其實(shí)是 disk cache宪拥,也叫 HTTP cache (因?yàn)椴幌?memory cache,它遵守 HTTP 協(xié)議頭中的字段)铣减。平時(shí)所說的強(qiáng)制緩存她君,對(duì)比緩存,以及 Cache-Control
等葫哗,也都?xì)w于此類缔刹。
強(qiáng)制緩存 (也叫強(qiáng)緩存)
強(qiáng)制緩存的含義是,當(dāng)客戶端請(qǐng)求后劣针,會(huì)先訪問緩存數(shù)據(jù)庫(kù)看緩存是否存在校镐。如果存在則直接返回;不存在則請(qǐng)求真的服務(wù)器酿秸,響應(yīng)后再寫入緩存數(shù)據(jù)庫(kù)灭翔。
強(qiáng)制緩存直接減少請(qǐng)求數(shù)魏烫,是提升最大的緩存策略辣苏。 它的優(yōu)化覆蓋了文章開頭提到過的請(qǐng)求數(shù)據(jù)的全部三個(gè)步驟。如果考慮使用緩存來優(yōu)化網(wǎng)頁(yè)性能的話哄褒,強(qiáng)制緩存應(yīng)該是首先被考慮的稀蟋。
可以造成強(qiáng)制緩存的字段是 Cache-control
和 Expires
。
Expires
這是 HTTP 1.0 的字段呐赡,表示緩存到期時(shí)間退客,是一個(gè)絕對(duì)的時(shí)間 (當(dāng)前時(shí)間+緩存時(shí)間),如
Expires: Thu, 10 Nov 2017 08:45:11 GMT
復(fù)制代碼
在響應(yīng)消息頭中链嘀,設(shè)置這個(gè)字段之后萌狂,就可以告訴瀏覽器,在未過期之前不需要再次請(qǐng)求怀泊。
但是茫藏,這個(gè)字段設(shè)置時(shí)有兩個(gè)缺點(diǎn):
- 由于是絕對(duì)時(shí)間,用戶可能會(huì)將客戶端本地的時(shí)間進(jìn)行修改霹琼,而導(dǎo)致瀏覽器判斷緩存失效务傲,重新請(qǐng)求該資源。此外枣申,即使不考慮自信修改售葡,時(shí)差或者誤差等因素也可能造成客戶端與服務(wù)端的時(shí)間不一致,致使緩存失效忠藤。
- 寫法太復(fù)雜了挟伙。表示時(shí)間的字符串多個(gè)空格,少個(gè)字母模孩,都會(huì)導(dǎo)致非法屬性從而設(shè)置失效像寒。
Cache-control
已知Expires的缺點(diǎn)之后烘豹,在HTTP/1.1中,增加了一個(gè)字段Cache-control诺祸,該字段表示資源緩存的最大有效時(shí)間携悯,在該時(shí)間內(nèi),客戶端不需要向服務(wù)器發(fā)送請(qǐng)求
這兩者的區(qū)別就是前者是絕對(duì)時(shí)間筷笨,而后者是相對(duì)時(shí)間憔鬼。如下:
Cache-control: max-age=2592000
復(fù)制代碼
下面列舉一些 Cache-control
字段常用的值:(完整的列表可以查看 MDN)
-
max-age
:即最大有效時(shí)間,在上面的例子中我們可以看到 -
must-revalidate
:如果超過了max-age
的時(shí)間胃夏,瀏覽器必須向服務(wù)器發(fā)送請(qǐng)求轴或,驗(yàn)證資源是否還有效。 -
no-cache
:雖然字面意思是“不要緩存”仰禀,但實(shí)際上還是要求客戶端緩存內(nèi)容的照雁,只是是否使用這個(gè)內(nèi)容由后續(xù)的對(duì)比來決定。 -
no-store
: 真正意義上的“不要緩存”答恶。所有內(nèi)容都不走緩存饺蚊,包括強(qiáng)制和對(duì)比。 -
public
:所有的內(nèi)容都可以被緩存 (包括客戶端和代理服務(wù)器悬嗓, 如 CDN) -
private
:所有的內(nèi)容只有客戶端才可以緩存污呼,代理服務(wù)器不能緩存。默認(rèn)值包竹。
這些值可以混合使用燕酷,例如 Cache-control:public, max-age=2592000
。在混合使用時(shí)周瞎,它們的優(yōu)先級(jí)如下圖:(圖片來自 developers.google.com/web/fundame…)
這里有一個(gè)疑問:max-age=0
和 no-cache
等價(jià)嗎苗缩?從規(guī)范的字面意思來說,max-age
到期是 應(yīng)該(SHOULD) 重新驗(yàn)證声诸,而 no-cache
是 必須(MUST) 重新驗(yàn)證酱讶。但實(shí)際情況以瀏覽器實(shí)現(xiàn)為準(zhǔn),大部分情況他們倆的行為還是一致的双絮。(如果是 max-age=0, must-revalidate
就和 no-cache
等價(jià)了)
順帶一提浴麻,在 HTTP/1.1 之前,如果想使用 no-cache
囤攀,通常是使用 Pragma
字段软免,如 Pragma: no-cache
(這也是 Pragma
字段唯一的取值)。但是這個(gè)字段只是瀏覽器約定俗成的實(shí)現(xiàn)焚挠,并沒有確切規(guī)范膏萧,因此缺乏可靠性。它應(yīng)該只作為一個(gè)兼容字段出現(xiàn),在當(dāng)前的網(wǎng)絡(luò)環(huán)境下其實(shí)用處已經(jīng)很小榛泛。
總結(jié)一下蝌蹂,自從 HTTP/1.1 開始患整,Expires
逐漸被 Cache-control
取代挫以。Cache-control
是一個(gè)相對(duì)時(shí)間,即使客戶端時(shí)間發(fā)生改變教翩,相對(duì)時(shí)間也不會(huì)隨之改變沛简,這樣可以保持服務(wù)器和客戶端的時(shí)間一致性齐鲤。而且 Cache-control
的可配置性比較強(qiáng)大。
Cache-control 的優(yōu)先級(jí)高于 Expires椒楣,為了兼容 HTTP/1.0 和 HTTP/1.1给郊,實(shí)際項(xiàng)目中兩個(gè)字段我們都會(huì)設(shè)置。
對(duì)比緩存 (也叫協(xié)商緩存)
當(dāng)強(qiáng)制緩存失效(超過規(guī)定時(shí)間)時(shí)捧灰,就需要使用對(duì)比緩存淆九,由服務(wù)器決定緩存內(nèi)容是否失效。
流程上說毛俏,瀏覽器先請(qǐng)求緩存數(shù)據(jù)庫(kù)炭庙,返回一個(gè)緩存標(biāo)識(shí)。之后瀏覽器拿這個(gè)標(biāo)識(shí)和服務(wù)器通訊拧抖。如果緩存未失效煤搜,則返回 HTTP 狀態(tài)碼 304 表示繼續(xù)使用免绿,于是客戶端繼續(xù)使用緩存唧席;如果失效,則返回新的數(shù)據(jù)和緩存規(guī)則嘲驾,瀏覽器響應(yīng)數(shù)據(jù)后淌哟,再把規(guī)則寫入到緩存數(shù)據(jù)庫(kù)。
對(duì)比緩存在請(qǐng)求數(shù)上和沒有緩存是一致的辽故,但如果是 304 的話徒仓,返回的僅僅是一個(gè)狀態(tài)碼而已,并沒有實(shí)際的文件內(nèi)容誊垢,因此 在響應(yīng)體體積上的節(jié)省是它的優(yōu)化點(diǎn)掉弛。它的優(yōu)化覆蓋了文章開頭提到過的請(qǐng)求數(shù)據(jù)的三個(gè)步驟中的最后一個(gè):“響應(yīng)”。通過減少響應(yīng)體體積喂走,來縮短網(wǎng)絡(luò)傳輸時(shí)間殃饿。所以和強(qiáng)制緩存相比提升幅度較小,但總比沒有緩存好芋肠。
對(duì)比緩存是可以和強(qiáng)制緩存一起使用的乎芳,作為在強(qiáng)制緩存失效后的一種后備方案。實(shí)際項(xiàng)目中他們也的確經(jīng)常一同出現(xiàn)。
對(duì)比緩存有 2 組字段(不是兩個(gè)):
Last-Modified & If-Modified-Since
-
服務(wù)器通過
Last-Modified
字段告知客戶端奈惑,資源最后一次被修改的時(shí)間吭净,例如Last-Modified: Mon, 10 Nov 2018 09:10:11 GMT 復(fù)制代碼
瀏覽器將這個(gè)值和內(nèi)容一起記錄在緩存數(shù)據(jù)庫(kù)中。
下一次請(qǐng)求相同資源時(shí)時(shí)肴甸,瀏覽器從自己的緩存中找出“不確定是否過期的”緩存寂殉。因此在請(qǐng)求頭中將上次的
Last-Modified
的值寫入到請(qǐng)求頭的If-Modified-Since
字段服務(wù)器會(huì)將
If-Modified-Since
的值與Last-Modified
字段進(jìn)行對(duì)比。如果相等原在,則表示未修改不撑,響應(yīng) 304;反之晤斩,則表示修改了焕檬,響應(yīng) 200 狀態(tài)碼,并返回?cái)?shù)據(jù)澳泵。
但是他還是有一定缺陷的:
- 如果資源更新的速度是秒以下單位实愚,那么該緩存是不能被使用的,因?yàn)樗臅r(shí)間單位最低是秒兔辅。
- 如果文件是通過服務(wù)器動(dòng)態(tài)生成的腊敲,那么該方法的更新時(shí)間永遠(yuǎn)是生成的時(shí)間,盡管文件可能沒有變化维苔,所以起不到緩存的作用碰辅。
Etag & If-None-Match
為了解決上述問題,出現(xiàn)了一組新的字段 Etag
和 If-None-Match
Etag
存儲(chǔ)的是文件的特殊標(biāo)識(shí)(一般都是 hash 生成的)介时,服務(wù)器存儲(chǔ)著文件的 Etag
字段没宾。之后的流程和 Last-Modified
一致,只是 Last-Modified
字段和它所表示的更新時(shí)間改變成了 Etag
字段和它所表示的文件 hash沸柔,把 If-Modified-Since
變成了 If-None-Match
循衰。服務(wù)器同樣進(jìn)行比較,命中返回 304, 不命中返回新資源和 200褐澎。
Etag 的優(yōu)先級(jí)高于 Last-Modified
緩存小結(jié)
當(dāng)瀏覽器要請(qǐng)求資源時(shí)
- 調(diào)用 Service Worker 的
fetch
事件響應(yīng) - 查看 memory cache
- 查看 disk cache会钝。這里又細(xì)分:
- 如果有強(qiáng)制緩存且未失效,則使用強(qiáng)制緩存工三,不請(qǐng)求服務(wù)器迁酸。這時(shí)的狀態(tài)碼全部是 200
- 如果有強(qiáng)制緩存但已失效,使用對(duì)比緩存俭正,比較后確定 304 還是 200
- 發(fā)送網(wǎng)絡(luò)請(qǐng)求奸鬓,等待網(wǎng)絡(luò)響應(yīng)
- 把響應(yīng)內(nèi)容存入 disk cache (如果 HTTP 頭信息配置可以存的話)
- 把響應(yīng)內(nèi)容 的引用 存入 memory cache (無視 HTTP 頭信息的配置)
- 把響應(yīng)內(nèi)容存入 Service Worker 的 Cache Storage (如果 Service Worker 的腳本調(diào)用了
cache.put()
)
一些案例
光看原理不免枯燥。我們編寫一些簡(jiǎn)單的網(wǎng)頁(yè)段审,通過案例來深刻理解上面的那些原理全蝶。
1. memory cache & disk cache
我們寫一個(gè)簡(jiǎn)單的 index.html
闹蒜,然后引用 3 種資源,分別是 index.js
, index.css
和 mashroom.jpg
抑淫。
我們給這三種資源都設(shè)置上 Cache-control: max-age=86400
绷落,表示強(qiáng)制緩存 24 小時(shí)始苇。以下截圖全部使用 Chrome 的隱身模式砌烁。
- 首次請(qǐng)求
毫無意外的全部走網(wǎng)絡(luò)請(qǐng)求,因?yàn)槭裁淳彺娑歼€沒有催式。
- 再次請(qǐng)求 (F5)
第二次請(qǐng)求函喉,三個(gè)請(qǐng)求都來自 memory cache。因?yàn)槲覀儧]有關(guān)閉 TAB荣月,所以瀏覽器把緩存的應(yīng)用加到了 memory cache管呵。(耗時(shí) 0ms,也就是 1ms 以內(nèi))
- 關(guān)閉 TAB哺窄,打開新 TAB 并再次請(qǐng)求
因?yàn)殛P(guān)閉了 TAB捐下,memory cache 也隨之清空。但是 disk cache 是持久的萌业,于是所有資源來自 disk cache坷襟。(大約耗時(shí) 3ms,因?yàn)槲募悬c(diǎn)小)
而且對(duì)比 2 和 3生年,很明顯看到 memory cache 還是比 disk cache 快得多的婴程。
2. no-cache & no-store
我們?cè)?index.html
里面一些代碼,完成兩個(gè)目標(biāo):
- 每種資源都(同步)請(qǐng)求兩次
- 增加腳本異步請(qǐng)求圖片
<!-- 把3種資源都改成請(qǐng)求兩次 -->
<link rel="stylesheet" href="/static/index.css">
<link rel="stylesheet" href="/static/index.css">
<script src="/static/index.js"></script>
<script src="/static/index.js"></script>
<img src="/static/mashroom.jpg">
<img src="/static/mashroom.jpg">
<!-- 異步請(qǐng)求圖片 -->
<script>
setTimeout(function () {
let img = document.createElement('img')
img.src = '/static/mashroom.jpg'
document.body.appendChild(img)
}, 1000)
</script>
復(fù)制代碼
- 當(dāng)把服務(wù)器響應(yīng)設(shè)置為
Cache-Control: no-cache
時(shí)抱婉,我們發(fā)現(xiàn)打開頁(yè)面之后档叔,三種資源都只被請(qǐng)求 1 次。
這說明兩個(gè)問題:
- 同步請(qǐng)求方面授段,瀏覽器會(huì)自動(dòng)把當(dāng)次 HTML 中的資源存入到緩存 (memory cache)蹲蒲,這樣碰到相同
src
的圖片就會(huì)自動(dòng)讀取緩存(但不會(huì)在 Network 中顯示出來) - 異步請(qǐng)求方面番甩,瀏覽器同樣是不發(fā)請(qǐng)求而直接讀取緩存返回侵贵。但同樣不會(huì)在 Network 中顯示。
總體來說缘薛,如上面原理所述窍育,no-cache
從語(yǔ)義上表示下次請(qǐng)求不要直接使用緩存而需要比對(duì),并不對(duì)本次請(qǐng)求進(jìn)行限制宴胧。因此瀏覽器在處理當(dāng)前頁(yè)面時(shí)漱抓,可以放心使用緩存。
- 當(dāng)把服務(wù)器響應(yīng)設(shè)置為
Cache-Control: no-store
時(shí)恕齐,情況發(fā)生了變化乞娄,三種資源都被請(qǐng)求了 2 次。而圖片因?yàn)檫€多一次異步請(qǐng)求,總計(jì) 3 次仪或。(紅框中的都是那一次異步請(qǐng)求)
這同樣說明:
- 如之前原理所述确镊,雖然 memory cache 是無視 HTTP 頭信息的,但是
no-store
是特別的范删。在這個(gè)設(shè)置下蕾域,memory cache 也不得不每次都請(qǐng)求資源。 - 異步請(qǐng)求和同步遵循相同的規(guī)則到旦,在
no-store
情況下旨巷,依然是每次都發(fā)送請(qǐng)求,不進(jìn)行任何緩存添忘。
3. Service Worker & memory (disk) cache
我們嘗試把 Service Worker 也加入進(jìn)去采呐。我們編寫一個(gè) serviceWorker.js
,并編寫如下內(nèi)容:(主要是預(yù)緩存 3 個(gè)資源搁骑,并在實(shí)際請(qǐng)求時(shí)匹配緩存并返回)
// serviceWorker.js
self.addEventListener('install', e => {
// 當(dāng)確定要訪問某些資源時(shí)懈万,提前請(qǐng)求并添加到緩存中。
// 這個(gè)模式叫做“預(yù)緩存”
e.waitUntil(
caches.open('service-worker-test-precache').then(cache => {
return cache.addAll(['/static/index.js', '/static/index.css', '/static/mashroom.jpg'])
})
)
})
self.addEventListener('fetch', e => {
// 緩存中能找到就返回靶病,找不到就網(wǎng)絡(luò)請(qǐng)求会通,之后再寫入緩存并返回。
// 這個(gè)稱為 CacheFirst 的緩存策略娄周。
return e.respondWith(
caches.open('service-worker-test-precache').then(cache => {
return cache.match(e.request).then(matchedResponse => {
return matchedResponse || fetch(e.request).then(fetchedResponse => {
cache.put(e.request, fetchedResponse.clone())
return fetchedResponse
})
})
})
)
})
復(fù)制代碼
注冊(cè) SW 的代碼這里就不贅述了涕侈。此外我們還給服務(wù)器設(shè)置 Cache-Control: max-age=86400
來開啟 disk cache。我們的目的是看看兩者的優(yōu)先級(jí)煤辨。
- 當(dāng)我們首次訪問時(shí)裳涛,會(huì)看到常規(guī)請(qǐng)求之外,瀏覽器(確切地說是 Service Worker)額外發(fā)出了 3 個(gè)請(qǐng)求众辨。這來自預(yù)緩存的代碼端三。
- 第二次訪問(無論關(guān)閉 TAB 重新打開,還是直接按 F5 刷新)都能看到所有的請(qǐng)求標(biāo)記為
from SerciceWorker
鹃彻。
from ServiceWorker
只表示請(qǐng)求通過了 Service Worker郊闯,至于到底是命中了緩存,還是繼續(xù) fetch()
方法光看這一條記錄其實(shí)無從知曉蛛株。因此我們還得配合后續(xù)的 Network 記錄來看团赁。因?yàn)橹鬀]有額外的請(qǐng)求了,因此判定是命中了緩存谨履。
從服務(wù)器的日志也能很明顯地看到欢摄,3 個(gè)資源都沒有被重新請(qǐng)求,即命中了 Service Worker 內(nèi)部的緩存笋粟。
-
如果修改
serviceWorker.js
的fetch
事件監(jiān)聽代碼怀挠,改為如下:// 這個(gè)也叫做 NetworkOnly 的緩存策略析蝴。 self.addEventListener('fetch', e => { return e.respondWith(fetch(e.request)) }) 復(fù)制代碼
可以發(fā)現(xiàn)在后續(xù)訪問時(shí)的效果和修改前是 完全一致的。(即 Network 僅有標(biāo)記為
from ServiceWorker
的幾個(gè)請(qǐng)求绿淋,而服務(wù)器也不打印 3 個(gè)資源的訪問日志)很明顯 Service Worker 這層并沒有去讀取自己的緩存嫌变,而是直接使用
fetch()
進(jìn)行請(qǐng)求。所以此時(shí)其實(shí)是Cache-Control: max-age=86400
的設(shè)置起了作用躬它,也就是 memory/disk cache腾啥。但具體是 memory 還是 disk 這個(gè)只有瀏覽器自己知道了,因?yàn)樗]有顯式的告訴我們冯吓。(個(gè)人猜測(cè)是 memory倘待,因?yàn)椴徽搹暮臅r(shí) 0ms 還是從不關(guān)閉 TAB 來看,都更像是 memory cache)
瀏覽器的行為
所謂瀏覽器的行為组贺,指的就是用戶在瀏覽器如何操作時(shí)凸舵,會(huì)觸發(fā)怎樣的緩存策略。主要有 3 種:
- 打開網(wǎng)頁(yè)失尖,地址欄輸入地址: 查找 disk cache 中是否有匹配啊奄。如有則使用;如沒有則發(fā)送網(wǎng)絡(luò)請(qǐng)求掀潮。
- 普通刷新 (F5):因?yàn)?TAB 并沒有關(guān)閉菇夸,因此 memory cache 是可用的,會(huì)被優(yōu)先使用(如果匹配的話)仪吧。其次才是 disk cache庄新。
- 強(qiáng)制刷新 (Ctrl + F5):瀏覽器不使用緩存,因此發(fā)送的請(qǐng)求頭部均帶有
Cache-control: no-cache
(為了兼容薯鼠,還帶了Pragma: no-cache
)择诈。服務(wù)器直接返回 200 和最新內(nèi)容。
緩存的應(yīng)用模式
了解了緩存的原理出皇,我們可能更加關(guān)心如何在實(shí)際項(xiàng)目中使用它們羞芍,才能更好的讓用戶縮短加載時(shí)間,節(jié)約流量等郊艘。這里有幾個(gè)常用的模式荷科,供大家參考
模式 1:不常變化的資源
Cache-Control: max-age=31536000
復(fù)制代碼
通常在處理這類資源資源時(shí),給它們的 Cache-Control
配置一個(gè)很大的 max-age=31536000
(一年)暇仲,這樣瀏覽器之后請(qǐng)求相同的 URL 會(huì)命中強(qiáng)制緩存步做。而為了解決更新的問題,就需要在文件名(或者路徑)中添加 hash奈附, 版本號(hào)等動(dòng)態(tài)字符,之后更改動(dòng)態(tài)字符煮剧,達(dá)到更改引用 URL 的目的斥滤,從而讓之前的強(qiáng)制緩存失效 (其實(shí)并未立即失效将鸵,只是不再使用了而已)。
在線提供的類庫(kù) (如 jquery-3.3.1.min.js, lodash.min.js 等) 均采用這個(gè)模式佑颇。如果配置中還增加 public
的話顶掉,CDN 也可以緩存起來,效果拔群挑胸。
這個(gè)模式的一個(gè)變體是在引用 URL 后面添加參數(shù) (例如 ?v=xxx
或者 ?_=xxx
)痒筒,這樣就不必在文件名或者路徑中包含動(dòng)態(tài)參數(shù),滿足某些完美主義者的喜好茬贵。在項(xiàng)目每次構(gòu)建時(shí)簿透,更新額外的參數(shù) (例如設(shè)置為構(gòu)建時(shí)的當(dāng)前時(shí)間),則能保證每次構(gòu)建后總能讓瀏覽器請(qǐng)求最新的內(nèi)容解藻。
特別注意: 在處理 Service Worker 時(shí)老充,對(duì)待 sw-register.js
(注冊(cè) Service Worker) 和 serviceWorker.js
(Service Worker 本身) 需要格外的謹(jǐn)慎。如果這兩個(gè)文件也使用這種模式螟左,你必須多多考慮日后可能的更新及對(duì)策啡浊。
模式 2:經(jīng)常變化的資源
Cache-Control: no-cache
復(fù)制代碼
這里的資源不單單指靜態(tài)資源,也可能是網(wǎng)頁(yè)資源胶背,例如博客文章巷嚣。這類資源的特點(diǎn)是:URL 不能變化,但內(nèi)容可以(且經(jīng)常)變化钳吟。我們可以設(shè)置 Cache-Control: no-cache
來迫使瀏覽器每次請(qǐng)求都必須找服務(wù)器驗(yàn)證資源是否有效涂籽。
既然提到了驗(yàn)證,就必須 ETag
或者 Last-Modified
出場(chǎng)砸抛。這些字段都會(huì)由專門處理靜態(tài)資源的常用類庫(kù)(例如 koa-static
)自動(dòng)添加评雌,無需開發(fā)者過多關(guān)心。
也正如上文中提到協(xié)商緩存那樣直焙,這種模式下景东,節(jié)省的并不是請(qǐng)求數(shù),而是請(qǐng)求體的大小奔誓。所以它的優(yōu)化效果不如模式 1 來的顯著斤吐。
模式 3:非常危險(xiǎn)的模式 1 和 2 的結(jié)合 (反例)
Cache-Control: max-age=600, must-revalidate
復(fù)制代碼
不知道是否有開發(fā)者從模式 1 和 2 獲得一些啟發(fā):模式 2 中,設(shè)置了 no-cache
厨喂,相當(dāng)于 max-age=0, must-revalidate
和措。我的應(yīng)用時(shí)效性沒有那么強(qiáng),但又不想做過于長(zhǎng)久的強(qiáng)制緩存蜕煌,我能不能配置例如 max-age=600, must-revalidate
這樣折中的設(shè)置呢派阱?
表面上看這很美好:資源可以緩存 10 分鐘,10 分鐘內(nèi)讀取緩存斜纪,10 分鐘后和服務(wù)器進(jìn)行一次驗(yàn)證贫母,集兩種模式之大成文兑,但實(shí)際線上暗存風(fēng)險(xiǎn)。因?yàn)樯厦嫣徇^腺劣,瀏覽器的緩存有自動(dòng)清理機(jī)制绿贞,開發(fā)者并不能控制。
舉個(gè)例子:當(dāng)我們有 3 種資源: index.html
, index.js
, index.css
橘原。我們對(duì)這 3 者進(jìn)行上述配置之后籍铁,假設(shè)在某次訪問時(shí),index.js
已經(jīng)被緩存清理而不存在趾断,但 index.html
, index.css
仍然存在于緩存中拒名。這時(shí)候?yàn)g覽器會(huì)向服務(wù)器請(qǐng)求新的 index.js
,然后配上老的 index.html
, index.css
展現(xiàn)給用戶歼冰。這其中的風(fēng)險(xiǎn)顯而易見:不同版本的資源組合在一起靡狞,報(bào)錯(cuò)是極有可能的結(jié)局。
除了自動(dòng)清理引發(fā)問題隔嫡,不同資源的請(qǐng)求時(shí)間不同也能導(dǎo)致問題甸怕。例如 A 頁(yè)面請(qǐng)求的是 A.js
和 all.css
,而 B 頁(yè)面是 B.js
和 all.css
腮恩。如果我們以 A -> B 的順序訪問頁(yè)面梢杭,勢(shì)必導(dǎo)致 all.css
的緩存時(shí)間早于 B.js
。那么以后訪問 B 頁(yè)面就同樣存在資源版本失配的隱患秸滴。
有開發(fā)者朋友(wd2010)在知乎的評(píng)論區(qū)提了一個(gè)很好的問題:
如果我不使用must-revalidate武契,只是Cache-Control: max-age=600,瀏覽器緩存的自動(dòng)清理機(jī)制就不會(huì)執(zhí)行么荡含?如果瀏覽器緩存的自動(dòng)清理機(jī)制執(zhí)行的話那后續(xù)的index.js被清掉的所引發(fā)的情況都是一樣的呀咒唆!
這個(gè)問題涉及幾個(gè)小點(diǎn),我補(bǔ)充說明一下:
-
'max-age=600' 和 'max-age=600,must-revalidate' 有什么區(qū)別释液?
沒有區(qū)別全释。在列出 max-age 了之后,must-revalidate 是否列出效果相同误债,瀏覽器都會(huì)在超過 max-age 之后進(jìn)行校驗(yàn)浸船,驗(yàn)證緩存是否可用。
在 HTTP 的規(guī)范中寝蹈,只闡述了 must-revalidate 的作用李命,卻沒有闡述不列出 must-revalidate 時(shí),瀏覽器應(yīng)該如何解決緩存過期的問題箫老,因此這其實(shí)是瀏覽器實(shí)現(xiàn)時(shí)的自主決策封字。(可能有少數(shù)瀏覽器選擇在源站點(diǎn)無法訪問時(shí)繼續(xù)使用過期緩存,但這取決于瀏覽器自身)
-
那 'max-age=600' 是不是也會(huì)引發(fā)問題?
是的周叮。問題的出現(xiàn)和是否列出 'must-revalidate' 無關(guān)辩撑,依然會(huì)存在 JS CSS等文件版本失配的問題界斜。因此常規(guī)的網(wǎng)站在不同頁(yè)面需要使用不同的 JS CSS 文件時(shí)仿耽,如果要使用 max-age 做強(qiáng)緩存,不要設(shè)置一個(gè)太短的時(shí)間各薇。
-
那這類比較短的 max-age 到底能用在哪里呢项贺?
既然版本存在失配的問題,那么要避開這個(gè)問題峭判,就有兩種方法开缎。
- 整站都使用相同的 JS 和 CSS,即合并后的文件林螃。這個(gè)比較適合小型站點(diǎn)奕删,否則可能過于冗余,影響性能疗认。(不過可能還是會(huì)因?yàn)闉g覽器自身的清理策略被清理完残,依然有隱患)
- 資源是獨(dú)立使用的,并不需要和其他文件配合生效横漏。例如 RSS 就歸在此類谨设。
后記
這篇文章真心有點(diǎn)長(zhǎng),但已經(jīng)囊括了前端緩存的絕大部分缎浇,包括 HTTP 協(xié)議中的緩存扎拣,Service Worker,以及 Chrome 瀏覽器的一些優(yōu)化 (Memory Cache)素跺。希望開發(fā)者們善用緩存二蓝,因?yàn)樗亲钊菀紫氲剑嵘沧畲蟮男阅軆?yōu)化策略指厌。
Token - 服務(wù)端身份驗(yàn)證的流行方案
簡(jiǎn)述:
- 需要一個(gè)secret(隨機(jī)數(shù))
- 后端利用secret和加密算法(如:HMAC-SHA256)對(duì)payload(如賬號(hào)密碼)生成一個(gè)字符串(token)刊愚,返回前端
- 前端每次request在header中帶上token
- 后端用同樣的算法解密
身份認(rèn)證
服務(wù)端提供資源給客戶端,但是某些資源是有條件的仑乌。所以服務(wù)端要能夠識(shí)別請(qǐng)求者的身份百拓,然后再判斷所請(qǐng)求的資源是否可以給請(qǐng)求者。
token是一種身份驗(yàn)證的機(jī)制晰甚,初始時(shí)用戶提交賬號(hào)數(shù)據(jù)給服務(wù)端衙传,服務(wù)端采用一定的策略生成一個(gè)字符串(token),token字符串中包含了少量的用戶信息厕九,并且有一定的期限衣吠。服務(wù)端會(huì)把token字符串傳給客戶端资溃,客戶端保存token字符串遏匆,并在接下來的請(qǐng)求中帶上這個(gè)字符串家破。
它的工作流程大概是這樣:
組件圖
Token機(jī)制
在這樣的流程下,我們需要考慮下面幾個(gè)問題:
- 服務(wù)端如何根據(jù)token獲取用戶的信息板甘?
- 如何確保識(shí)別偽造的token?
這里是指token不是經(jīng)過服務(wù)端來生成的。 - 如何應(yīng)付冒充的情況细睡?
非法客戶端攔截了合法客戶端的token,然后使用這個(gè)token向服務(wù)端發(fā)送請(qǐng)求帝火,冒充合法客戶端溜徙。
用戶匹配
服務(wù)端在生成token時(shí),加入少量的用戶信息犀填,比如用戶的id蠢壹。服務(wù)端接收到token之后,可以解析出這些數(shù)據(jù)九巡,從而將token和用戶關(guān)聯(lián)了起來图贸。
防偽造
一般情況下,建議放入token的數(shù)據(jù)是不敏感的數(shù)據(jù)冕广,這樣只要服務(wù)端使用私鑰對(duì)數(shù)據(jù)生成簽名疏日,然后和數(shù)據(jù)拼接起來,作為token的一部分即可佳窑。
防冒充
加干擾碼
服務(wù)端在生成token時(shí)制恍,使用了客戶端的UA作為干擾碼對(duì)數(shù)據(jù)加密,客戶端進(jìn)行請(qǐng)求時(shí)神凑,會(huì)同時(shí)傳入token净神、UA,服務(wù)端使用UA對(duì)token解密溉委,從而驗(yàn)證用戶的身份鹃唯。
如果只是把token拷貝到另一個(gè)客戶端使用,不同的UA會(huì)導(dǎo)致在服務(wù)端解析token失敗瓣喊,從而實(shí)現(xiàn)了一定程度的防冒充坡慌。但是攻擊者如果猜到服務(wù)端使用UA作為加密鑰,他可以修改自己的UA藻三。
有效期
給token加上有效期洪橘,即使被冒充也只是在一定的時(shí)間段內(nèi)有效。這不是完美的防御措施棵帽,只是盡量減少損失熄求。
服務(wù)端在生成token時(shí),加入有效期逗概。每次服務(wù)端接收到請(qǐng)求弟晚,解析token之后,判斷是否已過期,如果過期就拒絕服務(wù)卿城。
token刷新
如果token過期了枚钓,客戶端應(yīng)該對(duì)token續(xù)期或者重新生成token。這取決于token的過期機(jī)制瑟押。
- 服務(wù)器緩存token及對(duì)應(yīng)的過期時(shí)間
這個(gè)時(shí)候就可以采用續(xù)期的方式搀捷,服務(wù)器更新過期時(shí)間,token再次有效勉耀。 - token中含有過期時(shí)間
這個(gè)時(shí)候需要重新生成token指煎。
在token續(xù)期或者重新生成token的時(shí)候蹋偏,需要額外加入數(shù)據(jù)來驗(yàn)證身份便斥。因?yàn)閠oken已經(jīng)過期了,即token已經(jīng)不能用來驗(yàn)證用戶的身份了威始。這個(gè)時(shí)候可以請(qǐng)求用戶重新輸入賬號(hào)和密碼枢纠,但是用戶體驗(yàn)稍差。
另一種方式是使用摘要黎棠。服務(wù)端生成token晋渺,同時(shí)生成token的摘要,然后一起返回給客戶端脓斩∧疚鳎客戶端保存摘要,一般請(qǐng)求只需要用到token随静,在刷新token時(shí)八千,才需要用到摘要。服務(wù)端驗(yàn)證摘要燎猛,來驗(yàn)證用戶的身份恋捆。因?yàn)檎粫?huì)頻繁的在客戶端和服務(wù)端之間傳輸,所以被截取的概率較小重绷。
Token工作流程
生成token
生成token
一般在登錄的時(shí)候生成token沸停。Token管理者負(fù)責(zé)根據(jù)用戶的數(shù)據(jù)生成token和摘要,摘要用來給APP端刷新token用昭卓,類似于微信登錄中的refresh_token愤钾。
生成token的過程中,ua的充作干擾碼候醒。沒有相同的ua能颁,就無法解析生成的token字符串。如果客戶端是混合開發(fā)的模式火焰,生成token和使用token的代理可能不同(比如一個(gè)是h5劲装,一個(gè)是原生),ua就會(huì)不同,token就無法成功的使用占业∪拊梗可以選擇其他的客戶端數(shù)據(jù)作為干擾碼,注意考慮下面的問題:
- 不同的客戶端谦疾,干擾碼應(yīng)該不同
干擾碼的很大一個(gè)作用是防冒充南蹂,如果選擇的充當(dāng)干擾碼的客戶端數(shù)據(jù)沒有區(qū)分性,就達(dá)不到效果念恍。 - 選擇充當(dāng)干擾碼的數(shù)據(jù)六剥,在哪些情況下會(huì)變化?這些情況是否合理峰伙?
比如干擾碼數(shù)據(jù)中含有app的版本號(hào)疗疟,那么app版本升級(jí)就會(huì)導(dǎo)致干擾碼變化。服務(wù)端根據(jù)新的干擾碼瞳氓,無法解析舊的token策彤,此時(shí)用戶必須重新登錄。這種情況是合理的嗎匣摘?如果不合理店诗,干擾碼中就不應(yīng)該含有app的版本號(hào)。
攔截驗(yàn)證
攔截驗(yàn)證
客戶端的每一次請(qǐng)求音榜,都必須攜帶token庞瘸、ua,攔截器會(huì)對(duì)敏感資源的訪問進(jìn)行攔截赠叼,然后根據(jù)ua解析token擦囊,解析不成功,表示token和ua不匹配梅割。解析成功之后霜第,判斷token是否已過期,如果是户辞,拒絕服務(wù)泌类。所有都o(jì)k的情況下,攔截器放行底燎,請(qǐng)求傳達(dá)到業(yè)務(wù)服務(wù)者刃榨。
token刷新
token刷新
當(dāng)token過期,用戶需要刷新token双仍。刷新token本質(zhì)上是這樣的:
服務(wù)端:這個(gè)token是你的嗎枢希?
客戶端:是的。
服務(wù)端:當(dāng)初我給你token的時(shí)候朱沃,還給了一個(gè)摘要苞轿,你把摘要拿過來證明茅诱。
客戶端需要把token、摘要搬卒、ua都傳給服務(wù)端瑟俭,服務(wù)端對(duì)token重新生成摘要,通過判斷兩個(gè)摘要是否相同來驗(yàn)證這次請(qǐng)求刷新token的客戶端契邀,是不是上次請(qǐng)求生成token的客戶端摆寄。驗(yàn)證通過,服務(wù)端需要使用用戶數(shù)據(jù)重新生成token坯门,用戶數(shù)據(jù)則來自用ua解析token的結(jié)果微饥。
基于 Token 的身份驗(yàn)證:JSON Web Token
很多大型網(wǎng)站也都在用,比如 Facebook古戴,Twitter欠橘,Google+,Github 等等允瞧,比起傳統(tǒng)的身份驗(yàn)證方法简软,Token 擴(kuò)展性更強(qiáng),也更安全點(diǎn)述暂,非常適合用在 Web 應(yīng)用或者移動(dòng)應(yīng)用上。Token 的中文有人翻譯成 “令牌”建炫,我覺得挺好畦韭,意思就是,你拿著這個(gè)令牌肛跌,才能過一些關(guān)卡艺配。
文章先介紹了一下傳統(tǒng)身份驗(yàn)證與基于 JWT 身份驗(yàn)證的方法,再理解一下 JWT 的 Token 的組成部分(頭部衍慎,數(shù)據(jù)转唉,簽名),最后我們會(huì)在一個(gè) Node.js 項(xiàng)目上實(shí)施簽發(fā)與驗(yàn)證 JWT 的功能稳捆。
傳統(tǒng)身份驗(yàn)證的方法
HTTP 是一種沒有狀態(tài)的協(xié)議赠法,也就是它并不知道是誰(shuí)是訪問應(yīng)用。這里我們把用戶看成是客戶端乔夯,客戶端使用用戶名還有密碼通過了身份驗(yàn)證砖织,不過下回這個(gè)客戶端再發(fā)送請(qǐng)求時(shí)候,還得再驗(yàn)證一下末荐。
解決的方法就是侧纯,當(dāng)用戶請(qǐng)求登錄的時(shí)候,如果沒有問題甲脏,我們?cè)诜?wù)端生成一條記錄眶熬,這個(gè)記錄里可以說明一下登錄的用戶是誰(shuí)妹笆,然后把這條記錄的 ID 號(hào)發(fā)送給客戶端,客戶端收到以后把這個(gè) ID 號(hào)存儲(chǔ)在 Cookie 里娜氏,下次這個(gè)用戶再向服務(wù)端發(fā)送請(qǐng)求的時(shí)候晾浴,可以帶著這個(gè) Cookie ,這樣服務(wù)端會(huì)驗(yàn)證一個(gè)這個(gè) Cookie 里的信息牍白,看看能不能在服務(wù)端這里找到對(duì)應(yīng)的記錄脊凰,如果可以,說明用戶已經(jīng)通過了身份驗(yàn)證茂腥,就把用戶請(qǐng)求的數(shù)據(jù)返回給客戶端狸涌。
上面說的就是 Session,我們需要在服務(wù)端存儲(chǔ)為登錄的用戶生成的 Session 最岗,這些 Session 可能會(huì)存儲(chǔ)在內(nèi)存帕胆,磁盤,或者數(shù)據(jù)庫(kù)里般渡。我們可能需要在服務(wù)端定期的去清理過期的 Session 懒豹。
基于 Token 的身份驗(yàn)證方法
使用基于 Token 的身份驗(yàn)證方法,在服務(wù)端不需要存儲(chǔ)用戶的登錄記錄驯用。大概的流程是這樣的:
- 客戶端使用用戶名跟密碼請(qǐng)求登錄
- 服務(wù)端收到請(qǐng)求脸秽,去驗(yàn)證用戶名與密碼
- 驗(yàn)證成功后,服務(wù)端會(huì)簽發(fā)一個(gè) Token蝴乔,再把這個(gè) Token 發(fā)送給客戶端
- 客戶端收到 Token 以后可以把它存儲(chǔ)起來记餐,比如放在 Cookie 里或者 Local Storage 里
- 客戶端每次向服務(wù)端請(qǐng)求資源的時(shí)候需要帶著服務(wù)端簽發(fā)的 Token
- 服務(wù)端收到請(qǐng)求,然后去驗(yàn)證客戶端請(qǐng)求里面帶著的 Token薇正,如果驗(yàn)證成功片酝,就向客戶端返回請(qǐng)求的數(shù)據(jù)
JWT
實(shí)施 Token 驗(yàn)證的方法挺多的,還有一些標(biāo)準(zhǔn)方法挖腰,比如 JWT雕沿,讀作:jot ,表示:JSON Web Tokens 猴仑。JWT 標(biāo)準(zhǔn)的 Token 有三個(gè)部分:
- header(頭部)
- payload(數(shù)據(jù))
- signature(簽名)
中間用點(diǎn)分隔開审轮,并且都會(huì)使用 Base64 編碼,所以真正的 Token 看起來像這樣:
eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJuaW5naGFvLm5ldCIsImV4cCI6IjE0Mzg5NTU0NDUiLCJuYW1lIjoid2FuZ2hhbyIsImFkbWluIjp0cnVlfQ.SwyHTEx_RQppr97g4J5lKXtabJecpejuef8AqKYMAJc
Header
每個(gè) JWT token 里面都有一個(gè) header宁脊,也就是頭部數(shù)據(jù)断国。里面包含了使用的算法,這個(gè) JWT 是不是帶簽名的或者加密的榆苞。主要就是說明一下怎么處理這個(gè) JWT token 稳衬。
頭部里包含的東西可能會(huì)根據(jù) JWT 的類型有所變化,比如一個(gè)加密的 JWT 里面要包含使用的加密的算法坐漏。唯一在頭部里面要包含的是 alg 這個(gè)屬性薄疚,如果是加密的 JWT碧信,這個(gè)屬性的值就是使用的簽名或者解密用的算法。如果是未加密的 JWT街夭,這個(gè)屬性的值要設(shè)置成 none砰碴。
示例:
{
"alg": "HS256"
}
意思是這個(gè) JWT 用的算法是 HS256。上面的內(nèi)容得用 base64url 的形式編碼一下板丽,所以就變成這樣:
eyJhbGciOiJIUzI1NiJ9
Payload
Payload 里面是 Token 的具體內(nèi)容呈枉,這些內(nèi)容里面有一些是標(biāo)準(zhǔn)字段,你也可以添加其它需要的內(nèi)容埃碱。下面是標(biāo)準(zhǔn)字段:
- iss:Issuer猖辫,發(fā)行者
- sub:Subject,主題
- aud:Audience砚殿,觀眾
- exp:Expiration time啃憎,過期時(shí)間
- nbf:Not before
- iat:Issued at,發(fā)行時(shí)間
- jti:JWT ID
比如下面這個(gè) Payload 似炎,用到了 iss 發(fā)行人辛萍,還有 exp 過期時(shí)間這兩個(gè)標(biāo)準(zhǔn)字段。另外還有兩個(gè)自定義的字段羡藐,一個(gè)是 name 贩毕,還有一個(gè)是 admin 。
{
"iss": "ninghao.net",
"exp": "1438955445",
"name": "wanghao",
"admin": true
}
使用 base64url 編碼以后就變成了這個(gè)樣子:
eyJpc3MiOiJuaW5naGFvLm5ldCIsImV4cCI6IjE0Mzg5NTU0NDUiLCJuYW1lIjoid2FuZ2hhbyIsImFkbWluIjp0cnVlfQ
Signature
JWT 的最后一部分是 Signature 传睹,這部分內(nèi)容有三個(gè)部分耳幢,先是用 Base64 編碼的 header.payload ,再用加密算法加密一下欧啤,加密的時(shí)候要放進(jìn)去一個(gè) Secret ,這個(gè)相當(dāng)于是一個(gè)密碼启上,這個(gè)密碼秘密地存儲(chǔ)在服務(wù)端邢隧。
- header
- payload
- secret
const encodedString = base64UrlEncode(header) + "." + base64UrlEncode(payload);
HMACSHA256(encodedString, 'secret');
處理完成以后看起來像這樣:
SwyHTEx_RQppr97g4J5lKXtabJecpejuef8AqKYMAJc
最后這個(gè)在服務(wù)端生成并且要發(fā)送給客戶端的 Token 看起來像這樣:
eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJuaW5naGFvLm5ldCIsImV4cCI6IjE0Mzg5NTU0NDUiLCJuYW1lIjoid2FuZ2hhbyIsImFkbWluIjp0cnVlfQ.SwyHTEx_RQppr97g4J5lKXtabJecpejuef8AqKYMAJc
客戶端收到這個(gè) Token 以后把它存儲(chǔ)下來,下回向服務(wù)端發(fā)送請(qǐng)求的時(shí)候就帶著這個(gè) Token 冈在。服務(wù)端收到這個(gè) Token 倒慧,然后進(jìn)行驗(yàn)證,通過以后就會(huì)返回給客戶端想要的資源包券。
簽發(fā)與驗(yàn)證 JWT
在應(yīng)用里實(shí)施使用基于 JWT 這種 Token 的身份驗(yàn)證方法纫谅,你可以先去找一個(gè)簽發(fā)與驗(yàn)證 JWT 的功能包。無論你的后端應(yīng)用使用的是什么樣的程序語(yǔ)言溅固,系統(tǒng)付秕,或者框架,你應(yīng)該都可以找到提供類似功能的包侍郭。
下面我們?cè)谝粋€(gè) Node.js 項(xiàng)目里询吴,用最簡(jiǎn)單的方式來演示一下簽發(fā)還有驗(yàn)證 JWT 的方法掠河。
準(zhǔn)備項(xiàng)目
準(zhǔn)備一個(gè)簡(jiǎn)單的 Node.js 項(xiàng)目:
cd ~/desktop
mkdir jwt-demo
cd jwt-demo
npm init -y
安裝簽發(fā)與驗(yàn)證 JWT 的功能包,我用的叫 jsonwebtoken猛计,在項(xiàng)目里安裝一下這個(gè)包:
npm install jsonwebtoken --save
簽發(fā) JWT
在項(xiàng)目里隨便添加一個(gè) .js 文件唠摹,比如 index.js,在文件里添加下面這些代碼:
const jwt = require('jsonwebtoken')
// Token 數(shù)據(jù)
const payload = {
name: 'wanghao',
admin: true
}
// 密鑰
const secret = 'ILOVENINGHAO'
// 簽發(fā) Token
const token = jwt.sign(payload, secret, { expiresIn: '1day' })
// 輸出簽發(fā)的 Token
console.log(token)
非常簡(jiǎn)單奉瘤,就是用了剛剛為項(xiàng)目安裝的 jsonwebtoken 里面提供的 jwt.sign 功能勾拉,去簽發(fā)一個(gè) token。這個(gè) sign 方法需要三個(gè)參數(shù):
- playload:簽發(fā)的 token 里面要包含的一些數(shù)據(jù)盗温。
- secret:簽發(fā) token 用的密鑰藕赞,在驗(yàn)證 token 的時(shí)候同樣需要用到這個(gè)密鑰。
- options:一些其它的選項(xiàng)肌访。
在命令行下面找默,用 node 命令,執(zhí)行一下項(xiàng)目里的 index.js 這個(gè)文件(node index.js)吼驶,會(huì)輸出應(yīng)用簽發(fā)的 token:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoid2FuZ2hhbyIsImFkbWluIjp0cnVlLCJpYXQiOjE1MjkwMzM5MDYsImV4cCI6MTUyOTEyMDMwNn0.DctA2QlUCrM6wLWkIO78wBVN0NLpjoIq4T5B_2WJ-PU
上面的 Token 內(nèi)容并沒有加密惩激,所以如果用一些 JWT 解碼功能,可以看到 Token 里面包含的內(nèi)容蟹演,內(nèi)容由三個(gè)部分組成风钻,像這樣:
// header
{
"alg": "HS256",
"typ": "JWT"
}
// payload
{
"admin": true,
"iat": 1529033906,
"name": "wanghao",
"exp": 1529120306
}
// signature
DctA2QlUCrM6wLWkIO78wBVN0NLpjoIq4T5B_2WJ-PU
假設(shè)用戶通過了某種身份驗(yàn)證,你就可以使用上面的簽發(fā) Token 的功能為用戶簽發(fā)一個(gè) Token酒请。一般在客戶端那里會(huì)把它保存在 Cookie 或 LocalStorage 里面骡技。
用戶下次向我們的應(yīng)用請(qǐng)求受保護(hù)的資源的時(shí)候,可以在請(qǐng)求里帶著我們給它簽發(fā)的這個(gè) Token羞反,后端應(yīng)用收到請(qǐng)求布朦,檢查簽名,如果驗(yàn)證通過確定這個(gè) Token 是我們自己簽發(fā)的昼窗,那就可以為用戶響應(yīng)回他需要的資源是趴。
驗(yàn)證 JWT
驗(yàn)證 JWT 的用效性,確定一下用戶的 JWT 是我們自己簽發(fā)的澄惊,首先要得到用戶的這個(gè) JWT Token唆途,然后用 jwt.verify 這個(gè)方法去做一下驗(yàn)證。這個(gè)方法是 Node.js 的 jsonwebtoken 這個(gè)包里提供的掸驱,在其它的應(yīng)用框架或者系統(tǒng)里肛搬,你可能會(huì)找到類似的方法來驗(yàn)證 JWT。
打開項(xiàng)目的 index.js 文件毕贼,里面添加幾行代碼:
// 驗(yàn)證 Token
jwt.verify(token, 'bad secret', (error, decoded) => {
if (error) {
console.log(error.message)
return
}
console.log(decoded)
})
把要驗(yàn)證的 Token 數(shù)據(jù)温赔,還有簽發(fā)這個(gè) Token 的時(shí)候用的那個(gè)密鑰告訴 verify 這個(gè)方法,在一個(gè)回調(diào)里面有兩個(gè)參數(shù)帅刀,error 表示錯(cuò)誤让腹,decoded 是解碼之后的 Token 數(shù)據(jù)远剩。
執(zhí)行:
node ~/desktop/jwt-demo/index.js
輸出:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoid2FuZ2hhbyIsImFkbWluIjp0cnVlLCJpYXQiOjE1MjkwMzQ3MzMsImV4cCI6MTUyOTEyMTEzM30.swXojmu7VimFu3BoIgAxxpmm2J05dvD0HT3yu10vuqU
invalid signature
注意輸出了一個(gè) invalid signature ,表示 Token 里的簽名不對(duì)骇窍,這是因?yàn)槲覀兘M長(zhǎng) verify 方法提供的密鑰并不是簽發(fā) Token 的時(shí)候用的那個(gè)密鑰瓜晤。這樣修改一下:
jwt.verify(token, secret, (error, decoded) => { ...
再次運(yùn)行,會(huì)輸出類似的數(shù)據(jù):
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoid2FuZ2hhbyIsImFkbWluIjp0cnVlLCJpYXQiOjE1MjkwMzUzODYsImV4cCI6MTUyOTEyMTc4Nn0.mkNrt4TfcfmP22xd3C_GQn8qnUmlB39dKT9SpIBTBGI
{ name: 'wanghao', admin: true, iat: 1529035386, exp: 1529121786 }
RS256 算法
默認(rèn)簽發(fā)還有驗(yàn)證 Token 的時(shí)候用的是 HS256 算法腹纳,這種算法需要一個(gè)密鑰(密碼)痢掠。我們還可以使用 RS256 算法簽發(fā)與驗(yàn)證 JWT。這種方法可以讓我們分離開簽發(fā)與驗(yàn)證嘲恍,簽發(fā)時(shí)需要用一個(gè)密鑰足画,驗(yàn)證時(shí)使用公鑰,也就是有公鑰的地方只能做驗(yàn)證佃牛,但不能簽發(fā) JWT淹辞。
在項(xiàng)目下面創(chuàng)建一個(gè)新的目錄,里面可以存儲(chǔ)即將生成的密鑰與公鑰文件俘侠。
cd ~/desktop/jwt-demo
mkdir config
cd config
密鑰
先生成一個(gè)密鑰文件:
ssh-keygen -t rsa -b 2048 -f private.key
公鑰
基于上面生成的密鑰象缀,再去創(chuàng)建一個(gè)對(duì)應(yīng)的公鑰:
openssl rsa -in private.key -pubout -outform PEM -out public.key
簽發(fā) JWT(RS256 算法)
用 RS256 算法簽發(fā) JWT 的時(shí)候,需要從文件系統(tǒng)上讀取創(chuàng)建的密鑰文件里的內(nèi)容爷速。
const fs = require('fs')
// 獲取簽發(fā) JWT 時(shí)需要用的密鑰
const privateKey = fs.readFileSync('./config/private.key')
簽發(fā)仍然使用 jwt.sign 方法央星,只不過在選項(xiàng)參數(shù)里特別說明一下使用的算法是 RS256:
// 簽發(fā) Token
const tokenRS256 = jwt.sign(payload, privateKey, { algorithm: 'RS256' })
// 輸出簽發(fā)的 Token
console.log('RS256 算法:', tokenRS256)
前端與數(shù)據(jù)埋點(diǎn)
作者使用過的埋點(diǎn)方式:
1、使用神策埋點(diǎn)
2惫东、后臺(tái)給接口
3莉给、click和pv埋點(diǎn)
所謂埋點(diǎn)就是在應(yīng)用中特定的流程收集一些信息,用來跟蹤應(yīng)用使用的狀況廉沮,后續(xù)用來進(jìn)一步優(yōu)化產(chǎn)品或是提供運(yùn)營(yíng)的數(shù)據(jù)支撐颓遏,包括訪問數(shù)(Visits),訪客數(shù)(Visitor)滞时,停留時(shí)長(zhǎng)(Time On Site)州泊,頁(yè)面瀏覽數(shù)(Page Views)和跳出率(Bounce Rate)。這樣的信息收集可以大致分為兩種:頁(yè)面統(tǒng)計(jì)(track this virtual page view)漂洋,統(tǒng)計(jì)操作行為(track this button by an event)。
數(shù)據(jù)埋點(diǎn)的方式
現(xiàn)在埋點(diǎn)的主流有兩種方式:
- 第一種:自己公司研發(fā)在產(chǎn)品中注入代碼統(tǒng)計(jì)力喷,并搭建起相應(yīng)的后臺(tái)查詢刽漂。
- 第二種:第三方統(tǒng)計(jì)工具,如友盟弟孟、神策贝咙、Talkingdata、GrowingIO等拂募。
如果是產(chǎn)品早期庭猩,通常會(huì)使用第二種方式來采集數(shù)據(jù)窟她,并直接使用第三方分析工具進(jìn)行基本的分析。而對(duì)于那些對(duì)數(shù)據(jù)安全比較重視蔼水,業(yè)務(wù)又相對(duì)復(fù)雜的公司則通常是使用第一種方式采集數(shù)據(jù)震糖,并搭建相應(yīng)的數(shù)據(jù)產(chǎn)品實(shí)現(xiàn)其數(shù)據(jù)應(yīng)用或是分析的訴求。
關(guān)鍵指標(biāo)
我們先看看無論是APP趴腋,H5還是小程序都會(huì)關(guān)注的指標(biāo)吊说,了解這些指標(biāo)的計(jì)算方法的細(xì)微差異以及復(fù)雜性,換個(gè)角度來思考埋點(diǎn)的意義
-
訪問與訪客
訪問次數(shù)(Visits)與訪問人數(shù)(Vistors)是幾乎所有應(yīng)用都需要統(tǒng)計(jì)的指標(biāo)优炬,這也是最基礎(chǔ)的指標(biāo)颁井。
對(duì)于應(yīng)用的統(tǒng)計(jì)來說,經(jīng)炒阑ぃ看到的DAU雅宾,MAU,UV等指標(biāo)都是指統(tǒng)計(jì)訪客(Vistors)葵硕。訪問(Visits)是指會(huì)話層眉抬,用戶打開應(yīng)用花一段時(shí)間瀏覽又離開,從指標(biāo)定義(訪問次數(shù))來說這被稱之為統(tǒng)計(jì)會(huì)話(Session)數(shù)贬芥。
一次會(huì)話(Session 或 Visit)是打開應(yīng)用的第一個(gè)請(qǐng)求(打開應(yīng)用)和最后一個(gè)請(qǐng)求決定的吐辙。如果用戶打開應(yīng)用然后放下手機(jī)或是離開電腦,并在接下來30分鐘內(nèi)沒有任何動(dòng)作蘸劈,此次會(huì)話自動(dòng)結(jié)束托猩,通常也算作一次訪問或會(huì)話期(30分鐘是早起網(wǎng)頁(yè)版應(yīng)用約定俗成的會(huì)話數(shù)定義,目前用戶停留在應(yīng)用的時(shí)長(zhǎng)變長(zhǎng)抱环,30分鐘的限定也可能隨之不同伤哺,總之是能代表一次用戶訪問的時(shí)長(zhǎng))。
在計(jì)算訪問人數(shù)(Vistors)時(shí)棒掠,埋點(diǎn)上報(bào)的數(shù)據(jù)是盡可能接近真實(shí)訪客的人數(shù)孵构。對(duì)于有需要統(tǒng)計(jì)獨(dú)立訪客這個(gè)指標(biāo)的場(chǎng)景,這里還是需要強(qiáng)調(diào)一下烟很,訪問人數(shù)(Vistors)并不是真實(shí)獨(dú)立的人颈墅,因此收集數(shù)據(jù)時(shí)必須知道訪問人數(shù)雖然能夠很好的反映使用應(yīng)用的真實(shí)訪問者的數(shù)量,但不等于使用應(yīng)用的真實(shí)人數(shù)雾袱。(原因是恤筛,重復(fù)安裝的應(yīng)用,或是手機(jī)參數(shù)被修改都會(huì)使得獨(dú)立訪客的指標(biāo)收到影響芹橡。計(jì)算訪問人數(shù)的埋點(diǎn)都是依賴Cookie毒坛,用戶打開應(yīng)用,應(yīng)用都會(huì)在此人的終端創(chuàng)建一個(gè)獨(dú)立Cookie, Cookie會(huì)被保留,但還是難免會(huì)被用戶手動(dòng)清理或是Cookie被禁用導(dǎo)致同一用戶使用應(yīng)用Cookie不一致煎殷,所以獨(dú)立訪客只能高度接近于使用應(yīng)用的真實(shí)人數(shù)屯伞。)
-
停留時(shí)長(zhǎng)
停留時(shí)長(zhǎng)用來衡量用戶在應(yīng)用的某一個(gè)頁(yè)面或是一次訪問(會(huì)話)所停留的時(shí)間。
頁(yè)面停留時(shí)長(zhǎng)豪直,表示在每個(gè)頁(yè)面所花費(fèi)的時(shí)間劣摇;例如:首頁(yè)就是進(jìn)入首頁(yè)(10:00)到離開首頁(yè)進(jìn)入下一個(gè)頁(yè)面(10:01)的時(shí)長(zhǎng),首頁(yè)停留時(shí)長(zhǎng)計(jì)算為1分鐘顶伞。頁(yè)面A是2分鐘饵撑。停留時(shí)長(zhǎng)的數(shù)據(jù)并不都是一定采集得到的,比如頁(yè)面B進(jìn)入時(shí)間(10:03)唆貌,離開出現(xiàn)異郴耍或是退出時(shí)間沒有記錄,這時(shí)候計(jì)算就是0 (所以指標(biāo)計(jì)算時(shí)需要了解埋點(diǎn)的狀況锨咙,剔除這樣的無效數(shù)據(jù))语卤。
應(yīng)用的停留時(shí)長(zhǎng),表示一次訪問(會(huì)話)所停留的時(shí)間酪刀,計(jì)算起來就是所有頁(yè)面的訪問時(shí)長(zhǎng)粹舵,同樣是上一個(gè)流程,應(yīng)用的停留時(shí)長(zhǎng)就是4分鐘骂倘。
-
跳出率
跳出率的計(jì)算方法現(xiàn)在在各個(gè)公司還是很多種眼滤,最經(jīng)常被使用的是:用戶只訪問了一個(gè)頁(yè)面所占的會(huì)話比例(原因是:假設(shè)這種場(chǎng)景,用戶來了訪問了一個(gè)頁(yè)面就離開了历涝,想想用戶使用的心里畫面應(yīng)該是:打開應(yīng)用诅需,心想什么鬼,然后關(guān)閉應(yīng)用甚至卸載了荧库。這個(gè)場(chǎng)景多可怕堰塌,這也是為什么跳出率指標(biāo)被如此關(guān)注)
跳出率可以分解到兩個(gè)層次:一是整個(gè)應(yīng)用的跳出率,二是重點(diǎn)的著陸頁(yè)的跳出率分衫,甚至是搜索關(guān)鍵詞的跳出率场刑。跳出率的指標(biāo)可操作性非常強(qiáng),通過統(tǒng)計(jì)跳出率可以直接發(fā)現(xiàn)頁(yè)面的問題發(fā)現(xiàn)關(guān)鍵詞的問題蚪战。
-
退出率
退出率是針對(duì)頁(yè)面的牵现,這個(gè)指標(biāo)的目標(biāo)很簡(jiǎn)單,就是在針對(duì)某個(gè)頁(yè)面有多少用戶離開了應(yīng)用邀桑,主要用戶反映用戶從應(yīng)用離開的情況施籍。哪些頁(yè)面需要被改進(jìn)最快的方式被發(fā)掘。(注意:退出率高不一定是壞事概漱。例如:預(yù)測(cè)流程的最終節(jié)點(diǎn)的退出率就應(yīng)該是高的)
-
轉(zhuǎn)化率
我們?cè)诋a(chǎn)品上投入這么多,不就是為了衡量產(chǎn)出么喜喂?所以對(duì)于電商類應(yīng)用瓤摧,還有比轉(zhuǎn)化率更值得關(guān)注的指標(biāo)嗎竿裂?轉(zhuǎn)化率的計(jì)算方法是某種產(chǎn)出除以獨(dú)立訪客或是訪問量,對(duì)于電商產(chǎn)品來說照弥,就是提交訂單用戶數(shù)除以獨(dú)立訪客腻异。
轉(zhuǎn)化率的計(jì)算看起來想到那簡(jiǎn)單,但卻是埋點(diǎn)中最貼近業(yè)務(wù)的數(shù)據(jù)收集这揣。這也是最體現(xiàn)埋點(diǎn)技巧的指標(biāo)悔常,需要結(jié)合業(yè)務(wù)特點(diǎn)制定計(jì)算方法。提交訂單量/訪客數(shù)是最基本的轉(zhuǎn)化率给赞,轉(zhuǎn)化率還可以分層次机打,指定用戶路徑的,如:完成某條路徑的提交訂單數(shù)/訪客數(shù)片迅。
試著找一條路徑残邀,想想轉(zhuǎn)化率的數(shù)據(jù)怎么得來的吧,埋點(diǎn)都收集了什么樣的數(shù)據(jù)吧柑蛇?
-
參與度
參與度并不是一個(gè)指標(biāo)芥挣,而是一系列的指標(biāo)的統(tǒng)稱,例如訪問深度耻台,訪問頻次空免,針對(duì)電商的下單次數(shù),針對(duì)內(nèi)容服務(wù)商的播放次數(shù)盆耽,及用戶行為序列這些都可以是衡量參與度的指標(biāo)蹋砚。之所以把參與度列為一個(gè)指標(biāo),是希望大家明白把指標(biāo)結(jié)合業(yè)務(wù)征字,產(chǎn)生化學(xué)反應(yīng)都弹,活學(xué)活用去發(fā)現(xiàn)事物的本質(zhì)。
埋點(diǎn)的內(nèi)容
看完關(guān)鍵的這些指標(biāo)后匙姜,其實(shí)埋點(diǎn)大致分為兩部分畅厢,一部分是統(tǒng)計(jì)應(yīng)用頁(yè)面訪問情況,即頁(yè)面統(tǒng)計(jì)氮昧,隨頁(yè)面訪問動(dòng)作發(fā)生時(shí)進(jìn)行上報(bào)框杜;另外一部分是統(tǒng)計(jì)應(yīng)用內(nèi)的操作行為,在頁(yè)面中操作時(shí)進(jìn)行上報(bào)(例如:組件曝光時(shí)袖肥,組件點(diǎn)擊時(shí)咪辱,上滑,下滑時(shí))椎组。
為了統(tǒng)計(jì)到所需要的指標(biāo)油狂,應(yīng)用中的所有頁(yè)面,事件都被唯一標(biāo)記,用戶的信息专筷,設(shè)備的信息弱贼,時(shí)間參數(shù)以及符合業(yè)務(wù)需要的參數(shù)具體內(nèi)容被附加上報(bào),就是埋點(diǎn)磷蛹。
關(guān)于埋點(diǎn)的數(shù)據(jù)的注意事項(xiàng)
如何埋點(diǎn)
關(guān)于埋點(diǎn)數(shù)據(jù)有一點(diǎn)至關(guān)重要吮旅,埋點(diǎn)是為了更好地使用數(shù)據(jù),不要試圖得到精準(zhǔn)的數(shù)據(jù)要得到的是高質(zhì)量的埋點(diǎn)數(shù)據(jù)味咳,前面討論跳出率就是這個(gè)例子庇勃,得到能得到的數(shù)據(jù),用不完美的數(shù)據(jù)來達(dá)成下一步的行動(dòng)槽驶,追求的是高質(zhì)量而不是精確责嚷。這是很多數(shù)據(jù)產(chǎn)品容易入坑的地,要經(jīng)常提醒自己捺檬。
- 避免跨域(img 天然支持跨域)
- 利用空白gif或1x1 px的img是互聯(lián)網(wǎng)廣告或網(wǎng)站監(jiān)測(cè)方面常用的手段再层,簡(jiǎn)單、安全堡纬、相比PNG/JPG體積小聂受,1px 透明圖,對(duì)網(wǎng)頁(yè)內(nèi)容的影響幾乎沒有影響烤镐,這種請(qǐng)求用在很多地方蛋济,比如瀏覽、點(diǎn)擊炮叶、熱點(diǎn)碗旅、心跳、ID頒發(fā)等等镜悉,
- 圖片請(qǐng)求不占用 Ajax 請(qǐng)求限額
- 不會(huì)阻塞頁(yè)面加載祟辟,影響用戶的體驗(yàn),只要new Image對(duì)象就好了侣肄,一般情況下也不需要append到DOM中旧困,通過它的onerror和onload事件來檢測(cè)發(fā)送狀態(tài)。