React/Redux打造的同構Web應用

發表於2018-07-30

大家好,我是原一成(@herablog),目前在CyberAgent主要擔任前端開發。

Ameblo(注: Ameba部落格,Ameba Blog,簡稱Ameblo)於2016年9月,將前端部分由原來的Java架構的應用,重構成為以node.js、React為基礎的Web應用。這篇文章介紹了本次重構的起因、目標、系統設計以及最終達成的結果。

新系統釋出後,立即就有人注意到了這個變化。

 React/Redux打造的同構Web應用
React/Redux打造的同構Web應用
twitter_msg.png

系統重構的起因

2004年起,Ameblo成為了日本國內最大規模的部落格服務。然而隨著系統規模的增長,以及很多相關人員不斷追加各種模組、頁面引導連結等,最終使得頁面展現緩慢、對網頁瀏覽量(PV)造成了非常嚴重的影響。並且頁面展現速度方面,絕大多數是前端的問題,並非是後端的問題。

基於以上這些問題,我們決定以提高頁面展現速度為主要目標,對系統進行徹底重構。與此同時後端系統也在進行重構,將以往的資料部分進行API化改造。此時正是一個將All-in-one的巨型Java應用進行適當分割的絕佳良機。

目標

本次系統重構確立了以下幾個目標。

頁面展現速度的改善(總之越快越好)

用於測定使用者體驗的指標有很多,我們認為其中對使用者最重要的指標就是頁面展現速度。頁面展現速度越快,目標內容就能越快到達,讓任務在短時間內完成。這次重構的目標是儘可能的保持部落格文章、以及在Ameblo內所呈現的繁多的內容的固有形式,在不破壞現有價值、體驗的基礎上,提高展現和頁面行為的速度。

系統的現代化(搭乘生態系統)

從前的Web應用是將資料以HTML的形式返回,那個時候並沒有什麼問題。然而,隨著內容的增加,體驗的豐富化,以及裝置的多樣化,使得前端所佔的比重越來越大。此前要開發一個好的Web應用,如果要高效能,就一定不要將前後端分隔開。當年以這個要求開發的系統,在經歷了10年之後,已經遠遠無法適應當前的生態系統。

「跟上當前生態系統」,以此來構建系統會帶來許許多多的好處。因為作為核心的生態系統,其開發非常活躍,每天都會有許許多多新的idea。因而最新的技術和功能更容易被吸納,同時實現高效能也更加容易。同時,這個「新」對於年輕的技術新人也尤為重要。僅懂得舊規格舊技術的大叔對於一個優秀的團隊來說是沒有未來的(自覺本人膝蓋也中了一箭)。

升級介面設計、使用者體驗(2016年版Ameblo)

Ameblo的手機版在2010年經歷了一次改版之後,就基本上沒有太大的變化。這其間很多使用者都已經習慣了原生應用的設計和體驗。這個專案也是為了不讓人覺得很土很難用,達到順應時代的2016年版介面設計和使用者體驗。

OK,接下來讓我具體詳細聊聊。

頁面載入速度的改善

改善點

系統重構前,通過 SpeedCurve 進行分析,得出了下面結論:

  • 伺服器響應速度很快
  • HTML文件較大(頁面所有要素都包含其中)
  • 阻塞頁面渲染的資源(JavaScript、Stylesheet)較多
  • 資源讀取的次數過多,體積過大

依據這些確定了下面這幾項基本方針:

  • 為了不致於降低伺服器響應速度,對程式碼進行優化,快取等
  • 儘可能減少HTML文件大小
  • JavaScript非同步地載入與執行
  • 最初呈現頁面時,僅僅載入所需的必要資源

SSR還是SPA

近年來相比於新增到收藏夾中,使用者更傾向於通過搜尋結果、Facebook、Twitter等社交媒體上的分享連結開啟部落格頁面。Google和Twitter的AMP, Facebook的Instant Article表明第一頁的展現速度極大影響到使用者滿意度。

此外,從Google Analytics等日誌記錄中瞭解到在文章列表頁面和前後文章間進行跳轉的使用者也很多。這或許是因為部落格作為個人媒體,當某一使用者看到一篇不錯的文章,非常感興趣的時候,他也同時想看一看同一部落格內的其它文章。也就是說,部落格這種服務 第一頁快速載入與頁面間快速跳轉同等重要

