使用 React.js 和應用換成構建快速同步應用程序

jopen 9年前發布 | 30K 次閱讀 React.js

對大部分應用系統來說,在某種程度上,應用程序的快速加載和及時取得最新數據兩個方面同樣重要。傾向于積極使用緩存數據,可能會導致提供的數據陳舊;而傾 向于及時獲取最新數據,可能會犧牲加載時間。當然,也可以魚與熊掌兼得,但是可能會需要更多的硬件,更復雜的軟件,或兩者都需要(意味著一個字:錢)。

如何權衡取決于特定的應用系統和業務要求,本文就是我們的團隊使用React.js和應用緩存來解決這一問題的一個實例。

我們從哪里開始

標簽是每當你在瀏覽器上打開一個標簽去送出一份慈善捐助的好理由。這是一件很偉大的事——但事實上,我們僅僅點擊了一個價值 100,000美元的里程碑來完成慈善捐助——但是,我們有一個疑問。

我們的應用也太慢了。大家都明白這點。當用戶更換新的標簽頁時,他們需要得是速度與連貫性。而且,我們也沒有宣布:載入頁面的延遲成為了人們關閉標簽的首選理由。

我們想讓我們的頁面除了更有用,還要更好地被接受。但隨著我們向頁面中加了些附加功能后, 我們的頁面載入問題也越來越突出了。因為人們需要我們的 APP 能快速地提供內容信息。

我們正在用 Django 的模板系統做一個交互式服務器來召喚或服務一個頁面。當使用者是在快速的網絡環境中,而且我們的服務狀態是健康的情況下,服務器響應時間是 ~65毫秒,還不是比較慘。然而,如果在你父母的房子*里打開一個標簽,或者我們的數據庫產生了一個短暫的停頓時,這可能會給你在對其的信任上,潑了一盆冷水。

比較讓人煩惱,我應該承認我們所建立的 APP 并沒有采用標準的前端框架,除了僅僅是使用了 JQuery。 考慮到我們的 APP 有太多的互動,而且太混亂了。在各種各樣的代碼類型上,我要怎么才能喜歡它。

我們需要去修改它。

* 我愛你們,老媽、老爸!時代華納有線電視, 沒有太多什么了

明確我們的需求

當準備去處理這個問題時,我們必須決定優先處理哪些以及放棄哪此需求。在這里我們提出了一些建議:

  1. 頁面必須能快速載入。這是沒得討價還價的。

  2. 我們的頁面必須是非本地 URL。我們提高了 VIA 捐助廣告的價格,網絡在線廣告需要去核識真實性來確保這些廣告是夠安全的。由于瀏覽器端的用戶頁面插件總是將我們的廣告移除,以至于網絡廣告只能使用 http 或 https 協議。

  3. 我們希望頁面中的內容是最新的,但不必是實時性的。我 們通過設備對用戶數據進行同步, 并保持完美的體驗。我們以分頁的形式顯示出用戶的反饋;例如,我們顯示出新用戶的統計數據;我們有時也要運行捐助設備來以滾動條的形式顯示出 "募集資金" 量。雖然我們愿意去接收一定程度上稍舊的數據(就像頁面展示后才提交數據),但理想得是在提交數據的瞬間發生。

  4. 我們要減少前端混亂的代碼。將非優先權最高的代碼肅清,這是一件讓人興奮的事。

讓我們動起手來實踐關于處理這些問題的思路。

一、采用主動的服務器端緩存

我們一度認為應該增加首先擴展服務器端緩存結構,目前我們已經十分依賴Django的低級緩存(low-level cache),它有助于達到我們的目標,但是我們不得不在每次都要寫語句來判斷是否存在緩存到期或失效情況,我想這張摘自一場精彩演講(an excellent presentation)的幻燈片能夠反映出Django在緩存問題上面臨的挑戰:

使用 React.js 和應用換成構建快速同步應用程序

此外,為了更好地從服務器端緩存中獲益, 我們的緩存系統看起來是一個多層次的結構:(先是)每個用戶完整的頁面緩存,然后是用戶數據的模塊化的緩存,(同時)每當數據變化時還要智能判斷數據是否 失效。因為在實施過程中已經遭遇到一些與緩存相關的(系統)錯誤(bugs),所以我們并不希望繼續增加了緩存系統的復雜性。

