有幸作為講師受邀參加InfoQ在上海舉辦的QCon2017,不得不說,不論是從講師還是聽眾的角度衡量,QCon進一步擴大了技術視野。雖然前端專題只有四場,但每一場分享都是目前的熱門話題。並且Qcon的選題都是從實踐出發,並沒有一些看起來很炫但是尚未經過實踐檢驗的新技術,即使是目前剛剛起步且相對來說比較小眾的WebAssembly也是以餓了么的生產實踐為基礎。

我的分享話題是《面向SPA和Hybrid應用的前端工程體系和實踐經驗》,從我個人角度來講還是缺乏演講技巧,語速過快導致比預期的時間提前了將近1/3,為聽眾爭取到了一個比較長的茶歇時間╮(╯▽╰)╭。演講結束后與支付寶的同行探討了一些相關的問題,挖掘了目前搜狗地圖團隊從工程角度的一些不足和啟發,比如模板更新率以及解析速度提升等。但是與支付寶業務不同的是,搜狗地圖中對於模板的定義並不是“離線包”,而是一種類似於html模板的動態解析“引擎”。
分享的現場在正式進入話題前與現場的聽眾進行了一次小小的互動:粗略統計了一下當時在場的人當中搜狗地圖的用戶比例。尷尬的是,除了出品人@winter老師礙於面子舉起了手以外,現場並沒有第三個搜狗地圖用戶(我是第二個╮(╯▽╰)╭)。當然,這也算是意料之中,搜狗地圖雖然國內的市場占有率並不高,甚至我在QCon講師微信群里打招呼后有位老師竟然問“搜狗開始做地圖了?”。“開始”這個詞用的真是很尷尬啊,那么我就先科普一下搜狗地圖的歷史吧。
搜狗地圖前身是圖行天下,成立於1999年,是國內第一家互聯網地圖服務網站,2005年被搜狐收購后改名為“搜狗地圖”。所以這個剛“開始”做的地圖產品比大多數人預料的還要老。講歷史主要不是為了科普,也不是倚老賣老,而是從側面闡明我們在進行工程化改造時所面臨的項目特征:一個有着近20年歷史包袱、模塊結構混亂的“老家伙”(PS:搜狗地圖目前的pc web地圖可以完美兼容IE5╮(╯▽╰)╭)。這樣的老項目不可能短時間內切換到全新的技術棧,也不可能大膽地使用一些比較潮的技術和框架,更多的是從策略的角度進行優化。所以我分享內容更加貼近於經驗而不是技術本身,相比較其他三位的話題,我所分享內容的方方面面幾乎是每個人都熟悉的,我們的工作便是綜合這些成熟且穩定的“常識技術”進行工程優化。
前端工程體系並不是一個固有名詞,每個團隊由於組織、業務以及架構上的不同,對於前端工程體系的理解的也不盡相同。在進入正題之前必須區分的兩個概念是:工程化與工程體系。工程化是一個動詞,意指將業務項目進行工程改造,比如合理的模塊化、前后分離等等;而工程體系是一個名詞,可以理解為工程化的外在表現以及輔助框架,比如構建、測試、部署等等。搜狗地圖前端團隊對前端工程體系的理解是:工程體系本質上是一種服務,其服務的對象是技術團隊所采用的技術以及組織架構。而架構本身也定位為一種服務,其服務的對象是具體的業務。所以在這一層三角關系之中,業務是決定所有服務的核心和出發點。我們經常將的一句話是:技術不能脫離業務。我也希望這句話能夠成為每一個技術開發者和決策者的座右銘。

從業務出發進行工程優化的第一步是提煉業務特征,從而選擇合理的技術和組織架構。我們從四個方面提取業務特征:場景、類型、設備以及平台。

以Web地圖業務為例,從進入頁面到展示完整地圖的工作流程大致如下:

