搜狗地圖發布了新版的移動端地鐵圖,改版初衷是為了用戶交互體驗的提升以及性能的改善。原版地鐵圖被用戶吐槽最多的是pinch縮放不流暢、無過渡動畫、拖拽邊界不合理等等,大體上都是交互體驗上的問題。實際上原版的問題不僅僅存在於交互體驗上,源代碼也是一團糟:
- 無模塊化概念;
- 存在冗余邏輯和文件;
- 濫用第三方庫&工具;
- UI的更新仍舊是直接操作DOM;
- 構建&發布流程不規范。
以上問題其實跟業務以及技術選型無關,可以說是任何一個“歷史悠久”的項目都難以避免的問題。針對以上問題的重構方案不是本文要闡述的核心,所以就一筆帶過。如下:
- 重構模塊化架構;
- 刪除冗余邏輯和文件;
- 規范並盡量減少第三方庫&工具的使用;
- 使用Vue作為View層框架,盡量減少直接操作DOM;
- 規范構建&發布流程,完善工程體系。
本文重點討論搜狗地鐵圖對SVG的使用和優化方案。在討論技術細節之前,我們先說明一下為什么要使用SVG。
為什么使用SVG
不論是從業務類型還是操作方式的角度考慮,地鐵圖都可以被視為一種微型或者簡易的地圖。我們可以先回想一下手機地圖的一些基本操作,舉幾個簡單的例子:
- 可以縮放地圖查看微觀或者宏觀的內容;
- 可以點擊地圖上的一個POI點展示其信息,同時此POI點居中;
- 可以通過搜索查看某個地點的完整輪廓,同時地圖縮放到適合展示此地點完整輪廓的等級。
以上幾種操作的技術實現需要遵循以下幾個基本原則:
- 縮放后的地圖不能展示模糊的內容,必須看上去是清晰的。也就是說,地圖必須是“矢量的”[注];
- 居中某一個點則必須知道此點的坐標信息,然后結合瀏覽器坐標體系和viewport尺寸計算出正確的展示內容;
- 完整展示某個輪廓則必須知道此輪廓的尺寸以及坐標,然后結合瀏覽器坐標體系和viewport尺寸計算出正確的展示內容;
注:之所以將“矢量”加引號是因為地圖的實現包括柵格瓦片和矢量瓦片兩種不同的技術方案。顧名思義,矢量瓦片是真正意義上的矢量地圖,由OpenGL或者WebGL實現;而由柵格瓦片實現的地圖並不是矢量的,縮放時會看到明顯的模糊效果,但是縮放動作完成后會展示對應等級的柵格圖片,也就是說縮放后的內容是清晰的,只是縮放過程中存在模糊效果。隨着WebGL的普及,柵格瓦片技術逐漸退出了歷史舞台。
簡單概括,地圖必須是:
- 矢量的;
- 動態的。
即使是柵格瓦片地圖,POI點也是動態繪制的,感興趣的讀者可以自行查閱相關信息。
地鐵圖同樣如此,而Web展示矢量內容只有兩種方案:WebGL和SVG。雖然WebGL更富有視覺表現力,但是地鐵圖業務的體量較小,並沒有達到值得用WebGL實現的程度,所以SVG便成了唯一的選擇。
舊版地鐵圖的核心問題
舊版的搜狗地鐵圖雖然也是使用SVG繪制UI,但是並沒有將SVG的動態優勢發揮出來,而是將其視為靜態的圖片。圖1是舊版地鐵的DOM結構:
藍色框的svg是地鐵圖的UI內容,除了尺寸以外沒有任何其他的屬性。紅色框是地鐵圖外層容器,可以看到所有的偏移、縮放等交互都是借由外層容器的transform實現。黑色框的各個DOM節點包括了定位、求路、信息氣泡等內容,這些DOM往往需要跟隨用戶操作被改動,而且某些操作可能需要同時操作多個DOM。
接下來我們看看這樣的DOM結構存在什么問題。
定位、求路、信息氣泡等內容是與地鐵圖強耦合的,假設我選中了某個地鐵站,如圖2:
紅色框內的信息氣泡對應到上圖的container3
節點,地鐵底圖對應container1
節點。如果此時我們拖拽地鐵圖,底圖和信息氣泡都會隨着手勢而改變位置,那么就需要同時改變container1
和container3
的位置。
我們把同樣的問題帶入到求路,如圖3:
我並沒有畫出每個UI對應的節點,因為實在是太多了。上圖中包括了2個轉乘節點、2個起終節點和3個氣泡節點,拖拽過程中這7個DOM節點全部需要被操作。並且不僅僅是改寫DOM屬性那么簡單,而是需要先獲取每個節點的坐標然后再進行計算,而我們都知道,獲取DOM的offset是非常消耗性能的。此外,求路狀態下的地鐵圖必須縮放到完整展示求路路線的等級,那么就需要計算求路路線的輪廓尺寸,其中也會涉及到大量的計算和DOM操作。
其實拖拽是非常基本的操作,如果是縮放呢?拋開大量的計算和DOM操作不談,從視覺上表現如圖4所示:
為什么氣泡和起終點等節點沒有同比例縮放?因為這些節點不是矢量的SVG,縮放會失真。如果想得到“矢量”的縮放效果只能重新計算這些節點的尺寸,這樣的代價太大了。所以我們不得不忍受這些問題。
總結以上的問題可以概括出兩點:
- 坐標和求路輪廓的獲取非常消耗性能;
- 部分UI不能縮放。
以上問題的症結可以歸納為:
- 縮放和拖拽操作全部借由
container1
實現,坐標的獲取只能借助於常規的DOM API; - DOM結構不合理,定位、求路、信息氣泡等節點應該是矢量的,且應該被同步縮放。
簡單來講,舊版地鐵圖的核心問題是DOM結構不合理,並且沒有把SVG的動態特性發揮出來。
重構方案
重構后的DOM結構如圖5所示:
handler
節點負責直接響應手勢操作,拖拽、縮放等操作首先會改變handler
的transform
樣式;container
節點是svg容器,負責以瀏覽器窗口為參考將地鐵圖居中;view
節點是所有與地鐵圖展示相關內容的容器,包括底圖、定位、氣泡、求路等等等等。同時,手勢操作最終會修改view
的transform屬性,以實現地鐵圖本身的縮放。
以上說明可能有些難以理解,我們用具象的圖形加以說明。分層的結構大致如圖6所示,從外到里分別是handler/container/view:
此時如果用戶進行了手勢操作,以pan-拖動為例:
panstart
事件觸發后記錄拖動的初始坐標,不影響分層結構中的任何一層,也就是說不改變任何一層的任何屬性或樣式;panmove
事件頻繁觸發,即拖動過程中,映射為handler層transform
的改動,container和View無任何變化。如下圖7:
3. pancancel/panend
事件觸發后修正handler合理的偏移量(詳情請閱讀下文的邊界控制),同時將修正后的transform
屬性值換算為view的transform
,最后將handler的transform
歸零。如圖8:
代碼如下:
1 /** 2 * @constant PrevOffset 前一次拖拽的坐標偏移量 3 * @type {Object} 4 */ 5 const PrevOffset = { 6 x: undefined, 7 y: undefined 8 }; 9 10 EventRuntime.on('panstart panmove pancancel panend', e => { 11 e.preventDefault(); 12 e.srcEvent.stopPropagation(); 13 // panstart事件記錄初始坐標 14 if (e.type === 'panstart') { 15 PrevOffset.x = e.deltaX; 16 PrevOffset.y = e.deltaY; 17 } else if (e.type === 'panmove') { 18 // handler位移設置增量 19 subway.setTranslate(e.deltaX - PrevOffset.x, e.deltaY - PrevOffset.y); 20 PrevOffset.x = e.deltaX; 21 PrevOffset.y = e.deltaY; 22 } else { 23 // 拖拽結束后換算hander和view的transform,同時修正合理偏移量 24 subway.adjustTransform('translate'); 25 } 26 });
分層結構中三者的作用可以簡單概括為:
- handler負責展示用戶操作進行中的動態地鐵圖;
- container只是容器,一經設定不再改動;
- view負責展示無用戶操作狀態下的靜態地鐵圖。
可能你會疑問為什么不直接改變view的transform
?額外加一層handler的作用是什么?在回答這個問題之前我們不妨先思考一下如果直接改變view的transform
來響應拖動和縮放會有哪些不足。
Handler - 緩動動畫與GPU加速
動畫是前端交互中的重點,為了提供順暢的操作體驗,最典型的優化動畫方向是:
- 使用緩動;
- 優化性能。
緩動動畫
搜狗地鐵圖有三種基本的操作:
1) 點擊某個站點,將此站點居中,期間有緩動動畫如下圖9:
2) 拖動到地鐵圖邊界后,拖動結束(即手指離開屏幕)后需要修正拖動邊界,否則會停留在拖動結束的狀態可能造成大面積空白。這種修正類似Safari IOS的橡皮筋效果。修正過程中有緩動動畫如下圖10:
3) 與拖動類似,縮放同樣有邊界限制,否則會無限制的放大/縮小。修正縮放邊界期間有緩動動畫如下圖11:
GIF圖片表現力有限,不能表現完美的效果。體驗真實的效果請下載搜狗地圖APP進入到地鐵圖查看。
回到最初的問題:如果直接改變view的transform
如何實現緩動效果?
這里需要注明兩個前提知識點:
- SVG的
transform
是一個屬性,與CSS的transform
是兩個不同的概念,兩者使用的坐標體系有一定差異; - SVG沒有類似CSS
transition
的屬性,也就是說SVG沒有原生支持過渡動畫的功能。
關於SVG transform的詳細知識可以參考理解SVG transform坐標變換。
所以如果我們在view的transform
上下功夫實現緩動動畫的話,只能通過JS結合緩動公式和requestAnimationFrame
計算每一幀的SVGtransform
值,或者使用第三方現有的動畫工具庫,比如TweenJS。transform
的計算非常復雜,尤其是同時存在scale
和transiton
的場景下。既然CSS的transiton
可以使用瀏覽器提供的緩動動畫,那我們為什么不把復雜的工作交給瀏覽器呢?transiton
作為偏移、縮放的緩動動畫媒介必須搭配CSS的transform
,但是我們不能直接通過view的style修改transform
。原因有二:
- CSS的
transform
和SVG的transform
不能等同; - 我們需要借助SVG的
transform
進行邊界控制(下文詳述),也就是說偏移和縮放的效果最終需要換算為SVG的transform
但在動畫執行期間不能修改。
那么我們便得出了handler存在必要性的證明之一,也就是優化動畫的第一條:緩動。接下來我們嘗試進一步優化動畫的性能。
GPU加速
我們都知道CSS的3Dtransform
可以強制啟用GPU加速以優化動畫的表現,自然會想到SVG可不可以使用GPU加速呢?很可惜,答案是否定的。SVG是一種表現2D矢量圖形的技術,它在設計之初便沒有考慮3D的場景,所以SVG並沒有3Dtransform
,也無法借助GPU對動畫進行加速。
那么我們便得出了handler存在必要性的第二個證明:GPU加速。
其實業內對於借助GPU加速動畫的方案褒貶不一,即便是啟用GPU加速也有方案的優劣。我們此次重構只是第一步,后續仍舊會不斷探索進一步的優化方案。
transform-origin
SVG沒有transform-origin
概念,transform
的原點永遠都是自身的左上角,即(0,0)
。
大家可以想象一下在手機上用兩根手指縮放地鐵圖的場景,我們需要知道地鐵圖應該以屏幕上的哪一點作為中心進行縮放。從技術角度來講,我們需要知道兩個觸控點的中心位置坐標。不論是IOS系統原生的gesture
事件,還是通過touch事件模擬的pinch
事件(如HammerJS)使用的都是瀏覽器坐標系,也就是CSS坐標系。
如果一定要把中心點坐標映射到SVG坐標系,則需要一定的計算量(下文詳述)。在縮放操作過程中需要頻繁地改變被縮放DOM的transform
從而引起重繪(re-render),這期間瀏覽器本身就進行着大量計算,所以在應用程序層面應該盡可能減少計算量。
關於重繪和重排,可以參考瀏覽器的重繪與重排。
這也是handler節點存在必要性的第三個證明:減輕計算量。
有了handler節點的輔助,縮放操作進行中(請注意是進行中,不包括起始和結束時刻)唯一的計算便是handler的transform
,無需將其轉換為SVG的transform
。當然,換算仍然是必須的,但是我們將其推遲到縮放操作結束之后進行,這樣便可以在一次完整的操作流程中只進行一次換算工作,大大減少了總體的計算量。具體的換算公式下文詳述。
Container - 地鐵圖居中
上文並沒有過多的描述container節點,因為它的作用非常簡單。container作為svg的容器,同時在初始化時以瀏覽器窗口為參考將地鐵圖居中。如下圖12所示:
- 灰色的部分為svg節點;
- 白色的部分為地鐵圖線路的真實區域;
- 中間的長方形為瀏覽器窗口,同時也是handler節點的尺寸。
container節點的高寬均為2000,決定這個數字的唯一原則是:只要比view節點的尺寸大即可。所以我們設置了一個比較大的值。container節點的尺寸會影響它自身的left
和top
,上圖中紅色標注是container節點居中的偏移量:
1 Offset.x = (container.width - window.innerWidth)/2; 2 Offset.y = (container.height - window.innerHeight)/2;
那么container節點的CSS便是:
1 container.style.cssText = [ 2 'postion: absolute;', 3 `left: -${Offset.x};`, 4 `top: -${Offset.y};` 5 ].join('');
transform是應用到view節點,邊界控制同樣是以view節點的尺寸為計算因子。所以,在初始化之后container不再進行任何改動,它的作用至此便完全體現了。
transform是應用到view節點,邊界控制同樣是以view節點的尺寸為計算因子。所以,在初始化之后container不再進行任何改動,它的作用至此便完全體現了。
View - 靜態展示與邊界控制
CSS與SVG的transform換算
可能你會冒出這樣一個疑問:handler使用的是CSS的坐標體系,那么它的transform
要換算成SVG坐標的計算一定很復雜吧?這個問題的有兩個難點:
- CSS與SVG坐標的差異性;
- SVG沒有
transform-origin
的概念和功能,但是我們需要借助CSS的transform-origin
計算縮放中心,這進一步復雜化了換算邏輯。
必要知識點
CSS與SVG坐標的差異性
如果SVG設置了viewBox
屬性,那么它所使用的坐標系便不同於CSS坐標系。此外,SVG的preserveAspectRatio
也會影響坐標系的細節。這兩個屬性在實現SVG縮放時非常關鍵,但搜狗地鐵圖並沒有借助viewBox
實現縮放,而是將全部的展示交給了view節點的transform
,一定程度上減輕了CSS和SVG坐標差異性造成的計算復雜度。同時,我們將preserveAspectRatio
屬性值設置為"xMinYMin meet"
,即強制寬高等比例縮放。
遠於SVG坐標系的更多細節可以參考理解SVG坐標系和變換:視窗,viewBox和preserveAspectRatio
剩下的問題就是如何將CSS的transform-origin
換算成SVG的transform
了。
SVG的“transform-origin
”
SVG與CSStransform
的相同點是:兩者都是以自身為變換坐標系。但SVG的transform
原點不能改變,永遠都是自身的左上角,即(0,0)
。
那么SVG如何實現類似CSStransform-origin
效果呢?
假設我想讓SVG以點(50,30)
為原點放大1.5倍,我需要按照下述順序依次對SVG進行變換:translate(50 30)
->scale(1.5 1.5)
-> translate(-50 -30)
。先將SVG偏移到點(50,30)
;然后再將SVG放大1.5倍(請謹記SVGtransform
的原點是自身的左上角);最后再將SVG反向偏移(50,30)
。具體變換過程可以參考圖13:
更多技術細節請參考這篇文章。
SVG的transform
屬性值為translate(50 30) scale(1.5 1.5) translate(-50 -30)
。由於地鐵圖的操作頻繁是,涉及到大量變換,所以我們用matrix表示。以上的transform
屬性值換算為matrix表示為matrix(1.5 0 0 1.5 ${(1-1.5)*50} ${(1-1.5)*30})
。
至此我們便總結出SVG以點(ox,oy)
為原點進行縮放的transform
計算公式:
transform = matrix(sx 0 0 sy (1-sx)*ox (1-sy)*oy)
接下來我們根據以上的前提知識點推導出具體的換算公式。
換算公式
為了更清晰地推算換算公式,我們假設在縮放地鐵圖之前已經有了一定的偏移量和縮放比例,如下圖14:
假設此時View節點的transform
屬性值為matrix(scale 0 0 scale dx dy)
,簡化為:
View.scale
- view節點的初始縮放值;View.dx
&View.dy
- View節點的初始偏移量。
因為我們為SVG設置了
preserveAspectRatio="xMinYMin meet"
,即強制寬高等比例縮放,所以scaleX = scaleY,我們統一使用scale表示。
同時我們將handler的樣式設置為:
1 `transform: translate3d(${dx}, ${dy}, 0px) scale(${scale});` 2 `transform-origin: ${ox} ${oy} 0px;`
即:
Handler.dx
&Handler.dy
- handler節點的偏移量;Handler.scale
- handler節點的縮放值;Handler.
ox&Handler.oy
- handler節點的transform-origin
坐標。
需要特別注意的一點是,handler節點的transform
我們並未使用matrix表示,而是直接用translate3d
和scale
。非matrix表示transform
時的變換順序非常重要,按照從左往右的順序后面的變換是以前面的變換為基礎。也就是說,handler節點的transform
是先進行translate3d
-偏移變換,然后在偏移之后的狀態基礎上再進行scale
-縮放變換。
另外還有一個重要前提:目前版本我們將縮放和拖動操作割裂開,同一時間只能進行縮放或者拖動操作。也就是說,縮放操作只改變Handler.scale和Handler.ox&Handler.oy,拖動操作只改變Handler.dx&Handler.dy。后續版本會探索將兩種操作耦合的可行性方案。
scale換算
接下來我們詳細講解一下scale的換算公式,大家請先仔細研究下圖15所示的縮放狀態
- 白色區域內的黑色虛線框為View節點的初始化位置,也就是在用戶進入頁面后沒有任何操作的狀態;
- 白色區域內的藍色虛線框為上文我們假設的縮放之前的狀態,假定此時View節點的
transform
屬性值為matrix(scale 0 0 scale dx dy)
; - 白色區域內的紅色虛線框為縮放1.2倍之后的View節點(大框)和Handler節點(小框)尺寸。請注意此時我們還未將Handler節點的transform換算為View節點,由於View是Handler的子節點,所以它繼承了Handler的transform樣式,被同比例縮放;
- 黑色實線框代表瀏覽器窗口,灰色區域為Container節點,兩者在縮放過程中均未改變。
此時對應的DOM狀態如下圖16所示
- Handler節點以
(50px,40px)
為原點縮放了1.2倍; - 縮放之前View節點的初始
transform="matrix(1.1 0 0 1.1 194 75)"
,即縮放了1.1倍,X軸偏移194,Y軸偏移75。
接下來要做的事情是吧Handler的transform
以及transform-origin
換算為SVG的transform
,然后將Handler節點transform
和transform-origin
歸零。換算公式如下:
1 View.scale = View.scale * Handler.scale; 2 View.dx = View.dx + (1 - Handler.scale)*(Handler.ox + Offset.x - View.dx); 3 View.dy = View.dy + (1 - Handler.scale)*(Handler.oy + Offset.y - View.dy);
公式的推導過程並不復雜,因為我們並沒有改變SVG的Viewbox,所以其坐標系與CSS坐標系並無二致。所以只需要將場景代入CSS坐標系,同時將transform-origin
設置為(0,0)
,在此前提下進行推導公式便非常簡單了。
將CSS的transform-origin設置為’0,0’后,transform的規則與SVG的transform便完全一樣了。如果你熟悉CSS的transform,SVG的transform便不會有任何問題。因為CSS的transform屬性本身就是從SVG的transform借鑒而來,只是加入了transform-origin這個語法糖。
邊界控制
顧名思義,邊界控制的作用是限制地鐵圖的可操作邊界,包括拖拽邊界和縮放邊界。拖拽邊界指的是地鐵圖上下左右四個方向上的可拖動的最大距離。縮放邊界指的是地鐵圖可被縮放的最大和最小比例。兩種邊界控制的具體的交互表現可參考上文“緩動動畫”一節的圖10和圖11。
拖拽邊界
從圖12很容易得出初始的拖拽邊界,請參考以下偽代碼:
ViewBox <- 計算View的坐標和尺寸 Viewport <- 獲取瀏覽器的尺寸 Offset <- 計算Container相對瀏覽器的偏移量 THEN 往右拖動的最大距離MaxX = Offset.x - BBox.x 往左拖動的最大距離MinX = ViewBox.width-(Offset.x - BBox.x + Viewport.width) 往下拖動的最大距離MaxY = Offset.y - BBox.y 往上拖動的最大距離MinY = ViewBox.height-(Offset.y - BBox.y + Viewport.height)
注意,因為拖拽的邊界最終映射到translate
上,所以左拖動邊界和上拖動邊界的值是上述偽代碼所計算出來結果的相反數,即始終為負數或者0。
隨后用戶進行拖拽和縮放操作后,拖拽邊界便隨之動態變化。計算動態拖拽邊界的時候需要考慮兩點:
- 縮放中心點坐標,即
transform-origin
,是重要的計算因子; - 左拖動邊界始終為負數或者0,並且必須小於右拖動邊界,上下拖動邊界同理。
將以上規則帶入計算,偽代碼如下:
Viewport <- 獲取瀏覽器的尺寸 TransformOrigin <- transform-origin的值 Scale <- 縮放比例 Translate <- 偏移量 THEN 往右拖動的最大距離MaxX = Prev_MaxX*Scale + TransformOrigin.x*(Scale-1) - Translate.dx; 往左拖動的最大距離MinX = Prev_MinX*Scale - (Viewport.width-TransformOrigin.x)*(Scale-1) - Translate.dx; 往下拖動的最大距離MaxY = Prev_MaxY*Scale + TransformOrigin.y*(Scale-1) - Translate.dy; 往上拖動的最大距離MinY = Prev_MinY*Scale - (Viewport.height-TransformOrigin.y)*(Scale-1) - Translate.dy; THEN 修正 MinX: MinX<MaxX?MinX:Math.min(0,MinX) MaxX: MaxX>MinX?MaxX:Math.max(1,MaxX) MinY: MinY<MaxY?MinY:Math.min(0,MinY) MaxY: MaxY>MinY?MaxY:Math.max(1,MaxY)
這些公式的推導過程說復雜也復雜,說簡單其實也很簡單。道理與上文的scale換算一樣,因為SVG的viewBox沒有改變,所以只需將SVG帶入CSS坐標系即可迎刃而解。篇幅所限,具體的推導過程便不再贅述。
縮放邊界
與拖拽邊界不同的是,縮放邊界是固定的,一經初始化便不會再改動。具體如何控制縮放的邊界其實並沒有統一的方案,不同的團隊可能有不同的見解,比如高德和百度的地鐵圖最小縮放比例小仍然無法展示底圖的全貌。搜狗地鐵圖在評審和開發過程中有過幾次商討,最終定下的方案是:
- 最大縮放比例寫死為1.5倍;
- 最小縮放比例以完整展示當前城市的地鐵全貌為准。
也就是說,不同城市地鐵圖的最小縮放比例是不同的,因為每個城市的地鐵線路個數、長度均有所差異,需要動態計算。計算的方法很簡單,唯一需要注意的是一定要將瀏覽器的寬高比作為計算的因子。請參考以下偽代碼:
ViewBox <- 計算View的坐標和尺寸 Viewport <- 獲取瀏覽器的尺寸 AspectRatioOfWindow <- 瀏覽器的寬高比 THEN 最大縮放比例 = 1.5 最小縮放比例 = ViewBox.width/ViewBox.height < AspectRatioOfWindow ? Viewport.height/ViewBox.height : Viewport.width/ViewBox.width;
其實我個人覺得高德和百度的方案更佳,因為手機屏幕尺寸比較小,即使展示地鐵全貌也看不清楚細節,索性不如將最小比例寫死為一個能夠看清楚細節的臨界值。這樣不僅能減少計算量,而且從整體交互上也比較人性化。但是胳膊擰不過大腿,最終還是信了PM的邪。。。
直接操作DOM更快
為什么要把這一條單拎出來講,是想提醒一下大家千萬不要一味的追求所謂的流行技術和框架。我曾經見過很多前端工程師在介紹React/Vue的優點時一定要唾棄直接操作DOM和jQuery/PrototypeJS等“老家伙們”。不可否認React/Vue確實很大程度上解放了生產力,但是並非所有的場景均適合使用它們,比如地鐵圖的手勢操作。地鐵圖響應手勢操作的過程中需要頻繁的改變底圖的transform
,那么請大家思考以下兩種方式哪個性能更好:
- 使用Vue的
v-bind:transform="transform"
; - 直接操作DOM
this.$refs.handler.cssText=transform
第二種實現是不是Vue的“反模式”?仁者見仁。但是從實際效果來看第二種具有絕對的性能優勢,其背后的道理很簡單。對於手勢操作這種幾乎每一幀都需要響應的場景來說,邏輯越少越好,而Vue在改變DOM之前需要處理一系列復雜的邏輯,與直接操作DOM相比,性能孰好孰壞顯而易見。
Vue的動態綁定把DOM操作封裝在框架內部,高內聚的框架讓開發者無需關心具體實現,但是基本的原理仍然未脫離DOM這一核心因素。
數據優化
加載優化
舊版數據加載流程及問題
首先加載主邏輯文件index.js,然后index.js中的邏輯獲取url的城市參數名稱,隨后異步加載對應城市的數據文件,加載完成后進行解析和渲染。如下圖:

