SVG的动态之美-搜狗地铁图重构散记

 

搜狗地图发布了新版的移动端地铁图,改版初衷是为了用户交互体验的提高以及性能的改善。原版地铁图被用户吐槽最多的是pinch缩放不流畅、无过渡动画、拖拽边界不合理等等,大致上都是交互体验上的问题。实际上原版的问题不只仅存在于交互体验上,源代码也是一团糟:css

  • 无模块化概念;
  • 存在冗余逻辑和文件;
  • 滥用第三方库&工具;
  • UI的更新仍旧是直接操做DOM;
  • 构建&发布流程不规范。

以上问题其实跟业务以及技术选型无关,能够说是任何一个“历史悠久”的项目都难以免的问题。针对以上问题的重构方案不是本文要阐述的核心,因此就一笔带过。以下:html

  • 重构模块化架构;
  • 删除冗余逻辑和文件;
  • 规范并尽可能减小第三方库&工具的使用;
  • 使用Vue做为View层框架,尽可能减小直接操做DOM;
  • 规范构建&发布流程,完善工程体系。

本文重点讨论搜狗地铁图对SVG的使用和优化方案。在讨论技术细节以前,咱们先说明一下为何要使用SVG。前端

为何使用SVG

不管是从业务类型仍是操做方式的角度考虑,地铁图均可以被视为一种微型或者简易的地图。咱们能够先回想一下手机地图的一些基本操做,举几个简单的例子:html5

  • 能够缩放地图查看微观或者宏观的内容;
  • 能够点击地图上的一个POI点展现其信息,同时此POI点居中;
  • 能够经过搜索查看某个地点的完整轮廓,同时地图缩放到适合展现此地点完整轮廓的等级。

以上几种操做的技术实现须要遵循如下几个基本原则:git

  • 缩放后的地图不能展现模糊的内容,必须看上去是清晰的。也就是说,地图必须是“矢量的”[注];
  • 居中某一个点则必须知道此点的坐标信息,而后结合浏览器坐标体系和viewport尺寸计算出正确的展现内容;
  • 完整展现某个轮廓则必须知道此轮廓的尺寸以及坐标,而后结合浏览器坐标体系和viewport尺寸计算出正确的展现内容;

注:之因此将“矢量”加引号是由于地图的实现包括栅格瓦片和矢量瓦片两种不一样的技术方案。顾名思义,矢量瓦片是真正意义上的矢量地图,由OpenGL或者WebGL实现;而由栅格瓦片实现的地图并非矢量的,缩放时会看到明显的模糊效果,可是缩放动做完成后会展现对应等级的栅格图片,也就是说缩放后的内容是清晰的,只是缩放过程当中存在模糊效果。随着WebGL的普及,栅格瓦片技术逐渐退出了历史舞台。github

简单归纳,地图必须是:web

  • 矢量的;
  • 动态的。

即便是栅格瓦片地图,POI点也是动态绘制的,感兴趣的读者能够自行查阅相关信息。浏览器

地铁图一样如此,而Web展现矢量内容只有两种方案:WebGL和SVG。虽然WebGL更富有视觉表现力,可是地铁图业务的体量较小,并无达到值得用WebGL实现的程度,因此SVG便成了惟一的选择。前端工程师

旧版地铁图的核心问题

旧版的搜狗地铁图虽然也是使用SVG绘制UI,可是并无将SVG的动态优点发挥出来,而是将其视为静态的图片。图1是旧版地铁的DOM结构:架构

蓝色框的svg是地铁图的UI内容,除了尺寸之外没有任何其余的属性。红色框是地铁图外层容器,能够看到全部的偏移、缩放等交互都是借由外层容器的transform实现。黑色框的各个DOM节点包括了定位、求路、信息气泡等内容,这些DOM每每须要跟随用户操做被改动,并且某些操做可能须要同时操做多个DOM。

接下来咱们看看这样的DOM结构存在什么问题。

定位、求路、信息气泡等内容是与地铁图强耦合的,假设我选中了某个地铁站,如图2

红色框内的信息气泡对应到上图的container3节点,地铁底图对应container1节点。若是此时咱们拖拽地铁图,底图和信息气泡都会随着手势而改变位置,那么就须要同时改变container1container3的位置。