因此,為了讓兩者都能發揮最佳效能,我們決定在第一頁使用伺服器端渲染(Server-side Rendering, SSR),從第二頁起使用單頁面應用(Single Page Application, SPA)。這樣一來,既能確保第一頁的展示速度和機器可讀性(Machine-Readability)(含SEO),又能獲得SPA帶來的快速展示速度。

BTW,對於目前的架構,由於伺服器和客戶端使用相同的程式碼,全部進行SSR或是全部進行SPA也是可能的。目前已經實現即便在不能執行JavaScript的環境中,也可以正常通過SSR來瀏覽。可以預見將來等到Service Worker普及之後,初始頁面將更加高速化,而且可以實現離線瀏覽。

React/Redux打造的同構Web應用React/Redux打造的同構Web應用
z-ssrspa.png

以前的系統完全使用SSR,而現在的系統從第二頁起變為SPA。

 React/Redux打造的同構Web應用
React/Redux打造的同構Web應用
z-spa-speed.gif

SPA的魅力在於呈現速度之快。因為僅僅通過API獲取所需的必要資料,所以速度非常快!

延遲載入

我們使用SSR+SPA的方法來優化頁面間跳轉這種橫向移動的速度,並且使用延遲載入來改善頁面的縱向移動速度。一開始要展現的內容以及導航,還有部落格文章等最早呈現,在這些內容之下的次要內容隨著頁面的滾動逐漸呈現。這樣一來,重要的內容不會受頁面下面內容的影響而更快的顯示出來。對於那些想盡快讀文章的使用者來說,既不增加使用者體驗上的壓力,又能完整的提供頁面下方的內容。

 React/Redux打造的同構Web應用
React/Redux打造的同構Web應用
z-lazyload.png

之前的系統因為將頁面內的全部內容都放到HTML文件裡,所以使得HTML文件體積很大。而現在的系統,僅僅將主要內容放到HTML裡返回,減少了HTML的體積和資料請求的大小。

HTML快取

部落格文章是靜態文件,對於特定URL的請求會返回固定的內容,因此非常適合進行快取。快取使得伺服器處理內容減少,在提高頁面響應速度的同時減輕了伺服器的負擔。我們將不變的內容(文章等)生成的HTML進行快取返回,對於由於變化的內容能過JavaScript、CSS等進行操作(比如顯示、隱藏等)。

 React/Redux打造的同構Web應用
React/Redux打造的同構Web應用
z-newrelic-entrylist.png

這張圖顯示了2016年9月最後一週New relic上的統計資料。文章列表頁面的HTML的響應時間基本在50ms以下。

 React/Redux打造的同構Web應用
React/Redux打造的同構Web應用
z-newrelic-entry.png

這張圖是文章詳細頁面的統計資料。可以看出,這個頁面的響應時間也基本上是在50ms以下。由於存在文章過長的時候會造成頁面體積變大,以及文章頁面不能完全快取等情況,所以相比列表頁面會存在更多較慢的響應。

對於因請求的客戶端而產生變化部分的處理,我們在HTML的body標籤中通過加入相應的class,然後在客戶端通過JavaScript和CSS等進行操作。比如,一些內容不想在某些作業系統上顯示,我們就用CSS對這些內容進行隱藏。由於CSS樣式表會先載入,頁面佈局確定下來之後再進行頁面渲染,所以這個也可以解決後面要提到的「咯噔」問題。

系統的現代化(搭乘生態系統)

技術選型

這次專案的技術選擇時,遵循了儘可能採用當前當前市場上已經存在的普遍使用的技術這一原則。暗號就是:「活脫脫像範例應用一樣Start」。這樣一來,無論是誰都可以輕鬆的獲取到相應的文件等資訊,同時其它的團隊和公司如果要參與到專案中來也能很快的上手。然而在真正進行開發的時候,一些細節實現上因為各種各樣的原因存在一些例外的情況,但是在極大程度上保持了各個模組的獨立性。最終系統的大體構成如下圖所示:

 React/Redux打造的同構Web應用
React/Redux打造的同構Web應用
z-bigpicture.png

(有些地方做了省略)

React with Redux