地圖可以說是將按需加載發揮到極致的最佳實踐業務。大家可以想象一下,以街道為維度將北京市的全貌繪制到瀏覽器中,瀏覽器能否承載如此大的工作量?即使拋開技術的局限性,單純從需求的角度來講,用戶通常只需要查看以當前位置或者搜索位置為中心的有限區域內的地圖。所以對於地圖來說,第一步也是最重要便是定位:
- 進入頁面后首先請求定位服務,此時頁面的狀態是loading,也有人將其稱為骨架頁面;
- 定位成功后,用戶所在位置的經緯度以及對應比例尺數據決定后續瓦片數據的獲取;
- 瓦片數據請求成功后,瀏覽器端JS代碼將其排列組合最終展示出完整的局部地圖。
精確定位是非常復雜的功能,感興趣的可以自行查閱相關資料。
除了Web地圖以外,搜狗地圖前端業務的另一種主要形式是Hybrid。將這兩種業務形式進行歸納總結,提取的業務特征大致如下:

業務特征決定技術架構,最終提煉出適用於搜狗地圖前端業務的架構類型便是目前較流行的單頁應用—SPA。
不依賴與服務端渲染的SPA不論是從架構層面,還是從開發和部署層面都帶來很多便利。HTML文檔可以作為一種靜態資源與js、css等一同部署,然而從緩存處理方面,需要單獨處理HTML這種“特殊”的靜態資源。它的特殊之處便在於:HTML是所有其他靜態資源的入口。

HTML的特殊性決定它不能使用http強制緩存策略,只適用於協商緩存:

這樣可以保證各類型資源實時性的同時,最大化利用http緩存,對於常規的SPA項目(比如Web地圖)是一種比較普適的方案。然而協商緩存必須要求一次真實有效的http請求以便服務器進行緩存有效性判定,離線場景下並不適用。而離線是Hybrid應用較普遍的場景之一,后續會提到如何在協商緩存理念基礎上的優化策略。
搜狗地圖Hybrid架構經歷了三個階段,最初始的方案是:Web多頁項目+多Webview。也就是說,每個Webview承載一個Web頁面,頁面之間的切換就是Webview之間的切換,頁面之間的通信便是Webview間的通信。

這種架構一個最大的問題是:各頁面之間的通信非常不順暢,而且影響用戶體驗。如下所示的是一個非常普遍的場景:

- pageA包括兩個部分:pageB的入口、由服務端數據驅動的Content;
- pageA打開pageB的方式是新建一個Webview;
- pageB中的表單提交數據到服務端,成功后返回pageA;
- pageA需要獲取經pageB修改后的服務端數據,最簡單粗暴也是最省事的辦法就是:刷新。
這種方案存在的致命缺陷在於,pageA並不知道pageB是否提交了表單[注],所以返回pageA后不論pageB操作與否都要進行刷新。不論是從節省流量還是用戶體驗的角度來講都是負面的。
注:pageA其實有辦法獲取pageB是否進行了提交。一種方案是通過localstorage的storage事件,然而兼容性非常不理想;另一種方案是通過native提供特定的接口,這種方案雖然兼容性好但是需要客戶端的開發工作。
在上述問題的基礎上進行優化的第一步,是結合SPA架構和Webview自身的緩存機制。

Webview的緩存機制包括以下幾種:
LOAD_CACHE_ONLY- 不使用網絡,只讀取本地緩存數據LOAD_DEFAULT- 根據cache-control決定是否從網絡上取數據LOAD_NO_CACHE- 不使用緩存,只從網絡獲取數據LOAD_CACHE_ELSE_NETWORK- 只要本地有,無論是否過期,或者no-cache,都使用緩存中的數據
其中LOAD_DEFAULT 是最接近常規瀏覽器的緩存機制,在這種模式下,結合上文提到的SPA緩存策略,與常規的Web頁面並無二致。然而App並不是常規的瀏覽器,其使用場景(手機)的特殊性要求我們在一些特殊的方面進行優化,比如緩存清理和離線使用。

