一款Facebook出品的JS框架--使用React.js和應(yīng)用緩存構(gòu)建快速同步應(yīng)用程序
對大部分應(yīng)用系統(tǒng)來說,在某種程度上,應(yīng)用程序的快速加載和及時(shí)取得***數(shù)據(jù)兩個(gè)方面同樣重要。傾向于積極使用緩存數(shù)據(jù),可能會(huì)導(dǎo)致提供的數(shù)據(jù)陳舊;而傾向于及時(shí)獲取***數(shù)據(jù),可能會(huì)犧牲加載時(shí)間。當(dāng)然,也可以魚與熊掌兼得,但是可能會(huì)需要更多的硬件,更復(fù)雜的軟件,或兩者都需要(意味著一個(gè)字:錢)。
如何權(quán)衡取決于特定的應(yīng)用系統(tǒng)和業(yè)務(wù)要求,本文就是我們的團(tuán)隊(duì)使用React.js和應(yīng)用緩存來解決這一問題的一個(gè)實(shí)例。
我們從哪里開始
標(biāo)簽是每當(dāng)你在瀏覽器上打開一個(gè)標(biāo)簽去送出一份慈善捐助的好理由。這是一件很偉大的事——但事實(shí)上,我們僅僅點(diǎn)擊了一個(gè)價(jià)值 100,000美元的里程碑來完成慈善捐助——但是,我們有一個(gè)疑問。
我們的應(yīng)用也太慢了。大家都明白這點(diǎn)。當(dāng)用戶更換新的標(biāo)簽頁時(shí),他們需要得是速度與連貫性。而且,我們也沒有宣布:載入頁面的延遲成為了人們關(guān)閉標(biāo)簽的***理由。
我們想讓我們的頁面除了更有用,還要更好地被接受。但隨著我們向頁面中加了些附加功能后, 我們的頁面載入問題也越來越突出了。因?yàn)槿藗冃枰覀兊?APP 能快速地提供內(nèi)容信息。
我們正在用 Django 的模板系統(tǒng)做一個(gè)交互式服務(wù)器來召喚或服務(wù)一個(gè)頁面。當(dāng)使用者是在快速的網(wǎng)絡(luò)環(huán)境中,而且我們的服務(wù)狀態(tài)是健康的情況下,服務(wù)器響應(yīng)時(shí)間是 ~65毫秒,還不是比較慘。然而,如果在你父母的房子*里打開一個(gè)標(biāo)簽,或者我們的數(shù)據(jù)庫產(chǎn)生了一個(gè)短暫的停頓時(shí),這可能會(huì)給你在對其的信任上,潑了一盆冷水。
比較讓人煩惱,我應(yīng)該承認(rèn)我們所建立的 APP 并沒有采用標(biāo)準(zhǔn)的前端框架,除了僅僅是使用了 JQuery。 考慮到我們的 APP 有太多的互動(dòng),而且太混亂了。在各種各樣的代碼類型上,我要怎么才能喜歡它。
我們需要去修改它。
* 我愛你們,老媽、老爸!時(shí)代華納有線電視, 沒有太多什么了。
明確我們的需求
當(dāng)準(zhǔn)備去處理這個(gè)問題時(shí),我們必須決定優(yōu)先處理哪些以及放棄哪此需求。在這里我們提出了一些建議:
-
頁面必須能快速載入。這是沒得討價(jià)還價(jià)的。
-
我們的頁面必須是非本地 URL。我們提高了 VIA 捐助廣告的價(jià)格,網(wǎng)絡(luò)在線廣告需要去核識(shí)真實(shí)性來確保這些廣告是夠安全的。由于瀏覽器端的用戶頁面插件總是將我們的廣告移除,以至于網(wǎng)絡(luò)廣告只能使用 http 或 https 協(xié)議。
-
我們希望頁面中的內(nèi)容是***的,但不必是實(shí)時(shí)性的。我們通過設(shè)備對用戶數(shù)據(jù)進(jìn)行同步, 并保持***的體驗(yàn)。我們以分頁的形式顯示出用戶的反饋;例如,我們顯示出新用戶的統(tǒng)計(jì)數(shù)據(jù);我們有時(shí)也要運(yùn)行捐助設(shè)備來以滾動(dòng)條的形式顯示出 "募集資金" 量。雖然我們愿意去接收一定程度上稍舊的數(shù)據(jù)(就像頁面展示后才提交數(shù)據(jù)),但理想得是在提交數(shù)據(jù)的瞬間發(fā)生。
-
我們要減少前端混亂的代碼。將非優(yōu)先權(quán)***的代碼肅清,這是一件讓人興奮的事。
讓我們動(dòng)起手來實(shí)踐關(guān)于處理這些問題的思路。
一、采用主動(dòng)的服務(wù)器端緩存
我們一度認(rèn)為應(yīng)該增加首先擴(kuò)展服務(wù)器端緩存結(jié)構(gòu),目前我們已經(jīng)十分依賴Django的低級(jí)緩存(low-level cache),它有助于達(dá)到我們的目標(biāo),但是我們不得不在每次都要寫語句來判斷是否存在緩存到期或失效情況,我想這張摘自一場精彩演講(an excellent presentation)的幻燈片能夠反映出Django在緩存問題上面臨的挑戰(zhàn):
此外,為了更好地從服務(wù)器端緩存中獲益,我們的緩存系統(tǒng)看起來是一個(gè)多層次的結(jié)構(gòu):(先是)每個(gè)用戶完整的頁面緩存,然后是用戶數(shù)據(jù)的模塊化的緩存,(同時(shí))每當(dāng)數(shù)據(jù)變化時(shí)還要智能判斷數(shù)據(jù)是否失效。因?yàn)樵趯?shí)施過程中已經(jīng)遭遇到一些與緩存相關(guān)的(系統(tǒng))錯(cuò)誤(bugs),所以我們并不希望繼續(xù)增加了緩存系統(tǒng)的復(fù)雜性。
更重要的是,還存在網(wǎng)絡(luò)傳遞差異的問題,例如對一個(gè)新的TAB頁面來說,在快速和慢速的因特網(wǎng)網(wǎng)絡(luò)上的表現(xiàn)有著顯著的差異,即使將我們的服務(wù)響應(yīng)時(shí)間降低到小于1毫秒,對大部分用戶而言,這個(gè)頁面顯示的還是不夠快的。
不,這樣可不行。
二: 在我們的頁面上使用應(yīng)用緩存
"應(yīng)用緩存? 他不是個(gè)douchebag么?"
不,別這么粗魯!
… 好吧, 也許他是有點(diǎn)兒. 在使用應(yīng)用緩存之前,充分了解它的怪癖和陷阱是明智的.我們主要關(guān)心的是應(yīng)用緩存會(huì)降低我們在調(diào)試時(shí)的透明度,因?yàn)槲覀兎?wù)器在輕便的請求上沒有日志 (接下來我們將解決這個(gè)問題). 在代碼變更后的另一個(gè)與之前不同的小問題是,在兩個(gè)視圖頁上應(yīng)用了這些變更: 它需要一個(gè)頁面去提示瀏覽器獲取資源, 另一個(gè)頁面則去使用新的資源.這不是很理想, 但是在我們的案例中是可以接受的. 在一般情況下, 我們的團(tuán)隊(duì)在應(yīng)用緩存的限制下相對沒有多少煩惱; 更多我們的app不適用的情況下解決起來會(huì)更輕松.
好吧, 也許我們可以與應(yīng)用緩存合作. 可能這是一個(gè)方法在不必通過大量的重構(gòu)去實(shí)現(xiàn)它?
我們快速而粗糙的主意就是,使用 Django 處理視圖模版并返回一個(gè)html頁面來保持我們當(dāng)前頁面的原狀.在任何用戶數(shù)據(jù)變更時(shí),瀏覽器會(huì)從服務(wù)器和應(yīng)用緩存那里獲取一個(gè)重新渲染的頁面 .
我們的游戲計(jì)劃:
-
我們將在當(dāng)前頁面上激活應(yīng)用緩存, 所以它將會(huì)繞過服務(wù)器去加載.
-
當(dāng)一個(gè)用戶制造了一些數(shù)據(jù)改動(dòng)而我們又想保留時(shí), 我們的頁面將會(huì)使用一個(gè)ajax請求去保存數(shù)據(jù)到數(shù)據(jù)庫里,通常我們就是這么做的.
-
我們將會(huì)從應(yīng)用緩存清單引入一個(gè)對用戶特殊的版本號(hào),所以對于每個(gè)用戶來說這份清單都是***的. 當(dāng)用戶更新任意數(shù)據(jù)時(shí), 我們將會(huì)對這個(gè)用戶的應(yīng)用緩存清單的內(nèi)容創(chuàng)建一個(gè)新的版本,而且瀏覽器會(huì)知道并獲取頁面資源來更新.
-
在客戶端方面, 我們將會(huì)在用戶修改任意數(shù)據(jù)時(shí)檢查應(yīng)用緩存并更新.瀏覽器將會(huì)獲取用戶的緩存清單, 查看已經(jīng)被處理成一個(gè)新版本號(hào)的被更改的內(nèi)容,并且重新獲取頁面的內(nèi)容.
-
理論上, 當(dāng)用戶下次瀏覽這個(gè)頁面時(shí), 應(yīng)用緩存會(huì)提供一個(gè)在服務(wù)端重新渲染過的***的頁面.
從好的方面講,這些選擇將會(huì)引入極小的工程投資.
一個(gè)小缺點(diǎn):這個(gè)選項(xiàng)沒起作用。
瀏覽器獲取資源的速度不夠快是主要的問題。 如果你在新的標(biāo)簽頁修改了數(shù)據(jù)(例如,在你的便簽里添加了一條筆記),然后在幾秒內(nèi)打開了一個(gè)新的標(biāo)簽,應(yīng)用緩存可能還沒有獲取到你修改的新的頁面,顯示的依舊是你沒添加筆記的舊頁面。從用戶體驗(yàn)的角度看,這就像是數(shù)據(jù)丟失 — 即使是技術(shù)上的數(shù)據(jù)延遲 ,也是我們無法接受的。
當(dāng)多個(gè)設(shè)備參與時(shí)這個(gè)問題會(huì)變得更嚴(yán)重。如果你在設(shè)備A上對你的新標(biāo)簽頁有修改,接著在設(shè)備B打開一個(gè)標(biāo)簽頁,保證你得到的是舊的數(shù)據(jù)。在隨后的頁面加載之前你都看不到新的數(shù)據(jù)。
這不是很好。 抱歉,這是個(gè)快速而粗糙的選擇。
三:面向模板的本地存儲(chǔ)和應(yīng)用程序存儲(chǔ)
更簡潔地做到這一點(diǎn),我們可以結(jié)合客戶端模板使用應(yīng)用緩存,在本地存儲(chǔ)數(shù)據(jù)。這看起來是個(gè)很好的選擇,除了應(yīng)用緩存的“第二頁加載”那個(gè)問題所出現(xiàn)的糟糕情況,它是非??斓?,并且它還可以清理掉我們的前端(重構(gòu)...哇?)。作為獎(jiǎng)勵(lì),我們的新標(biāo)簽頁在在線的時(shí)候?qū)⒈辉L問。
我們選擇使用 React.js 作為模板是有一些原因的。最主要的一個(gè)就是我們有一些在其他領(lǐng)域使用應(yīng)用的經(jīng)驗(yàn)。我們也覺得學(xué)習(xí)曲線比Angular更淺顯些,我們也是嚴(yán)肅地考慮過其他方案的。說來奇怪,長久以來建立一個(gè)前端框架都是在我們已有的jQuery上努力,我們的數(shù)據(jù)被改變更像是React中的“狀態(tài)”,這會(huì)讓我們轉(zhuǎn)換到React更容易些。
我們還選用了 Facebook 的 Flux 構(gòu)型, 因?yàn)槲覀冋J(rèn)可單向的數(shù)據(jù)流可以讓我們的代碼更順理成章. Flux 的調(diào)度器也能讓我們更加容易的進(jìn)行數(shù)據(jù)同步,下面我會(huì)對此進(jìn)行描述.
#p#
它是如何運(yùn)作的
新的tab一打開,瀏覽器就會(huì)從應(yīng)用緩存中獲取我們的頁面。我們的React應(yīng)用或從Flux存儲(chǔ)中獲取數(shù)據(jù) (1), 后者會(huì)去本地存儲(chǔ)里抽取數(shù)據(jù) (2). React 應(yīng)用一安裝,頁面就會(huì)被加載 (3, 4). 然后,我們的頁面會(huì)向應(yīng)用服務(wù)器進(jìn)行一次Ajax調(diào)用 (5), 發(fā)送應(yīng)用的所有數(shù)據(jù)—其實(shí)就是一個(gè)帶有所有Flux存儲(chǔ)數(shù)據(jù)的對象. 服務(wù)器接收到用戶所有的本地?cái)?shù)據(jù),并使用用戶在數(shù)據(jù)庫中的數(shù)據(jù)對其進(jìn)行調(diào)和 (這會(huì)在下面的 "數(shù)據(jù)同步" 中有更詳細(xì)的描述), 然后向應(yīng)用返回***的數(shù)據(jù) (6). 應(yīng)用會(huì)從服務(wù)器接收到***的數(shù)據(jù),并且更新每一個(gè)Flux存儲(chǔ) (7, 8). Flux 存儲(chǔ)一更新,存儲(chǔ)會(huì)觸發(fā)一個(gè)變化事件 (3), 而 React 組件就會(huì)更新他們的狀態(tài) (4). 當(dāng)用戶改變了什么東西的時(shí)候,就會(huì)發(fā)起一個(gè)動(dòng)作 (11), 更新存儲(chǔ)的數(shù)據(jù) (7, 8); 當(dāng)存儲(chǔ)更新并觸發(fā)變化時(shí)間的時(shí)候,我們會(huì)將數(shù)據(jù)持久化到本地存儲(chǔ)中 (9) 而如果用戶在線的話,就將數(shù)據(jù)持久化到數(shù)據(jù)庫中 (10).
如果你了解過有關(guān) Flux 的東西, 下面這幅圖看起來應(yīng)該會(huì)很熟悉:
(抱歉,這圖看起來有點(diǎn)亂.)
這幅數(shù)據(jù)流圖的意思是,我們會(huì)向你快速的顯示新的tab頁,然后在一秒鐘左右之內(nèi),我們將會(huì)用來自服務(wù)器的新數(shù)據(jù)更新你的頁面. 這份新數(shù)據(jù)可能包含你在另外一個(gè)設(shè)備上對一個(gè)窗口小組件做出的變化 (像便條里的內(nèi)容) , 或者也可能是一個(gè)慈善活動(dòng)中“籌集資金" 實(shí)時(shí)統(tǒng)計(jì).
有關(guān)使用 Flux 構(gòu)型最令人驚奇的一件事情就是通過調(diào)度器的遠(yuǎn)程數(shù)據(jù)流同步時(shí)間完全同用戶的操作保持一致,使得調(diào)試異常的簡單. 因?yàn)榇鎯?chǔ)是我們應(yīng)用狀態(tài)的真實(shí)來源, 所以我們可以放心的讓應(yīng)用在存儲(chǔ)的數(shù)據(jù)被遠(yuǎn)程的數(shù)據(jù)同步或者用戶的輸入改變時(shí),仍然可以始終如一地響應(yīng). 我們?nèi)匀豢梢栽僬麄€(gè)應(yīng)用中保持單項(xiàng)的數(shù)據(jù)流, 這使得代碼理所當(dāng)然的變得簡單很多.
在應(yīng)用速度和數(shù)據(jù)實(shí)時(shí)性兩者之間,我們已經(jīng)找到了理想的平衡.
數(shù)據(jù)同步
我提到過我們會(huì)在每次頁面加載的時(shí)候向服務(wù)器同步你的數(shù)據(jù)。那我們是如何去實(shí)現(xiàn)這個(gè)東西的呢?
我們通過為數(shù)據(jù)“塊”的***一次更新打上時(shí)間戳,然后在客戶端(的Flux存儲(chǔ))上,以及遠(yuǎn)程的數(shù)據(jù)庫中保存數(shù)據(jù)發(fā)生變化的時(shí)間戳(modified_at),這樣的方式來處理同步. 例如,如果在你的一個(gè)便條窗口中進(jìn)行了輸入,就會(huì)把窗口的modified_at時(shí)間戳設(shè)置成現(xiàn)在,然后把你的便條內(nèi)容保存到本地存儲(chǔ)中,并入如果條件可能的話,也會(huì)保存到遠(yuǎn)程數(shù)據(jù)庫中. 而后,下次你打開一個(gè)tab的時(shí)候,我們將會(huì)把有關(guān)窗口的數(shù)據(jù)發(fā)送到應(yīng)用服務(wù)器,在那里會(huì)對跟該窗口相關(guān)的客戶端時(shí)間戳跟數(shù)據(jù)庫中保存的時(shí)間戳進(jìn)行比對,并返回***的數(shù)據(jù).
為了簡單起見,我們用Flux存儲(chǔ)對象來進(jìn)行數(shù)據(jù)的發(fā)送和接收. 這讓我們可以無痛的用發(fā)送自應(yīng)用的數(shù)據(jù)更新Flux存儲(chǔ), 因?yàn)槲覀兠靼姿鼘?huì)被保持***,并且同我們的存儲(chǔ)一樣具有相同的數(shù)據(jù)結(jié)構(gòu).
我們當(dāng)前的同步過程肯定是不***的: 在發(fā)生同步?jīng)_突的情況下,我們會(huì)簡單地去獲取***的數(shù)據(jù). 對我們而言,這只是一個(gè)可以接受的細(xì)節(jié)狀況; 畢竟,我們可不是 Evernote. 即使這會(huì)變得不可接受,也可以在以后用更智能的數(shù)據(jù)合并和用戶消息進(jìn)行解決.
讓我們運(yùn)行得更快一些 ! (或者說與呈現(xiàn))
加載應(yīng)用緩存的頁面很不錯(cuò),但在我們向用戶展示之前,我們?nèi)匀灰\(yùn)行React應(yīng)用代碼,并對所有的組件進(jìn)行渲染. 對于一個(gè)相當(dāng)大的應(yīng)用而言,這可能要花上幾百毫秒甚至超過1秒.
為了能有一個(gè)快速的***次加載體驗(yàn), React 提供了一個(gè)方便的 renderToString 方法,讓你可以先向?yàn)g覽器發(fā)送DOM(讓頁面先出現(xiàn)) ,然后再連接上所有的偵聽器 (讓頁面可交互). 這樣就適應(yīng)服務(wù)器端的預(yù)呈現(xiàn)了. 在我們的案例中,我們想是否可以用把它用在客戶端上 — 而我們做到了.
每次我們將數(shù)據(jù)持久化到本地存儲(chǔ)時(shí),我們也會(huì)將我們的React應(yīng)用做成字符串,并將這個(gè)字符串保存到本地存儲(chǔ)中. 然后,頁面一加載,在我們做任何事情之前,我們會(huì)從本地存儲(chǔ)中加載渲染好的應(yīng)用并將它放到一個(gè)HTML元素中. 換言之,頁面只用了3行JavaScript就加載了DOM! 對于我們的應(yīng)用而言,預(yù)呈現(xiàn)減少了大概400毫秒的預(yù)加載時(shí)間。
"見鬼":挑戰(zhàn)和缺陷
沒有什么東西是***的。重構(gòu)的時(shí)候還是有些事情不那么有趣的。
再見吧, JQuery UI
在轉(zhuǎn)換到React過程中的一個(gè)速度損失讓我們放棄了幾乎所有的JQuery UI組件,比如 draggable. 這稍微煩人地讓我們花了點(diǎn)時(shí)間來重新做之前已經(jīng)做過的事情. 不過,事實(shí)證明我們還是可以依靠不斷增長的實(shí)用的 開源React組件 來構(gòu)建我們自身想要的東西.
"為什么, renderToString, 為什么?"
另外一個(gè)小的實(shí)現(xiàn)上的挑戰(zhàn): 如果你用過React的 renderToString 方法, 你可能已經(jīng)看到過這個(gè)錯(cuò)誤:
- React attempted to use reuse markup in a container but the checksum was invalid.
當(dāng)React在已經(jīng)有預(yù)渲染DOM存在之后渲染它的應(yīng)用時(shí),它就要預(yù)計(jì)預(yù)渲染好的DOM應(yīng)該同將要被渲染的DOM相同. 那就意味著你不能讓像 Date.now() 和 Math.random() 這樣的東西影響到你的DOM. 為了解決這個(gè)問題,你將可能要花點(diǎn)時(shí)間在你的差異編輯器上面,來比對這兩個(gè)DOM字符串.
不夠靈活的存儲(chǔ)數(shù)據(jù)結(jié)構(gòu)
我們設(shè)計(jì)為應(yīng)用同服務(wù)器返回的應(yīng)用數(shù)據(jù)結(jié)構(gòu)之間的不匹配敞開了大門. 在我們想生產(chǎn)環(huán)境推送新的代碼之后,你***次加載的頁面視圖會(huì)包含從應(yīng)用緩存加載的老應(yīng)用代碼. 不過,從應(yīng)用服務(wù)器返回的同步數(shù)據(jù)將會(huì)是結(jié)構(gòu)化的,而我們的新版本會(huì)對其進(jìn)行構(gòu)造.
所以,如果在新版本中我們決定對存儲(chǔ)中的一塊數(shù)據(jù)進(jìn)行重命名或者移除,你的頁面就會(huì)在新的tab***次打開時(shí)被打斷; 老的應(yīng)用代碼不會(huì)知道如何去處理它. 在打開下一個(gè)tab之前,你的瀏覽器可能已經(jīng)獲取到了***的應(yīng)用代碼,并將其放到了應(yīng)用緩存中,因此頁面會(huì)運(yùn)作得很好.
為了防止新tab的打斷, 我們需要為我們的存儲(chǔ)數(shù)據(jù)維護(hù)一套可靠的內(nèi)部API. 那樣會(huì)有點(diǎn)兒痛苦.
說到代碼的推送...
如果我們搞砸了,弄壞了應(yīng)用,每個(gè)看到一個(gè)破頁面的用戶都會(huì)在我們修復(fù)它之前看到一個(gè)額外的破頁面. 應(yīng)用程序緩存就會(huì)進(jìn)行惱人的二次重新加載更新。
大家好,結(jié)局才會(huì)好
切換到React和Flux是一件令人很愉快的事情. 我們的團(tuán)隊(duì)發(fā)現(xiàn)我們自己重新愛上了前端開發(fā), 而我們做出的變化讓新進(jìn)工程師接觸代碼庫容易了許多。
在用戶體驗(yàn)方面,我們的新tab一直在快速推進(jìn). 對于擁有優(yōu)良網(wǎng)絡(luò)條件的用戶而言,這次的版本不會(huì)有太多的變化;但是對于其他人,他們是能在發(fā)現(xiàn)我們的應(yīng)用不可用和喜歡上使用它之間發(fā)現(xiàn)不同的。
因?yàn)門ab需要從橫幅廣告展示為慈善機(jī)構(gòu)籌集基金的原因,更快的頁面加載能增加在用戶離開我們的頁面之前看到的廣告的數(shù)量. 這次的版本增加了大約12%的廣告展示 (還有對應(yīng)的籌資收入).
當(dāng)然,一個(gè)快速的應(yīng)用并不會(huì)是一個(gè)好的應(yīng)用; 它只是好的應(yīng)用不會(huì)是一個(gè)馬上就會(huì)讓人討厭的應(yīng)用. 對于我們而言,它提供了未來更多有趣動(dòng)人的事情的基礎(chǔ).
-----
這是不是很有趣 ? 你想要通過一個(gè)有趣,充滿活力的團(tuán)隊(duì)工作不 ? 我們在招人哦 ! 還有,如果你本周就在 San Francisco 附近, 我就會(huì)在周五的 React.js 推介見面會(huì) 上 — 如果你想要一次會(huì)談的話就讓我知道吧.
感謝 Ti Zhao 和 Josiah Gaskin 對這篇文章的評論。
英文原文:Using React.js and Application Cache for a fast, synced app