咱们把一样的问题带入到求路,如图3

我并无画出每一个UI对应的节点,由于实在是太多了。上图中包括了2个转乘节点、2个起终节点和3个气泡节点,拖拽过程当中这7个DOM节点所有须要被操做。而且不只仅是改写DOM属性那么简单,而是须要先获取每一个节点的坐标而后再进行计算,而咱们都知道,获取DOM的offset是很是消耗性能的。此外,求路状态下的地铁图必须缩放到完整展现求路路线的等级,那么就须要计算求路路线的轮廓尺寸,其中也会涉及到大量的计算和DOM操做。

其实拖拽是很是基本的操做,若是是缩放呢?抛开大量的计算和DOM操做不谈,从视觉上表现如图4所示:

为何气泡和起终点等节点没有同比例缩放?由于这些节点不是矢量的SVG,缩放会失真。若是想获得“矢量”的缩放效果只能从新计算这些节点的尺寸,这样的代价太大了。因此咱们不得不忍受这些问题。

总结以上的问题能够归纳出两点:

  • 坐标和求路轮廓的获取很是消耗性能;
  • 部分UI不能缩放。

以上问题的症结能够概括为:

  • 缩放和拖拽操做所有借由container1实现,坐标的获取只能借助于常规的DOM API;
  • DOM结构不合理,定位、求路、信息气泡等节点应该是矢量的,且应该被同步缩放。

简单来说,旧版地铁图的核心问题是DOM结构不合理,而且没有把SVG的动态特性发挥出来。

重构方案

重构后的DOM结构如图5所示:

 

  • handler节点负责直接响应手势操做,拖拽、缩放等操做首先会改变handlertransform样式;
  • container节点是svg容器,负责以浏览器窗口为参考将地铁图居中;
  • view节点是全部与地铁图展现相关内容的容器,包括底图、定位、气泡、求路等等等等。同时,手势操做最终会修改view的transform属性,以实现地铁图自己的缩放。

以上说明可能有些难以理解,咱们用具象的图形加以说明。分层的结构大体如图6所示,从外到里分别是handler/container/view:

此时若是用户进行了手势操做,以pan-拖动为例:

  1. panstart事件触发后记录拖动的初始坐标,不影响分层结构中的任何一层,也就是说不改变任何一层的任何属性或样式;
  2. 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如何实现缓动效果?

这里须要注明两个前提知识点:

  1. SVG的transform是一个属性,与CSS的transform是两个不一样的概念,二者使用的坐标体系有必定差别;
  2. SVG没有相似CSS transition的属性,也就是说SVG没有原生支持过渡动画的功能。

关于SVG transform的详细知识能够参考理解SVG transform坐标变换

因此若是咱们在view的transform上下功夫实现缓动动画的话,只能经过JS结合缓动公式和requestAnimationFrame计算每一帧的SVGtransform值,或者使用第三方现有的动画工具库,好比TweenJStransform的计算很是复杂,尤为是同时存在scaletransiton的场景下。既然CSS的transiton可使用浏览器提供的缓动动画,那咱们为何不把复杂的工做交给浏览器呢?transiton做为偏移、缩放的缓动动画媒介必须搭配CSS的transform,可是咱们不能直接经过view的style修改transform。缘由有二:

  1. CSS的transform和SVG的transform不能等同;
  2. 咱们须要借助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节点的尺寸会影响它自身的lefttop,上图中红色标注是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坐标的计算必定很复杂吧?这个问题的有两个难点:

  1. CSS与SVG坐标的差别性;
  2. 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表示,而是直接用translate3dscale非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节点transformtransform-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。

随后用户进行拖拽和缩放操做后,拖拽边界便随之动态变化。计算动态拖拽边界的时候须要考虑两点:

  1. 缩放中心点坐标,即transform-origin,是重要的计算因子;
  2. 左拖动边界始终为负数或者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";
  • 直接操做DOMthis.$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站点,这种特殊性也提供了进一步优化的空间,这是重构工做的第二步。因此在本次地铁图重构项目过程当中能够提炼出重构的两个基本点:

  1. 从技术架构的角度思考;
  2. 从业务特征的角度思考。
相关文章
相关标签/搜索