這種流程對於常規的web站點沒有任何問題,因為常規的web網站所有城市共用一套代碼,只能從參數區分城市名稱。但是Hybrid地鐵圖使用的是離線包而不是web站點,每個城市均打包為對應名稱的離線包,比如北京的源碼被打包為beijing.zip。也就是說,每個城市的代碼是互不影響的,這是優化的重要前提。
優化方案
針對離線包的構建流程中加入額外的功能,即把每個城市的數據js引用在構建階段注入到index.html中。如下:
這樣可以實現數據文件的同步加載,與舊版的對比節省了以下時間:
- index.js從URL中獲取城市名稱的時間;
- index.js創建引用源為城市數據文件script標簽的時間,這屬於耗時的DOM操作;
- 異步加載數據文件的時間。
需要說明的是,雖然單純加載數據文件,不論是同步還是異步方式,兩者的時間完全一致。但是如果按照原本的異步加載流程,數據文件便無法利用瀏覽器http並行加載的優勢,即使這個時間可能微乎其微。
解析優化
舊版數據解析流程及問題
歷史原因,地鐵數據被制備為XML格式的字符串,解析數據需要先將其轉換為XML對象,然后再轉換為JSON格式。且所有的解析工作均在客戶端瀏覽器執行,如下:
優化方案
將數據的解析工作提前到源碼構建階段,客戶端直接接觸的是解析后的JSON格式數據,減少客戶端負載和用戶的等待時間。如下:
此外,舊版的解析數據中存在大量冗余的字段,本次重構將這些冗余字段刪除,進一步減小了文件體積。
優化前后對比
以北京的地鐵數據為例,分別對比優化前后的數據文件的體積以及解析所消耗的時間。
1> 文件體積
- | XML | JSON-未優化 | JSON-優化 |
未壓縮 | 145KB | 288KB | 149KB |
壓縮 | 30KB | 58KB | 31KB |
結論:單純從文件體積衡量,優化前后的差距幾乎可以忽略。
2> 解析時間
設備信息:
- 平台:Macbook
- CPU:2.7 GHz Intel Core i5
- 內存:8 GB 1867 MHz DDR3
模擬環境:Chrome
測試結果(取十次平均值):
設備性能 | 原始 | 慢4倍 | 慢6倍 |
解析時間-優化前 | 45.6ms | 281.2ms | 294.3ms |
解析時間-優化后 | 0 | 0 | 0 |
結論:優化后無需解析,直接進行底圖渲染。設備性能越差,優化前后的對比越明顯。
總結
技術棧本身並無好壞之分,優劣體現在與業務的契合度上。老版本搜狗地鐵圖的問題核心並非在於技術棧的不合理,甚至以當時開發第一版地鐵圖的時間節點來看,其技術棧算得上優秀。技術架構和實現方式上的混亂是造成老版本地鐵圖性能和交互問題的根本。
優化技術架構是重構的第一步,但完成架構的升級只算完成了一半。特殊的運行方式(離線包)決定了不能將地鐵圖等同為常規的Web站點,這種特殊性也提供了進一步優化的空間,這是重構工作的第二步。所以在本次地鐵圖重構項目過程中可以提煉出重構的兩個基本點:
- 從技術架構的角度思考;
- 從業務特征的角度思考。