更重要的是,還存在網絡傳遞差異的問題,例如對一個新的TAB頁面來說,在快速和慢速的因特網網絡上的表現有著顯著的差異,即使將我們的服務響應時間降低到小于1毫秒,對大部分用戶而言,這個頁面顯示的還是不夠快的。

不,這樣可不行。

二: 在我們的頁面上使用應用緩存

"應用緩存? 他不是個douchebag么?"

不,別這么粗魯!

… 好吧, 也許他是有點兒. 在使用應用緩存之前,充分了解它的怪癖和陷阱是明智的.我們主要關心的是應用緩存會降低我們在調試時的透明度,因為我們服務器在輕便的 請求上沒有日志  (接下來我們將解決這個問題). 在代碼變更后的另一個與之前不同的小問題是,在兩個視圖頁上應用了這些變更: 它需要一個頁面去提示瀏覽器獲取資源, 另一個頁面則去使用新的資源.這不是很理想, 但是在我們的案例中是可以接受的. 在一般情況下, 我們的團隊在應用緩存的限制下相對沒有多少煩惱; 更多我們的app不適用的情況下解決起來會更輕松.

好吧, 也許我們可以與應用緩存合作. 可能這是一個方法在不必通過大量的重構去實現它?

我們快速而粗糙的主意就是,使用 Django 處理視圖模版并返回一個html頁面來保持我們當前頁面的原狀.在任何用戶數據變更時,瀏覽器會從服務器和應用緩存那里獲取一個重新渲染的頁面 .

我們的游戲計劃:

  • 我們將在當前頁面上激活應用緩存, 所以它將會繞過服務器去加載.

  • 當一個用戶制造了一些數據改動而我們又想保留時, 我們的頁面將會使用一個ajax請求去保存數據到數據庫里,通常我們就是這么做的.

  • 我們將會從應用緩存清單引入一個對用戶特殊的版本號,所以對于每個用戶來說這份清單都是獨一無二的. 當用戶更新任意數據時, 我們將會對這個用戶的應用緩存清單的內容創建一個新的版本,而且瀏覽器會知道并獲取頁面資源來更新. 

  • 在客戶端方面, 我們將會在用戶修改任意數據時檢查應用緩存并更新.瀏覽器將會獲取用戶的緩存清單, 查看已經被處理成一個新版本號的被更改的內容,并且重新獲取頁面的內容.

  • 理論上, 當用戶下次瀏覽這個頁面時, 應用緩存會提供一個在服務端重新渲染過的最新的頁面.

從好的方面講,這些選擇將會引入極小的工程投資.

一個小缺點:這個選項沒起作用。

瀏覽器獲取資源的速度不夠快是主要的問題。 如果你在新的標簽頁修改了數據(例如,在你的便簽里添加了一條筆記),然后在幾秒內打開了一個新的標簽,應用緩存可能還沒有獲取到你修改的新的頁面,顯示的依舊是你沒添加筆記的舊頁面。從用戶體驗的角度看,這就像是數據丟失 — 即使是技術上的數據延遲 ,也是我們無法接受的。

當多個設備參與時這個問題會變得更嚴重。如果你在設備A上對你的新標簽頁有修改,接著在設備B打開一個標簽頁,保證你得到的是舊的數據。在隨后的頁面加載之前你都看不到新的數據。

這不是很好。 抱歉,這是個快速而粗糙的選擇。

三:面向模板的本地存儲和應用程序存儲

更簡潔地做到這一點,我們可以結合客戶端模板使用應用緩存,在本地存儲數據。這看起來是個很好的選擇,除了應用緩存的“第二頁加載”那個問題所出現的糟糕 情況,它是非常快的,并且它還可以清理掉我們的前端(重構...哇?)。作為獎勵,我們的新標簽頁在在線的時候將被訪問。

我們選擇使用 React.js 作為模板是有一些原因的。最主要的一個就是我們有一些在其他領域使用應用的經驗。我們也覺得學習曲線比Angular更淺顯些,我們也是嚴肅地考慮過其他 方案的。說來奇怪,長久以來建立一個前端框架都是在我們已有的jQuery上努力,我們的數據被改變更像是React中的“狀態”,這會讓我們轉換到 React更容易些。