使用React和React進行開發的的時候,很多地方可以用 純函式 的形式進行組合。純函式是指特定的引數總是返回特定的結果,不會對函式以外的範圍造成汙染。使用純函式進行開發可以保證各個處理模組最小化,不用擔心會無意間改變引用物件的值。這樣一來,十分有助於大規模開發以及在同一客戶端中維持多個狀態。

介面更新的流程是: Action(Event) -> Reducer (返回新的state(狀態)) -> React (基於更新後的store內的state更新顯示內容)

這是一個Redux Action的例子,演示了React Action (Action Creator) 基於引數返回一個Plain Object。處理非同步請求的時候,我們參考 官方文件 ,分別定義了成功請求和失敗請求。獲取資料時使用了 redux-dataloader

Redux Reducer是一完全基於Action中攜帶的資料,對已有state進行復制並更新的函式。

React/Redux基於更新後的store中的資料,對UI進行更新。各個元件依據傳遞過來的props值,總是以相同的結果返回HTML。React將View元件也作為函式來對待。

有關Redux的資訊在 官方文件 中說明得非常詳細,推薦隨時參考一下這個文件。

同構Web應用(Isomorphic web app)

Ameblo 2016年版基本上完全是用JavaScript重寫的。無論是Node伺服器上還是客戶端上都使用了相同的程式碼和流程,也就是所謂的同構Web應用。專案的目錄結構大體上如下所示,伺服器端的入口檔案是 server.js ,瀏覽器的入口檔案是 client.js

  • actions/ Redux Action (伺服器,客戶端共用)
  • api/ 封裝的API介面
  • components/ React元件 (伺服器,客戶端共用)
  • reducer/ <span class=”underline”>Redux Reducers</span> (伺服器,客戶端共用)
  • services/ 服務層模型,使用 Fetchr 對資料請求進行適當粒度的劃分。同時這個也使得node.js作為代理,間接請求API(伺服器專用)。
  • server.js 伺服器入口(伺服器專用)
  • app.js node伺服器的配置、啟動,由server.js呼叫(伺服器專用)
  • client.js 客戶端入口(客戶端專用)
 React/Redux打造的同構Web應用
React/Redux打造的同構Web應用
z-isomorphic.png

寫好的JavaScript同時執行在伺服器端還是客戶端上的執行行為、以及從資料讀取直到在頁面上顯示為止的整個瀏程,都以相同的形式進行。

React/Redux打造的同構Web應用

z-code-stats.png

使用Github的語言統計可以看出 ,JavaScript佔了整個專案的94.0%,幾乎全部都是由JavaScript寫成的。

原子設計(Atomic Design)

對於元件的規劃,我們採用了 原子設計 理念。其實專案並沒有一開始就採用原子設計,而是根據 Presentational and Container Components ,對 containercomponent 進行了兩層劃分。然而Ameblo中的元件實在是太多,很容易造成職責不明確的情況,因此最終採用了原子設計理念。專案的實際運用中,採用了以下的規則。

 React/Redux打造的同構Web應用
React/Redux打造的同構Web應用
z-atomic-design.png

Atoms

元件的最小單位,比如Icon、Button等。原則上不具有狀態,從父元件中獲取傳遞過來的props,並返回HTML。

Molecules

以複用為前提的元件,比如List、Modal、User thunmbnail等。原則上不具有狀態,從父元件中獲取傳遞過來的props,並返回HTML。

Organisms

頁面上較大的一塊元件,比如Header,Entry,Navi等。對於這一層的元件,可以在其中進行資料獲取處理,以及使用Redux State 和 connect ,維護元件的狀態。這裡獲取的元件狀態以props的形式,傳遞給 MoleculesAtom

Template

各個請求路徑(URL)所對應的元件。其職責是將所需的部件從Organisms中import過來,以一定的順序和格式整合在一起。

Pages

作為頁面的頁面元件。基本上是把傳遞過來的 this.props.children 原原本本的顯示出來。由於Ameblo是單頁面應用,因而只有一個頁面元件。

CSS Modules

CSS樣式表使用 CSS Modules 將CSS樣式規則的作用範圍嚴格限制到了各個元件內。各個樣式規則的作用範圍進行限制使得樣式的變更和刪除更加容易。因為Ameblo是由許多人協同開發完成,不一定每個人都精通CSS,而且不免要時常對一些不知是誰何時寫的程式碼進行更改,在這個時候將作用範圍限制到元件的CSS Modules就發揮其作用了。

相關文章