其中第一條是歷史原因,公司運維層面將CDN緩存有效期固定位1小時,遷移優化成本較高。http緩存過期后並不會自動清理,之所以常規瀏覽器不用顧忌這個問題是由於PC設備儲存空間大,並且可以使用電腦管家之類的優化軟件手動清理。雖然手機等移動設備的儲存空間也不斷加大,但仍然有相當一部分設備的儲存空間十分感人(我自己用的16G的iphone 7P,感同身受╮(╯▽╰)╭)。如果放任過期的http緩存不管便會造成app占用的空間越來越大,極端的用戶可能一氣之下就把app卸載了,我自己便曾經在陰陽師和狂野飆車之間做過抉擇,最終卸載了陰陽師╮(╯▽╰)╭。
所以這並不是最終合理的方案,但是這次探索給了進一步的優化工作靈感:是不是可以吸取協商緩存的理念,同時結合Webview自身的緩存機制呢?以此為方向便產生了目前采用的協商緩存理念的Hybrid模板更新策略。

模板是什么?前文提到了模板並不是靜態的離線包,而是具備動態數據解析功能的邏輯模塊。這個理念來源於SSR(服務端渲染)中的html模板,這應該是前端工程師們再熟悉不過的名詞了,前幾年尚未實現前后端分離開發時,html模板可以說是折磨前端工程師的主力之一。

模板以壓縮包的形式傳輸,進入App之后如果處於Wifi環境則會自動檢查並下載最新版本的模板包。並且在App進程運行以及掛起期間不會進行多次檢查。

具體每個模板包對應的頁面,進入之后並不會檢查模板包的版本,只要本地存在便展示,否則fallback展示線上的Web URL。這種策略是為了盡可能減少具體業務頁面的解析時間。作為fallback的Web地址采用WebView的LOAD_DEFAULT緩存策略,有效期為CDN緩存(1小時)。另外,如果用戶通過任務管理器手動殺死了App進程,下次進入App之后首先會清理之前殘留的http緩存文件。

綜上,搜狗地圖的前端工程體系簡易架構大致如下:

與常規Web項目的不同點在於,地圖項目大量使用SVG和Canvas,組件庫包括兩者相關的組件。另外,負責與native通信的bridageJS是Hybrid應用所特有的。平台層由Gitlab把關,Webhook觸發自動構建、測試和部署。另外,模板包可以由開發人員直接部署,不需要經過公司運維,這也是與常規Web項目相比的優勢之一。
由於每個模板包都會對應一個fallback的Web地址,所以在構建流程中需要針對兩種場景分別構建。模板文件對於App來說其實就是本地文件,所以模板文件中對於其他文件的引用統一使用相對地址,並且由於模板本身就是增量的,無需在靜態文件名中加入hash指紋。構建工具有Node.js為底層平台,使用特殊的環境變量結合EJS引擎區分構建,如下:

至此便是搜狗地圖目前針對SPA和Hybrid項目的整體工程體系,當然這並不是終點,甚至稱不上是最佳實踐。此次分享的目標也並不是剖析我們團隊的工程實踐,更多的是將這一路走來的探索歷程分享給大家,希望能夠給同樣面臨老項目改造的團隊一些啟發。
最后簡單提一個優化的案例。模板也是分模塊的,不能將所有的業務集中在一個模板中,否則任何一個微小的修改都會造成整個模板包的更新,而且隨着業務的不斷擴展,模板包的體積越來越大,下載和解析時間最終會超過用戶的心理承受界限。所以我們在模板顆粒度划分方面做了一些優化:將邏輯無耦合的業務定義為一個模板包,比如用戶中心與詳情頁,兩者除了登錄信息共享以外,幾乎不存在邏輯上的耦合,所以將兩者划分為兩個模板。在此基礎上將共用的類庫文件提取出來單獨作為一個模板。

如果讓我給這套工程體系打分可能只達到了60分的及格線,但是對於一個“歷史悠久”的團隊而言,這仍然是非常可觀的“一大步”。后續仍然需要不斷進行優化和迭代,比如會后與支付寶的同學一起探討的更新率問題。技術的道路遠沒有盡頭,回到一開始的那句話:技術永遠服務於業務。總結這次的QCon之行,我看到了優秀的技術從業者們以實際業務為中心的探索和務實精神,收獲的不僅僅是技術的增長,更重要的是擴寬了眼界。
最后,感謝主辦方InfoQ的邀請,完整PPT下載。