我們還選用了 非死book 的 Flux 構型, 因為我們認可單向的數據流可以讓我們的代碼更順理成章. Flux 的調度器也能讓我們更加容易的進行數據同步,下面我會對此進行描述.

它是如何運作的

新 的tab一打開,瀏覽器就會從應用緩存中獲取我們的頁面。我們的React應用或從Flux存儲中獲取數據 (1), 后者會去本地存儲里抽取數據 (2). React 應用一安裝,頁面就會被加載 (3, 4). 然后,我們的頁面會向應用服務器進行一次Ajax調用 (5), 發送應用的所有數據—其實就是一個帶有所有Flux存儲數據的對象. 服務器接收到用戶所有的本地數據,并使用用戶在數據庫中的數據對其進行調和 (這會在下面的 "數據同步" 中有更詳細的描述), 然后向應用返回最新的數據 (6). 應用會從服務器接收到最新的數據,并且更新每一個Flux存儲 (7, 8).  Flux 存儲一更新,存儲會觸發一個變化事件 (3), 而 React 組件就會更新他們的狀態 (4). 當用戶改變了什么東西的時候,就會發起一個動作 (11), 更新存儲的數據 (7, 8); 當存儲更新并觸發變化時間的時候,我們會將數據持久化到本地存儲中 (9) 而如果用戶在線的話,就將數據持久化到數據庫中 (10).

如果你了解過有關 Flux 的東西, 下面這幅圖看起來應該會很熟悉:

使用 React.js 和應用換成構建快速同步應用程序

(抱歉,這圖看起來有點亂.)

這 幅數據流圖的意思是,我們會向你快速的顯示新的tab頁,然后在一秒鐘左右之內,我們將會用來自服務器的新數據更新你的頁面. 這份新數據可能包含你在另外一個設備上對一個窗口小組件做出的變化 (像便條里的內容) , 或者也可能是一個慈善活動中“籌集資金" 實時統計.

有 關使用 Flux 構型最令人驚奇的一件事情就是通過調度器的遠程數據流同步時間完全同用戶的操作保持一致,使得調試異常的簡單. 因為存儲是我們應用狀態的真實來源, 所以我們可以放心的讓應用在存儲的數據被遠程的數據同步或者用戶的輸入改變時,仍然可以始終如一地響應. 我們仍然可以再整個應用中保持單項的數據流, 這使得代碼理所當然的變得簡單很多.

在應用速度和數據實時性兩者之間,我們已經找到了理想的平衡.

數據同步

我提到過我們會在每次頁面加載的時候向服務器同步你的數據。那我們是如何去實現這個東西的呢?

我們通過為數據“塊”的最后一次更新打上時間戳,然后在客戶端(的Flux存儲)上,以及遠程的數據庫中保存數據發生變化的時間戳(modified_at),這樣的方式來處理同步. 例如,如果在你的一個便條窗口中進行了輸入,就會把窗口的modified_at時間戳設置成現在,然后把你的便條內容保存到本地存儲中,并入如果條件可能的話,也會保存到遠程數據庫中. 而后,下次你打開一個tab的時候,我們將會把有關窗口的數據發送到應用服務器,在那里會對跟該窗口相關的客戶端時間戳跟數據庫中保存的時間戳進行比對,并返回最新的數據.

為了簡單起見,我們用Flux存儲對象來進行數據的發送和接收. 這讓我們可以無痛的用發送自應用的數據更新Flux存儲, 因為我們明白它將會被保持最新,并且同我們的存儲一樣具有相同的數據結構.

我們當前的同步過程肯定是不完美的: 在發生同步沖突的情況下,我們會簡單地去獲取最新的數據. 對我們而言,這只是一個可以接受的細節狀況; 畢竟,我們可不是 Evernote. 即使這會變得不可接受,也可以在以后用更智能的數據合并和用戶消息進行解決.

讓我們運行得更快一些 ! (或者說與呈現)

加載應用緩存的頁面很不錯,但在我們向用戶展示之前,我們仍然要運行React應用代碼,并對所有的組件進行渲染. 對于一個相當大的應用而言,這可能要花上幾百毫秒甚至超過1秒.

