## 前言
http緩存對我們開發項目以及對項目針對http方向做優化有著比較重要的作用,作為常識我們需要知道http的緩存機制,以及如何利用它的緩存機制提高我們的應用體驗。

針對瀏覽器的http緩存的分析也算是老生常談了,每隔一段時間就會冒出一篇不錯的文章,其原理也是各大公司面試時幾乎必考的問題。
之所以還寫一篇這樣的文章,是因為近期都在搞新技術,想“回歸”下基礎,也希望盡量總結的更詳盡些。
那么你是否還需要閱讀本篇文章呢?可以試著回答下面這個問題:
我們在訪問百度首頁的時候,會發現不管怎么刷新頁面,靜態資源基本都是返回 200(from cache):
隨便點開一個靜態資源是醬的:
哎喲有Response報頭數據呢,看來服務器也正常返回了etag什么鬼的應有盡有,那狀態200不是應該對應的非緩存狀態么?要from cache的話不是應該返回304才合理么?
難道是度娘的服務器故障了嗎?如果你知道答案,那就可以忽略本文了。
1 Http報文中與緩存相關的首部字段
我們先來瞅一眼RFC2616規定的47種http報文首部字段中與緩存相關的字段,事先了解一下能讓咱在心里有個底:
1. 通用首部字段(就是請求報文和響應報文都能用上的字段)
2. 請求首部字段
3. 響應首部字段
4. 實體首部字段
后續大體也會依次介紹它們。
2 場景模擬
為方便模擬各種緩存效果,我們建個非常簡單的場景。
1. 頁面文件
我們建個非常簡單的html頁面,上面只有一個本地樣式文件和圖片:
XHTML
<!DOCTYPE html>
<html>
<head>
<title>緩存測試</title>
<link rel="stylesheet" href="css/reset.css">
</head>
<body>
<h1>哥只是一個標題</h1>
<p><img src="img/dog.jpg" /></p>
</body>
</html>
2. 首部字段修改
有時候一些瀏覽器會自行給請求首部加上一些字段(如chrome使用F5會強制加上“cache-control:max-age=0”),會覆蓋掉一些字段(比如pragma)的功能;另外有時候我們希望服務器能多/少返回一些響應字段。
這種情況我們就希望可以手動來修改請求或響應報文上的內容了。那么如何實現呢?這里我們使用Fiddler來完成任務。
在Fiddler中我們可以通過“bpu XXX”指令來攔截指定請求,然后手動修改請求內容再發給服務器、修改響應內容再發給客戶端。
以我們的example為例,頁面文件走nginx通過 http://localhost/ 可直接訪問,所以我們直接執行“bpu localhost”攔截所有地址中帶有該字樣的請求:
點擊被攔截的請求,可以在右欄直接修改報文內容(上半區域是請求報文,下半區域是響應報文),點擊黃色的“Break on Response”按鈕可以執行下一步(把請求發給服務器),點擊紅色的按鈕“Run to Completion”可以直接完成整個請求過程:
通過這個方法我們可以很輕松地模擬出各種http緩存場景。
3. 瀏覽器的強制策略
如上述,當下大多數瀏覽器在點擊刷新按鈕或按F5時會自行加上“Cache-Control:max-age=0”請求字段,所以我們先約定成俗——后文提及的“刷新”多指的是選中url地址欄并按回車鍵(這樣不會被強行加上Cache-Control)。
事實上有的瀏覽器還有一些更奇怪的行為,在后續我們回答文章開頭問題的時候會提到。
3 石器時代的緩存方式
在 http1.0 時代,給客戶端設定緩存方式可通過兩個字段——“Pragma”和“Expires”來規范。雖然這兩個字段早可拋棄,但為了做http協議的向下兼容,你還是可以看到很多網站依舊會帶上這兩個字段。
1. Pragma
當該字段值為“no-cache”的時候(事實上現在RFC中也僅標明該可選值),會知會客戶端不要對該資源讀緩存,即每次都得向服務器發一次請求才行。
Pragma屬于通用首部字段,在客戶端上使用時,常規要求我們往html上加上這段meta元標簽(而且可能還得做些hack放到body后面去):
<meta http-equiv="Pragma" content="no-cache">
它告訴瀏覽器每次請求頁面時都不要讀緩存,都得往服務器發一次請求才行。BUT!!!事實上這種禁用緩存的形式用處很有限:
僅有IE才能識別這段meta標簽含義,其它主流瀏覽器僅能識別“Cache-Control: no-store”的meta標簽(見出處)。
在IE中識別到該meta標簽含義,并不一定會在請求字段加上Pragma,但的確會讓當前頁面每次都發新請求(僅限頁面,頁面上的資源則不受影響)。
做了測試后發現也的確如此,這種客戶端定義Pragma的形式基本沒起到多少作用。不過如果是在響應報文上加上該字段就不一樣了:
如上圖紅框部分是再次刷新頁面時生成的請求,這說明禁用緩存生效,預計瀏覽器在收到服務器的Pragma字段后會對資源進行標記,禁用其緩存行為,進而后續每次刷新頁面均能重新發出請求而不走緩存。
2. Expires
有了Pragma來禁用緩存,自然也需要有個東西來啟用緩存和定義緩存時間,對http1.0而言,Expires就是做這件事的首部字段。
Expires的值對應一個GMT(格林尼治時間),比如“Mon, 22 Jul 2002 11:12:01 GMT”來告訴瀏覽器資源緩存過期時間,如果還沒過該時間點則不發請求。
在客戶端我們同樣可以使用meta標簽來知會IE(也僅有IE能識別)頁面(同樣也只對頁面有效,對頁面上的資源無效)緩存時間:
<meta http-equiv="expires" content="mon, 18 apr 2016 14:30:00 GMT">
如果希望在IE下頁面不走緩存,希望每次刷新頁面都能發新請求,那么可以把“content”里的值寫為“-1”或“0”。
注意的是該方式僅僅作為知會IE緩存時間的標記,你并不能在請求或響應報文中找到Expires字段。
如果是在服務端報頭返回Expires字段,則在任何瀏覽器中都能正確設置資源緩存的時間:
在上圖里,緩存時間設置為一個已過期的時間點(見紅框),則刷新頁面將重新發送請求(見藍框)。
那么如果Pragma和Expires一起上陣的話,聽誰的?我們試一試就知道了:
我們通過Pragma禁用緩存,又給Expires定義一個還未到期的時間(紅框),刷新頁面時發現均發起了新請求(藍框),這意味著Pragma字段的優先級會更高。
BUT,響應報文中Expires所定義的緩存時間是相對服務器上的時間而言的,如果客戶端上的時間跟服務器上的時間不一致(特別是用戶修改了自己電腦的系統時間),那緩存時間可能就沒啥意義了。
4 Cache-Control
針對上述的“Expires時間是相對服務器而言,無法保證和客戶端時間統一”的問題,http1.1新增了 Cache-Control 來定義緩存過期時間,若報文中同時出現了 Pragma、Expires 和 Cache-Control,會以 Cache-Control 為準。
Cache-Control也是一個通用首部字段,這意味著它能分別在請求報文和響應報文中使用。在RFC中規范了 Cache-Control 的格式為:
"Cache-Control" ":" cache-directive
作為請求首部時,cache-directive 的可選值有:
作為響應首部時,cache-directive 的可選值有:
我們依舊可以在HTML頁面加上meta標簽來給請求報頭加上 Cache-Control 字段:
另外 Cache-Control 允許自由組合可選值,例如:
Cache-Control: max-age=3600, must-revalidate
它意味著該資源是從原服務器上取得的,且其緩存(新鮮度)的有效時間為一小時,在后續一小時內,用戶重新訪問該資源則無須發送請求。
當然這種組合的方式也會有些限制,比如 no-cache 就不能和 max-age、min-fresh、max-stale 一起搭配使用。
組合的形式還能做一些瀏覽器行為不一致的兼容處理。例如在IE我們可以使用 no-cache 來防止點擊“后退”按鈕時頁面資源從緩存加載,但在 Firefox 中,需要使用 no-store 才能防止歷史回退時瀏覽器不從緩存中去讀取數據,故我們在響應報頭加上如下組合值即可做兼容處理:
Cache-Control: no-cache, no-store
5 緩存校驗字段
上述的首部字段均能讓客戶端決定是否向服務器發送請求,比如設置的緩存時間未過期,那么自然直接從本地緩存取數據即可(在chrome下表現為200 from cache),若緩存時間過期了或資源不該直接走緩存,則會發請求到服務器去。
我們現在要說的問題是,如果客戶端向服務器發了請求,那么是否意味著一定要讀取回該資源的整個實體內容呢?
我們試著這么想——客戶端上某個資源保存的緩存時間過期了,但這時候其實服務器并沒有更新過這個資源,如果這個資源數據量很大,客戶端要求服務器再把這個東西重新發一遍過來,是否非常浪費帶寬和時間呢?
答案是肯定的,那么是否有辦法讓服務器知道客戶端現在存有的緩存文件,其實跟自己所有的文件是一致的,然后直接告訴客戶端說“這東西你直接用緩存里的就可以了,我這邊沒更新過呢,就不再傳一次過去了”。
為了讓客戶端與服務器之間能實現緩存文件是否更新的驗證、提升緩存的復用率,Http1.1新增了幾個首部字段來做這件事情。
1. Last-Modified
服務器將資源傳遞給客戶端時,會將資源最后更改的時間以“Last-Modified: GMT”的形式加在實體首部上一起返回給客戶端。
客戶端會為資源標記上該信息,下次再次請求時,會把該信息附帶在請求報文中一并帶給服務器去做檢查,若傳遞的時間值與服務器上該資源最終修改時間是一致的,則說明該資源沒有被修改過,直接返回304狀態碼即可。
至于傳遞標記起來的最終修改時間的請求報文首部字段一共有兩個:
⑴ If-Modified-Since: Last-Modified-value
示例為 If-Modified-Since: Thu, 31 Mar 2016 07:07:52 GMT
該請求首部告訴服務器如果客戶端傳來的最后修改時間與服務器上的一致,則直接回送304 和響應報頭即可。
當前各瀏覽器均是使用的該請求首部來向服務器傳遞保存的 Last-Modified 值。
⑵ If-Unmodified-Since: Last-Modified-value
告訴服務器,若Last-Modified沒有匹配上(資源在服務端的最后更新時間改變了),則應當返回412(Precondition Failed) 狀態碼給客戶端。
當遇到下面情況時,If-Unmodified-Since 字段會被忽略:
Last-Modified值對上了(資源在服務端沒有新的修改);
服務端需返回2XX和412之外的狀態碼;
傳來的指定日期不合法
Last-Modified 說好卻也不是特別好,因為如果在服務器上,一個資源被修改了,但其實際內容根本沒發送改變,會因為Last-Modified時間匹配不上而返回了整個實體給客戶端(即使客戶端緩存里有個一模一樣的資源)。
2. ETag
為了解決上述Last-Modified可能存在的不準確的問題,Http1.1還推出了 ETag 實體首部字段。
服務器會通過某種算法,給資源計算得出一個唯一標志符(比如md5標志),在把資源響應給客戶端的時候,會在實體首部加上“ETag: 唯一標識符”一起返回給客戶端。
客戶端會保留該 ETag 字段,并在下一次請求時將其一并帶過去給服務器。服務器只需要比較客戶端傳來的ETag跟自己服務器上該資源的ETag是否一致,就能很好地判斷資源相對客戶端而言是否被修改過了。
如果服務器發現ETag匹配不上,那么直接以常規GET 200回包形式將新的資源(當然也包括了新的ETag)發給客戶端;如果ETag是一致的,則直接返回304知會客戶端直接使用本地緩存即可。
那么客戶端是如何把標記在資源上的 ETag 傳去給服務器的呢?請求報文中有兩個首部字段可以帶上 ETag 值:
⑴ If-None-Match: ETag-value
示例為 If-None-Match: "56fcccc8-1699"
告訴服務端如果 ETag 沒匹配上需要重發資源數據,否則直接回送304 和響應報頭即可。
當前各瀏覽器均是使用的該請求首部來向服務器傳遞保存的 ETag 值。
⑵ If-Match: ETag-value
告訴服務器如果沒有匹配到ETag,或者收到了“*”值而當前并沒有該資源實體,則應當返回412(Precondition Failed) 狀態碼給客戶端。否則服務器直接忽略該字段。
If-Match 的一個應用場景是,客戶端走PUT方法向服務端請求上傳/更替資源,這時候可以通過 If-Match 傳遞資源的ETag。
需要注意的是,如果資源是走分布式服務器(比如CDN)存儲的情況,需要這些服務器上計算ETag唯一值的算法保持一致,才不會導致明明同一個文件,在服務器A和服務器B上生成的ETag卻不一樣。
如果 Last-Modified 和 ETag 同時被使用,則要求它們的驗證都必須通過才會返回304,若其中某個驗證沒通過,則服務器會按常規返回資源實體及200狀態碼。
在較新的 nginx 上默認是同時開啟了這兩個功能的:
上圖的前三條請求是原始請求,接著的三條請求是刷新頁面后的新請求,在發新請求之前我們修改了 reset.css 文件,所以它的 Last-Modified 和 ETag 均發生了改變,服務器因此返回了新的文件給客戶端(狀態值為200)。
而 dog.jpg 我們沒有做修改,其Last-Modified 和 ETag在服務端是保持不變的,故服務器直接返回了304狀態碼讓客戶端直接使用緩存的 dog.jpg 即可,沒有把實體內容返回給客戶端(因為沒必要)。
6 緩存實踐
當我們在一個項目上做http緩存的應用時,我們還是會把上述提及的大多數首部字段均使用上,例如使用 Expires 來兼容舊的瀏覽器,使用 Cache-Control 來更精準地利用緩存,然后開啟 ETag 跟 Last-Modified 功能進一步復用緩存減少流量。
那么這里會有一個小問題——Expires 和 Cache-Control 的值應設置為多少合適呢?
答案是不會有過于精準的值,均需要進行按需評估。
例如頁面鏈接的請求常規是無須做長時間緩存的,從而保證回退到頁面時能重新發出請求,百度首頁是用的 Cache-Control:private,騰訊首頁則是設定了60秒的緩存,即 Cache-Control:max-age=60。
而靜態資源部分,特別是圖片資源,通常會設定一個較長的緩存時間,而且這個時間最好是可以在客戶端靈活修改的。以騰訊的某張圖片為例:
http://i.gtimg.cn/vipstyle/vipportal/v4/img/common/logo.png?max_age=2592000
客戶端可以通過給圖片加上“max_age”的參數來定義服務器返回的緩存時間:
當然這需要有一個前提——靜態資源能確保長時間不做改動。如果一個腳本文件響應給客戶端并做了長時間的緩存,而服務端在近期修改了該文件的話,緩存了此腳本的客戶端將無法及時獲得新的數據。
解決該困擾的辦法也簡單——把服務側ETag的那一套也搬到前端來用——頁面的靜態資源以版本形式發布,常用的方法是在文件名或參數帶上一串md5或時間標記符:
https://#/hm.js?e23800c454aa573c0ccb16b52665ac26
http://tb1.bdstatic.com/tb/_/tbean_safe_ajax_94e7ca2.js
http://img1.gtimg.com/ninja/2/2016/04/ninja145972803357449.jpg
如果文件被修改了,才更改其標記符內容,這樣能確保客戶端能及時從服務器收取到新修改的文件。
7 關于開頭的問題
現在回過頭來看文章開頭的問題,可能會覺得答案很容易回答出來。
百度首頁的資源在刷新后實際沒有發送任何請求,因為 Cache-Control 定義的緩存時間段還沒到期。在Chrome中即使沒發送請求,但只要從本地的緩存中取,都會在Network面板顯示一條狀態為200且注明“from cache”的偽請求,其Response內容只是上一次回包留下的數據。
然而這并不是問題的全部答案,我們前面提到過,在Chrome中如果點擊“刷新”按鈕,Chrome會強制給所有資源加上“Cache-Control: max-age=0”的請求首部并向服務器發送驗證請求的,而在文章開頭的動圖中,我們的確點擊了“刷新”按鈕,卻不見瀏覽器發去新請求(并返回304)。
關于這個問題其實在組內跟小伙伴們討論過,通過Fiddler抓包發現,如果關閉Chrome的開發者面板再點擊“刷新”按鈕,瀏覽器是會按預期發送驗證請求且接收返回的304響應的,另外這個奇怪的情況在不同的網站甚至不同的電腦下出現頻率都不一致,所以暫時將其歸咎于瀏覽器的怪異反應。
那么有這么一個問題——是否有辦法在瀏覽器點擊“刷新”按鈕的時候不讓瀏覽器去發新的驗證請求呢?
辦法還是有的,就是不怎么實用——在頁面加載完畢后通過腳本動態地添加資源:
$(window).load(function() {
var bg='http://img.infinitynewtab.com/wallpaper/100.jpg';
setTimeout(function() { //setTimeout是必須的
$('#bgOut').css('background-image', 'url('+bg+')');
},0);
});
8 其它相關的首部字段
事實上較常用和重要的緩存相關字段我們都介紹完了,這里順帶講講幾個跟緩存有關系,但沒那么主要的響應首部字段。
1. Vary
“vary”本身是“變化”的意思,而在http報文中更趨于是“vary from”(與。。。不同)的含義,它表示服務端會以什么基準字段來區分、篩選緩存版本。
我們先考慮這么一個問題——在服務端有著這么一個地址,如果是IE用戶則返回針對IE開發的內容,否則返回另一個主流瀏覽器版本的內容。這很簡單,服務端獲取到請求的 User-Agent 字段做處理即可。但是用戶請求的是代理服務器而非原服務器,且代理服務器如果直接把緩存的IE版本資源發給了非IE的客戶端,這就出問題了。
因此 Vary 便是著手處理該問題的首部字段,我們可以在響應報文加上:
Vary: User-Agent
便能知會代理服務器需要以 User-Agent 這個請求首部字段來區別緩存版本,防止傳遞給客戶端的緩存不正確。
Vary 也接受條件組合的形式:
Vary: User-Agent, Accept-Encoding
這意味著服務器應以 User-Agent 和 Accept-Encoding 兩個請求首部字段來區分緩存版本。
2. Date 和 Age
HTTP并沒有提供某種方法來幫用戶區分其收到的資源是否命中了代理服務器的緩存,但在客戶端我們可以通過計算響應報文中的 Date 和 Age 字段來得到答案。
Date 理所當然是原服務器發送該資源響應報文的時間(GMT格式),如果你發現 Date 的時間與“當前時間”差別較大,或者連續F5刷新發現 Date 的值都沒變化,則說明你當前請求是命中了代理服務器的緩存。
上述的“當前時間”自然是相對于原服務器而言的時間,那么如何獲悉原服務器的當前時間呢?
常規從頁面地址請求的響應報文中可獲得,以博客園首頁為例:
每次你刷新頁面,瀏覽器都會重新發出這條url的請求,你會發現其 Date 值是不斷變化的,這說明該鏈接沒有命中緩存,都是從原服務器返回過來的數據。
因此我們可以拿頁面上其它靜態資源請求回包中的 Date 與其進行對比,若靜態資源的 Date 早于原服務端時間,則說明命中了代理服務器緩存。
通常還滿足這么個條件:
靜態資源Age + 靜態資源Date = 原服務端Date
這里的 Age 也是響應報文中的首部字段,它表示該文件在代理服務器中存在的時間(秒),如文件被修改或替換,Age會重新由0開始累計。
我們在上面那張博客園首頁報文截圖的同個場景下,看看某個文件(jQuery.js)命中代理服務器緩存的回包數據:
會發現它滿足我們上述的規則:
//return true
new Date('Mon, 04 Apr 2016 07:03:17 GMT')/1000 == new Date('Sat, 19 Dec 2015 01:29:14 GMT')/1000 + 9264843
不過這條規則也不一定準確,特別是當原服務器經常修改系統時間的情況下。
關于http緩存原理的知識就整理到這,希望能讓你有所收獲,共勉~
## 參考文檔
* [http緩存機制](http://mp.weixin.qq.com/s/W2ybb7-r1ZKW6B9mI6ITzw)
- 前端工程化
- 架構總綱
- 001
- 美團技術架構
- 前端工程化說明
- 歷史背景說明
- 架構說明
- 前端工程化技術棧
- 技術文檔說明
- 功能模塊說明
- 前端模塊管理器簡介
- 框架對比分析
- vue&react&ng對比分析(一)
- vue&react&ng對比分析(二)
- vue&react&ng對比分析(三)
- 工程化專題系列
- 需要解決的問題
- 001
- 002
- 003
- 常見代碼錯誤
- jslint中常見的錯誤
- css規范常見錯誤
- html規范常見錯誤
- 工程化目錄
- 工程化初始化
- 項目構建流程
- 項目打包優化
- 上線與迭代注意事項
- 前端部署發布
- jetkins部署
- 部署需求整理
- 前端監控
- 工程化實踐指南
- dock持續部署
- 系列文章
- 插拔式前端的設計
- 其他實踐
- 工程化的前端管理
- 宋小菜借鑒
- 大前端團隊介紹
- 人員組成
- 人員發展
- 研發流程
- 任務分類
- 前端基礎建設與架構
- 技術棧以及技術方案
- 業務目錄大綱
- 前端大綱
- api管理
- 后端api工具
- 前端easymock
- api攔截與代理
- api優化
- api請求時長策略設計
- 前端架構專題
- 架構專題一
- 產品原型對接
- 與ui對接
- 圖片專題
- 圖片工程化大綱
- 圖片優化
- 圖標字體
- 圖標字體vs雪碧圖
- 工程化的前端矩陣
- 螞蟻金服前端矩陣分享
- BFF架構
- 概念解析
- 前端腳手架
- 初始化項目
- 個性化配置
- 部署與發布
- 性能優化專題
- http專題
- https常識
- http優化1
- http優化2
- http優化3
- http緩存
- 常規web性能優化攻略
- 性能優化大綱
- 樣式優化
- js優化
- 第三方依賴優化
- 代碼分割優化
- 圖片優化
- 打包優化
- 服務器優化
- 緩存優化
- 交互優化
- pc事件優化
- 手機事件優化
- 推薦文章
- 01
- 前端安全專題
- 前端安全大綱
- 前端第三方庫
- seo優化
- web框架的對比
- 001
- 學習資源
- 珠峰前端架構
- npm教程
- npm入門
- cnpm入門
- cnpm搭建
- 你該知道的js模塊
- browserSync
- opn
- js-cookie
- npm-script進階
- 入門篇
- 進階篇
- 高階篇
- 實踐篇
- yarn入門
- nodejs教程
- axios&&fetch
- xhr
- axios
- fetch
- babel專題
- babel入門
- profill入門
- nodejs入門
- 快速入門
- 大綱介紹
- node基礎
- global obj
- assert斷言
- procss-進程
- child_process子進程
- cluster集群
- console控制臺
- crypto-加密
- dgram-數據報
- dns-域名服務器
- error-異常
- events-事件
- global-全局變量
- http-基本協議
- https-安全協議
- modules-模塊
- os-操作系統
- path-路徑
- querystring-查詢字符串
- readline-逐行讀取
- fs-文件系統
- net-網絡操作
- 命令行工具
- 內存泄露
- 代碼的組織與部署
- 異步編程
- orm模塊
- 異步編程解決方案
- node-lessons
- 環境準備
- nodejs實踐
- 項目搭建
- 異步優化
- 創建web和tcp服務器
- 終端問答程序
- 爬蟲系統
- mongleDb
- mongoDB簡介
- 基本使用
- 實用技巧
- 匯總001
- 餓了么后臺搭建
- nodejs干貨
- 滬江基于node的實踐
- 蘇寧基于nodejs優化
- 基于nodejs開發腳手架
- 書籍干貨
- 深入淺出nodejs
- 異步I/O(一)
- gulp教程
- gulp入門
- gulp常用插件(1)
- gulp常用插件(2)
- gulp創建目錄
- 經驗普及貼
- webpack教程
- webpack入門
- 簡單入門
- entry配置
- output配置
- 插件使用01
- 插件使用02
- loader使用
- dev-server介紹
- 構建css
- css模塊化
- 使用less和sass
- 構建圖片
- 引入字體
- babel配置攻略
- eslint
- 001
- webpack進階
- 分不同文件檢出
- 優化打包大小
- 優化打包速度
- 自定義配置
- 單頁以及多頁如何配置
- 優化實踐
- 文章導讀
- 001
- 優化指南
- 參考列表
- webpack4
- 多入口程序構建
- 參考教程
- 項目實踐
- 環境區分
- 常見問題
- 解讀webpack
- 從vuejs權威指南中解決
- 深入淺出webpack
- rollup
- 入門
- parcel
- 入門篇
- express教程
- nuxt教程
- 入門
- 基本入門
- koa教程
- koa基本入門
- koa開發注意事項
- koa實踐指南
- 關于路由
- koa優化指南
- 001
- Vuejs
- vuejs入門系列
- vue-cli入門
- vue2基本認識
- vuejs入門教程
- 樣式綁定
- vuex入門學習筆記
- vue組件生命周期
- 組件的使用
- vue-router入門
- vue-filter
- 計算屬性使用
- 開發注意事項
- mixins
- 組件通訊
- vuejs進階
- 進階資源
- router進階
- 官網介紹
- 前進與后退優化
- keep-alive基本使用
- keep-alive原理詳解
- 鉤子函數進階
- 計算屬性&監聽&方法
- vue服務端渲染技術
- 項目實踐之路
- 實踐大綱
- 插槽專題篇
- vue-cli升級
- 進階入門
- vuejs架構
- nuxt
- vuejs項目實踐
- vue實踐常見問題
- 001
- 002
- 003
- 004
- 005
- 改造api參數探索
- 007
- 008
- 009
- 010
- 項目技術棧
- vue性能問題以及優化方案
- vue-spa應用的理解
- vue-ssr的部署與使用
- 滴滴出行實踐案例
- 2.0重構
- vue-element-admin實踐
- 準備工作
- 菜單設計
- 權限設計
- 依賴模塊
- vue-betterScroll
- 性能優化懶加載
- 京東組件實踐
- vue2項目小結
- vue探索與實踐
- 去哪實踐
- 介紹
- 餓了么項目實踐
- 項目解析
- vue骨架屏實踐
- vue生態推薦
- ui框架
- elementUI
- 001
- 002
- VUE-material
- vant-ui
- 解讀入門
- iview
- 使用問題匯總
- vux
- mint-ui
- loadmore
- vue資源導航
- vueconf
- 源碼解讀
- vm
- 雙向綁定
- 基本原理
- 數組雙向綁定
- 報錯機制
- 封裝方法
- 運行環境
- 入門
- 指令
- vue-router解讀
- util
- vue-props
- 流程邏輯
- 推薦文章
- 源碼解讀
- 文章導讀
- 001
- vuejs實戰
- 基礎篇
- 進階篇
- 實踐篇
- 面試專題
- angularjs教程
- angularjs入門系列
- 基本入門
- ng2入門
- ng進階
- ng項目實踐
- 源碼解讀
- typescript
- reactjs教程
- reactjs入門系列
- react的基本入門
- react組件
- virtalDom認識
- react-cli入門
- react組件的生命周期
- 基本知識點
- react-router教程
- react進階
- 基本實踐
- react加載性能優化指南
- react屬性封裝
- 進階45講
- 01概述
- 02jsx
- 06高階組件&函數子組件
- contextApi
- react-router
- 入門章節
- 進階
- 高階組件
- react進階組件
- 基本介紹
- render props
- render props的封裝
- render props getter
- react-native入門
- 源碼解讀
- 001
- 002-reactDemo
- 參考教程
- 參考教程1
- 了解react-hooks
- ui框架
- pc端ui框架推薦
- 項目實踐
- weatherApp
- 001
- 002
- 不同生命周期使用場景
- react項目結構和組件的命名
- 常見問題解答
- 參考書籍
- react全棧
- 前言
- react與redux進階
- 常見誤解
- 反模式
- react設計模式與最佳實踐
- 7美化組件
- 7.2行內樣式
- 7.4css模塊
- 深入react技術棧
- react學習手冊
- 序
- mobx教程
- 入門
- 大佬推薦
- 001
- react面試
- 001
- linux教程
- linux入門
- 基本入門
- 文件管理
- 文件傳輸
- 文檔編輯
- 磁盤管理
- 磁盤維護
- 網絡通訊
- 系統管理
- 系統設置
- 備份壓縮
- 設備管理
- 查看系統信息
- linux其他
- webhook
- rsync入門教程
- ssh免登陸設置
- 安裝nodejs
- nginx教程
- 入門教程
- 安裝
- 基本配置
- 服務基本使用
- 高性能nginx
- 001
- pm2教程
- shell教程
- 入門大綱
- echo命令
- 參考文獻
- linux常用命令2
- linux常見問題
- 001
- python
- 入門教程
- 機器學習
- 準備工作
- 服務器常識
- tomcat
- 入門常識
- iis
- redis教程
- 入門第一篇
- redis進階
- 項目實踐
- redis使用問題
- mongleDB
- 入門
- 使用進階
- 項目實踐
- 常見問題
- electron
- 入門系列
- 前言
- 小程序
- 入門
- 準備工作
- 路由
- 參考文檔
- 001
- 小程序開發--雙路視頻調研
- 準備工作
- 參考資源
- 參考網址
- docker
- 入門
- 基本認識
- 安裝與使用
- docker安裝nginx
- docker安裝jetkins(1)
- docker部署jenkins(2)
- 進階
- 實踐總結
- docker群分享
- docker部署前端應用
- 文章導讀
- docker其他
- 網絡安全
- 入門
- 大綱
- 項目解析
- schoolpal.web
- 功能模塊大綱
- 目錄結構大綱
- 前端國際化
- 國際化方案
- 其他
- bower入門教程
- weex
- 入門
- memcached
- 入門
- sails
- 入門