為了能有一個快速的第一次加載體驗, React 提供了一個方便的 renderToString 方法,讓你可以先向瀏覽器發送DOM(讓頁面先出現) ,然后再連接上所有的偵聽器 (讓頁面可交互). 這樣就適應服務器端的預呈現了. 在我們的案例中,我們想是否可以用把它用在客戶端上 — 而我們做到了.

每 次我們將數據持久化到本地存儲時,我們也會將我們的React應用做成字符串,并將這個字符串保存到本地存儲中. 然后,頁面一加載,在我們做任何事情之前,我們會從本地存儲中加載渲染好的應用并將它放到一個HTML元素中. 換言之,頁面只用了3行JavaScript就加載了DOM! 對于我們的應用而言,預呈現減少了大概400毫秒的預加載時間。

"見鬼":挑戰和缺陷

沒有什么東西是完美的。重構的時候還是有些事情不那么有趣的。

再見吧, JQuery UI

在轉換到React過程中的一個速度損失讓我們放棄了幾乎所有的JQuery UI組件,比如 draggable. 這稍微煩人地讓我們花了點時間來重新做之前已經做過的事情. 不過,事實證明我們還是可以依靠不斷增長的實用的 開源React組件 來構建我們自身想要的東西.

"為什么, renderToString, 為什么?"

另外一個小的實現上的挑戰: 如果你用過React的 renderToString 方法, 你可能已經看到過這個錯誤:

React attempted to use reuse markup in a container but the checksum was invalid.

當 React在已經有預渲染DOM存在之后渲染它的應用時,它就要預計預渲染好的DOM應該同將要被渲染的DOM相同. 那就意味著你不能讓像 Date.now() 和 Math.random() 這樣的東西影響到你的DOM. 為了解決這個問題,你將可能要花點時間在你的差異編輯器上面,來比對這兩個DOM字符串.

不夠靈活的存儲數據結構

我們設計為應用同服務器返回的應用數據結構之間的不匹配敞開了大門. 在我們想生產環境推送新的代碼之后,你第一次加載的頁面視圖會包含從應用緩存加載的老應用代碼. 不過,從應用服務器返回的同步數據將會是結構化的,而我們的新版本會對其進行構造.

所 以,如果在新版本中我們決定對存儲中的一塊數據進行重命名或者移除,你的頁面就會在新的tab第一次打開時被打斷; 老的應用代碼不會知道如何去處理它. 在打開下一個tab之前,你的瀏覽器可能已經獲取到了最新的應用代碼,并將其放到了應用緩存中,因此頁面會運作得很好.

為了防止新tab的打斷, 我們需要為我們的存儲數據維護一套可靠的內部API. 那樣會有點兒痛苦.

說到代碼的推送...

如果我們搞砸了,弄壞了應用,每個看到一個破頁面的用戶都會在我們修復它之前看到一個額外的破頁面. 應用程序緩存就會進行惱人的二次重新加載更新。

大家好,結局才會好

切換到React和Flux是一件令人很愉快的事情. 我們的團隊發現我們自己重新愛上了前端開發, 而我們做出的變化讓新進工程師接觸代碼庫容易了許多。

在用戶體驗方面,我們的新tab一直在快速推進. 對于擁有優良網絡條件的用戶而言,這次的版本不會有太多的變化;但是對于其他人,他們是能在發現我們的應用不可用和喜歡上使用它之間發現不同的。

因為Tab需要從橫幅廣告展示為慈善機構籌集基金的原因,更快的頁面加載能增加在用戶離開我們的頁面之前看到的廣告的數量. 這次的版本增加了大約12%的廣告展示 (還有對應的籌資收入).

當然,一個快速的應用并不會是一個好的應用; 它只是好的應用不會是一個馬上就會讓人討厭的應用. 對于我們而言,它提供了未來更多有趣動人的事情的基礎.

-----

這是不是很有趣 ? 你想要通過一個有趣,充滿活力的團隊工作不 ? 我們在招人哦 ! 還有,如果你本周就在 San Francisco 附近, 我就會在周五的 React.js 推介見面會 上 — 如果你想要一次會談的話就讓我知道吧.

感謝 Ti Zhao 和 Josiah Gaskin 對這篇文章的評論。

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!