1. 项目定位与设计思路
1.1 lingbot-map 到底解决什么问题
lingbot-map 是我目前在一套机器人业务系统里维护的地图组件库,最初立项时只有一个朴素需求:业务侧需要在一个 web 环境里展示机器人所在场地的实时位置、移动轨迹、工作状态,同时还要能在地图上交互式地配置点位、框选区域、打点标注。这套能力如果完全从零自研,周期太长,直接套用现成的地图大全家桶,又没法灵活嵌入到现有的多端框架里,于是才有了 lingbot-map 这个项目。
它本质上是"地图渲染层 + 业务数据层 + 交互控制层"的轻量封装,不依赖任何重型 GIS 引擎,核心渲染基于 Canvas 2D,底层瓦片数据可以对接开源瓦片源,也能在后端预渲染好业务专有的底图。对上层业务,只暴露一个容器节点和一套干净的 API,地图的缩放、平移、旋转、图层管理、事件派发全部由组件库内部消化。
这个项目适合谁?两类人最合适参考:一类是在做室内机器人调度、可视化管理平台的前端开发,另一类是需要自己控制地图渲染细节而不想被大而全的地图 SDK 绑死的工具开发者。如果你只是要在传统网页里展示标准经纬度地图,直接切现成方案更省事;但如果你需要自定义底图、大量动态点位、特殊旋转逻辑、甚至离线场景下的降级渲染,lingbot-map 的这套设计思路会有实在的参考价值。
1.2 命名里的隐藏信息
"lingbot-map" 拆开看其实有两层含义。第一层是 lingbot 这个业务代号,对应的是"语言指令驱动的机器人"这套整体系统,map 是这个系统里面的地图子模块;第二层是我在设计时私心埋入的含义——"ling" 取意"灵活的连接",因为整个地图库最核心的设计目标就是保证"连接"的可靠性:地图数据源和渲染层的连接、业务数据与图层事件的连接、用户手势和地图状态的连接。
我在实际开发中最大的体会是,地图组件库的复杂度从来不在"画出地图"这一步,而是在这些连接关系的设计上。瓦片请求和渲染更新之间的连接如果做得不够干净,就会出现地图拖一下崩一下的尴尬局面;点位数据和聚合逻辑之间的连接如果耦合太紧,后期每加一种业务状态都要动渲染核心,维护成本会一路飙高。因此 lingbot-map 在命名层面的暗示,实际上决定了整个项目的架构走向,所有模块都围绕"连接层"做文章,而不是围绕"渲染层"做文章。
1.3 项目的核心能力清单
- 基础地图能力:多底图源配置、快速切换、缩放平移旋转、指北针复位、视图中心坐标实时回调。
- 业务图层能力:标注点图层、路径线图层、动态轨迹、热力图、区域面图层,图层之间支持独立显隐和透明度控制。
- 交互能力:点击、双击、拖拽、滚轮缩放、双指手势、框选、测距。事件机制统一走"视图坐标到地理坐标"的逆算管道,业务侧拿到的一律是地理坐标。
- 状态同步能力:地图视图状态(中心点、缩放级别、旋转角)支持序列化成对象,也支持从对象恢复,这对机器人远程监控和回放功能非常关键。
- 自定义扩展能力:支持注入自定义瓦片渲染器、自定义点位图标、自定义聚合算法。
这套能力看起来不复杂的,但如果架构上没有一个清晰的设计,每加一个功能都要跟在坑后面填很久。我接下来会把我踩过的主要设计坑和实现思路完整捋一遍,尽量把最有价值的细节展开讲清楚。
2. 整体架构与关键设计取舍
2.1 分层架构:渲染层、数据层、交互层
lingbot-map 内部严格划分为三层。
第一层是渲染层,负责所有图形元素的最终绘制。这一层只处理"怎么画",完全不关心业务数据的语义。底图瓦片、标注点、路径线、区域面,在渲染层里一律被抽象为可绘制的图元对象,每个图元只暴露 zIndex、opacity、visible 这些通用属性和 draw 方法。这样做的好处是,后续新增任何自定义图元,我只需要实现一个绘制协议,而不需要改渲染核心。
第二层是数据层,负责把业务数据转换成渲染层能够理解的图元描述。比如业务侧传入一条机器人 JSON 路径数据,数据层负责解析坐标、抽稀、分段、动态规划颜色,然后输出一组路径线的图元对象。数据层还维护了一个图层表,记录每个图层对应哪些图元、当前显隐状态、是否参与聚合计算等。
第三层是交互层,监听鼠标、触摸、滚轮等输入事件,通过一个统一的手势识别器把原始事件转换成地图语义操作。双击是放大而不是"分发两次点击事件",这一层的转换逻辑在项目后期非常值钱,因为所有业务交互的复杂性都可以收敛在一个地方处理。
这个分层结构看起来很简单,但我在设计中反复调整过的核心点是:数据层绝对不能直接操作渲染层的具体绘制方法。如果业务数据直接跟 Canvas 绘制搅在一起,短期内开发速度快,一旦涉及坐标系变换调整、渲染器切换、或性能优化,改动面会瞬间爆炸。分层隔离的成本在前一两周感觉是"多写了很多接口",到了项目第三周开始,每加一个功能都明显比原来更快,效果非常稳定。
2.2 坐标系与投影:最容易翻车的地方
地图开发里最绕不开、最容易出问题的是坐标系。lingbot-map 在设计时有几个场景要同时兼容:标准经纬度坐标、Web 墨卡托投影坐标、室内机器人场景偏本地化的"平面坐标"。
很多做地图绘制的新手会默认"经纬度就是可以直接用来画图的坐标",这其实是第一个坑。经纬度是球面坐标,绘制在平面屏幕上的时候必须经过投影变换。lingbot-map 默认使用 Web 墨卡托投影,核心公式并不复杂,但有几个细节非常考验工程能力:
- 投影的纬度范围有限制,超过约 85.06 度会发散,需要在数据层做预检并丢弃非法值。
- 墨卡托投影在屏幕上的平移操作需要换算为经度偏移量,不能用简单的"像素 = 比例尺倍数 × 坐标"去算。
- 前端进行不断的缩放、拖拽后,浮点累计误差会越来越大,所以需要周期性把视图状态重新对齐到基准坐标。
业务侧在调用 lingbot-map 时,默认输入输出都是经纬度坐标。地图内部会统一维护一个"世界坐标"模型,所有图元位置在世界坐标系里计算,只有在实际 draw 阶段才投影到屏幕坐标。这样一个设计切换非常重要,它让旋转、缩放等视图操作变得极为干净:我只需要调整世界坐标系到屏幕坐标系的变换矩阵,所有图层自动响应。
如果你打算自己实现类似机制,我强烈建议你把"坐标系转换的代码单独放在一个工具模块里",并且写足够的单元测试。因为转换函数是纯函数,输入输出明确,测试成本极低,但一旦出错,你会在完全无关的图层渲染上看到奇怪的偏移,排查起来非常痛苦。
2.3 地图容器生命周期管理
前端组件开发里,生命周期是一个老生常谈但总是被低估的地方。地图组件比一般组件更敏感,因为地图容器内部持有大量浏览器级资源:Canvas 上下文、定时器、事件监听器、瓦片缓存、动画帧任务池。
map-view 的容器在 lingbot-map 里被抽象成了 5 个状态:已创建、已初始化、渲染中、已暂停、已销毁。业务方在调用 destroy 接口之后,组件库内部必须依次释放事件监听器、取消所有未完成的动画帧、清空瓦片请求池、销毁 canvas 上下文,最后把容器的 DOM 内容清空。这个流程里的任何一步遗漏,都会导致内存泄漏和隐藏的僵尸事件回调。
我在实际项目里就踩过一个印象极深的坑:地图所在的路由页面切换后,老地图实例已经销毁,但瓦片请求的回调依然执行,尝试访问已经被销毁的 canvas 上下文,结果浏览器控制台刷出一大片灰色错误,用户侧还能看到地图闪烁一下才消失。后来我在瓦片请求层加了一个 generation 自增 id,每次初始化或销毁都会推进这个 id,在异步回调里统一检查请求携带的 generation 是否与当前实例一致,不一致就直接丢弃结果,问题彻底消失。
因此,如果你的项目里有多页面、可动态创建销毁地图的场景,务必从第一天起就把生命周期管理当作一等公民对待。不要等到测试报告里出现内存泄漏了才补课。
3. 核心功能拆解与实操配置
3.1 底图源配置与多源切换
底图源是 lingbot-map 对外暴露的第一个高频配置项。在设计上,我把底图源抽象成"数据源接口",要求它至少实现两个关键方法:按瓦片编号返回 URL,以及返回瓦片编号与地理坐标的正反算方法。
初始化时通过 tileSource 参数传入 JSON 配置:
const map = new LingbotMap({ container: 'map-container', zoom: 5, center: [104.06, 30.67], rotation: 0, tileSource: { type: 'xyz', urlTemplate: 'https://example-tiles.example.com/{z}/{x}/{y}.png', maxZoom: 19, minZoom: 3, attribution: 'example tiles' } })这里的核心参数我逐个说明一下。
zoom 是初始缩放级别,数字越大视野越细致,我建议在业务侧不要直接使用底层瓦片的最大缩放级别作为初始值,而是根据实际展示范围反推中心点和缩放级别,这样可以避免用户进入一个完全找不到参照物的"迷失缩放级别"。
center 是初始中心点的经纬度坐标。如果你做的是室内场景,这个坐标可以是自定义的平面坐标系的虚拟经纬度,并不一定要真实匹配地表地理。
rotation 是初始旋转角,单位是度。机器人调度场景经常会需要按业务区域的朝向旋转地图,让"上"方向对应机器人前进的参考方向。
多源切换是本项目最实用的能力之一。我从一开始就考虑到了"默认加载一张公共底图,但用户需要切换成内网高精度底图"的场景。切换底图源最关键的是保持视图中心点和缩放级别不变,否则用户会感觉地图突然跳走了。
map.switchTileSource({ urlTemplate: 'https://internal-tiles.example.com/{z}/{x}/{y}.png' })在 switchTileSource 的实现里,我会执行一个三步流程:暂停所有正在进行的瓦片请求、清空当前渲染缓存、用新底图源的 urlTemplate 重新根据当前视图状态预取瓦片。这三步顺序不能乱,特别是"先暂停再清空"的顺序。如果先清空缓存再暂停,会让一批已发起但未返回的旧瓦片在加载完成后重新填充到新底图图层里,出现新旧瓦片交错闪现的诡异画面。
3.2 标注点图层与聚合功能
标注点是地图业务里最基础也最复杂的图层类型,因为业务侧对"点"的表现形态要求极多:静态点位、闪烁点位、按状态改变颜色、按属性切换图标、拖拽点位、新增点位。
lingbot-map 的标注点图层设计上采用了一个"图元生成器"模式:
map.createLayer({ id: 'robot-markers', type: 'markers', data: robotPositionData, renderer: (feature) => ({ // 返回具体绘制描述 shape: 'circle', radius: feature.status === 'online' ? 8 : 4, color: feature.status === 'busy' ? '#f43f5e' : '#22c55e', label: feature.name }) })renderer 是一个纯函数,输入是一条业务 feature 数据,输出是绘制描述对象。这样的设计有几个好处:业务方不直接触达 Canvas 绘制细节,纯函数方便复用与测试,而且数据更新时组件库内部可以精准对比绘制描述对象,只重绘真正变化过的点位而不是全部重绘。
在聚合功能方面,我最初的实现是一个蠢笨的"按网格聚合",即把地图划分为固定像素大小的网格,把落入同一网格的点合并为一个聚合点。这个方案实现快,但有一个致命问题:当两个网格边界附近出现两个聚合点时,用户缩放一级之后会看到聚合点位置跳变明显,视觉体验比较割裂。
后来调整成"距离阈值动态聚合":以每个点为圆心,根据当前缩放级别和屏幕密度计算一个聚合半径,点与点之间距离小于这个半径就合并。计算时用了一个简单的空间索引加速邻域搜索,实测一万个点的聚合计算在普通笔记本上控制在了 20 毫秒以内,用户体验提升非常大。如果你要处理几千到几万个动态点,不要直接用双重循环做聚合,否则性能上的卡顿感会很直接。
3.3 路径可视化与实时轨迹
路径和轨迹是 lingbot-map 在机器人业务里最核心的展示能力。路径是一条预定义的路线,有起点、终点、途经点;轨迹是机器人实际走出来的历史数据,可能几千个点,还带时间戳。
路径可视化的实现要点在于"抽稀与平滑"。一个几百个途经点的复杂路径如果全部绘制出来,不仅视觉杂乱,而且每帧绘制成本都在上升。我在数据层用了一个非常经典的 Douglas-Peucker 抽稀算法,将路径点压缩到视觉差异可接受的范围内。这个算法本身不复杂,但有一个调试时需要特别注意的地方:抽稀的阈值是相对"屏幕像素"而不是相对"地理坐标"定义的,因为在地图上一段真实世界距离很短的弯曲,在屏幕放大很多倍之后看起来可能就是一段弧度非常明显的路线。如果阈值用地理坐标定义,缩放级别变化后抽稀结果会失真。
实时轨迹的实现稍微不同。机器人上报的位置信息通常由后端定时推送,前端每收到一个新点就追加到轨迹末尾。这套逻辑看起来没什么,状态一多就会开始漏水。轨迹线是否需要闪动、是否需要显示箭头方向、是否需要显示当前点与路径的偏差、是否需要在特定位置打一个"事件标记",这些状态叠加起来会让数据更新逻辑变得臃肿。
我的做法是让轨迹数据进入数据层时被拆分成三个独立的子请求:几何更新、样式更新、属性更新。几何更新的优先级最高,任何位置数据变化都立刻触发;样式更新会做一次数据抽帧,把不需要变化的样式重算丢弃;属性更新则只在时间窗口位移超过阈值时才触发图元重新生成。这样处理后,连续上报高频点位时渲染压力也维持在一个可接受的水平。
3.4 交互手势与事件绑定
地图交互体验好不好,很大程度上不取决于底层渲染快不快,而是取决于手势识别的细节处理。lingbot-map 封装了一套轻量手势识别器,统一处理鼠标、单指、双指事件。
手势识别中最值得展开的一个是旋转。很多地图库在双指旋转和单指平移之间切换时,会因为手指移动产生的微小抖动而出现"旋转回跳",也就是用户明明只想平移,视觉上却发现地图轻微旋转了一下。解决这个问题的方法是引入"手势锁定状态":在双指识别的初始阶段,记录两根手指初始坐标连线的角度,之后在整个双指手势过程中,如果角度变化小于一个阈值(我使用的是 3 度),就判定为平移而不是旋转,不做任何旋转处理。一旦超过阈值则切换为旋转手势并锁定后续所有位移都参与旋转计算。
const initialAngle = Math.atan2( touch2.y - touch1.y, touch2.x - touch1.x ) // 每次 touchmove 时计算当前角度差 deltaAngle // 当 |deltaAngle| > 3° 时才触发 map.rotateBy(deltaAngle)另外,滚动缩放与瓦片加载的配合也值得注意。当用户快速滚动滚轮时,会连续触发多级缩放请求,如果不加控制,瓦片请求会在短时间内爆发几十个。处理方案是引入一个缩放消抖机制:在一段时间窗口内(我使用 150ms),只保留最后一次缩放操作的实际渲染,中间的中间值可以丢弃。这种做法会让缩放过程有一些"跟手"的粘滞感,但瓦片加载稳定之后总的用户体验明显更舒适。
事件绑定层面,我统一向外抛出的事件包括:map:click、map:dblclick、map:move、map:zoomchange、map:rotate、map:layer:click。全部事件都携带地理坐标视图坐标数组,方便业务侧直接使用。特别提醒一点:不要把 layer 的点击事件直接绑定到 canvas 元素上,因为点位是否被命中需要经过坐标转换和命中判定,直接在 canvas 上监听事件只能拿到像素坐标,不满足业务诉求。
4. 渲染性能与内存优化实录
4.1 首屏加载优化:瓦片预取与并行数控制
地图首屏加载速度是用户对项目质量的第一印象。lingbot-map 在这块做了两层优化。
第一层是瓦片预取。在视图初始化时,组件库除了请求当前可视区域内的瓦片,还会往所有方向多取一层瓦片,也就是当前视野外扩一个瓦片距离的范围。这个预取的量不大,但用户在轻微拖拽时不需要等待新瓦片加载,体验流畅度明显上升。
第二层是请求并发数控制。浏览器对同域名的并发连接数是有限制的,而瓦片请求又是高频率、小体量、并行性强的请求。如果不做并发限制,几十个瓦片请求同时涌出会占用大量连接槽位,导致首页其他静态资源加载被阻塞。
我引入了一个简单的并发池,默认并发上限设为 6,同时为瓦片请求设置 8 秒超时。瓦片加载失败或超时后,会重试一次;再次失败则直接渲染一个占位底块,不再影响后续瓦片加载。这个超时时间设置是关键,设短了会频繁重试浪费流量,设长了用户在弱网下会看到大块空白区域迟迟不消失。
4.2 交互卡顿定位与帧率优化
地图卡顿问题的定位,在 lingbot-map 开发过程中占了我相当多的时间。交互卡顿的核心原因是"每帧渲染耗时过高",但这个耗时分布在多个环节,没有性能分析工具的情况下很容易盲猜。
我采用的排查方式是分层打点。在交互层、数据层、渲染层各插入一个耗时统计点,用一维数组记录最近 100 帧的耗时分布。这样当用户反馈"旋转地图特别卡"时,我可以快速从统计数据里判断卡顿是否来自渲染层的某个特殊图元,而不是盲目地怀疑整个地图核心。
最终定位到的主要优化点有三个:
第一个是 Canvas 绘制中 shadowBlur 的使用。标注点文字加阴影效果看起来确实是漂亮的,但在低端移动设备上,阴影是导致重绘变慢的头号凶手。优化方案是尽量用"图片预模糊"或者干脆去掉阴影,代之以对比更明显的颜色边框。
第二个是重复图元对象造成的 GC 压力。在轨迹高频更新时,每条新轨迹数据都会创建一批新的几何对象,而旧对象需要被 GC 回收。如果 GC 频率过高,会看到明显的掉帧。处理方式引入了对象池:统一维护一个几何对象池,数据变化时把不用的对象回收到池子里,新数据优先复用池中对象,避免每次都触发浏览器内存分配和垃圾回收。
第三个是离屏 Canvas 的使用。对于静态底图、长时间不变的标注点图层,我可以把它们单独渲染到一个离屏 canvas 上,每次交互时只绘制变化的图层和离屏 canvas 的合成结果。实测下来,复杂点位的场景下帧率能提升约 30% 到 40%。
4.3 大规模点位渲染:Canvas 还是 DOM
做地图可视化的人一定会面对一个经典问题:点位量大之后,用 DOM 还是 Canvas?lingbot-map 的方向从设计之初就是 Canvas 2D 为主,但并不是说每个项目都应该无脑用 Canvas。
DOM 方案在大规模点位下的最大瓶颈有两个:一是每个 DOM 节点的创建和销毁成本高,几千个节点同时存在时对内存压力非常大;二是在地图平移缩放过程中,每个节点的 transform 都需要被重新计算和触发重排。点位数量低于 500 时,DOM 的布点方案确实更灵活,因为 CSS 的 hover、动画、事件绑定都很方便。但一旦超过 2000,Canvas 的优势就会变得非常必要。
在实际开发中,我还遇到了一个低于 2000 却不得不切 Canvas 的场景:点位图标是动态生成的,每个图标都不一样。用 DOM 生成 2000 个不同样式的图标然后给每个节点赋背景图,浏览器的样式计算开销就已经大到不可忽略了。后来统一改成在离屏 canvas 上批量绘制图标,再作为 spritesheet 贴到主 canvas 上,性能完全改观。
如果你在做一个点位数量超过 5000 的场景,我的建议是直接考虑 WebGL 渲染方案。Canvas 2D 在五千到一万个点位之间还是可以优化的,但再往上画圆形、多边形这类基础图元,CPU 渲染的上限就逐渐逼近了。lingbot-map 目前把渲染接口抽象成了统一的 draw 协议,未来接一个 WebGL 渲染引擎不需要改动数据层代码,这也是前面分层架构带来的红利之一。
5. 常见问题与排查技巧实录
5.1 高频问题速查表
| 问题现象 | 可能原因 | 解决方式 |
|---|---|---|
| 地图拖拽后出现"白格子" | 瓦片请求失败或未命中缓存 | 检查瓦片 URL 拼接、瓦片源是否支持当前缩放级别,必要时提高预取层级 |
| 地图显示位置与实际上报位置偏差大 | 坐标系不统一,某处直接使用了经纬度而未经过投影转换 | 确认全部坐标统一走 map 的 fromLngLat / toLngLat 接口 |
| 缩放级别超过瓦片源最大级别后地图模糊 | 未配置 maxZoom 限制 | 在 tileSource 配置中设置 maxZoom,并在加载瓦片时做级别截断 |
| 图层数据更新后地图不刷新 | 图层显隐或图元描述未触发重绘 | 检查数据更新后是否调用了 layer.updateData 接口 |
| 双指缩放时地图上下抖动 | 手势识别相位没处理好,缩放和平移互相干扰 | 将双指手势的缩放中心设置为两指中点,并在初始阶段锁定中心点 |
| 旋转后标注文字方向不对 | 标注图元未随视图旋转角逆旋 | 在渲染器里对文字方向做逆旋转处理,保持文字正立显示 |
| 页面切换后地图事件仍在触发 | 实例销毁时未解除事件监听 | 严格按照生命周期顺序销毁实例,并加入 generation 校验机制 |
| 相同数据下聚合结果不稳定 | 聚合算法未考虑视觉层级变化 | 切换到动态距离阈值聚合,并定期清理缓存的分组结果 |
5.2 几个值得特别警惕的隐性坑
第一个隐性坑是瓦片缓存策略。如果没有显式控制缓存上限,瓦片缓存会在长时间浏览大地图后越积越多,最终吞掉大量内存。我在 lingbot-map 里设置了缓存的上限为 600 个瓦片对象,超过这个数字就按最近最少使用策略淘汰。淘汰时要注意不能移除当前可视区域内的瓦片,否则会导致画面闪白。这个策略的实现在数据量较大的业务里能明显降低长时间使用后的卡顿风险。
第二个隐性坑是 Canvas DPR 设置。低分辨率 canvas 在高分屏上会显得很模糊,但盲目把 canvas 宽高设成设备物理像素,又会在低端设备上造成过重的绘制压力。折中方案是设置一个 DPR 上限,比如最多按 2 倍处理,超过这个倍数的设备也保持 2 倍渲染。这个细节不会在功能测试阶段暴露,但到用户实际使用阶段,地图文字的清晰度感受差异会很明显。
第三个隐性坑与定时器有关。地图动画、聚合计算、轨迹平滑这类任务如果直接用 setInterval 实现,在页面切后台之后依然会被浏览器节流唤醒,造成无谓的资源占用。lingbot-map 里所有周期性任务都优先使用 requestAnimationFrame,并监听页面可见性变化,页面不可见时自动暂停所有非必要的动画任务,恢复到可见状态后再重新同步地图状态。
第四个坑是我踩过最痛的:不要直接对用户 gesture 的原始坐标做图层命中测试。因为 canvas 在 devicePixelRatio 不为 1 时存在坐标缩放,直接使用 event.clientX 去对比图层点位会偏移得非常离谱。统一做法是通过 canvas 的 boundingClientRect 和 DPR 换算回 canvas 内部坐标系,再执行命中计算。这个转换逻辑应该放在交互层里,而不是由每个业务方法各自实现。
6. lingbot-map 的扩展价值与后续思路
开发到这里,lingbot-map 已经能稳定支撑上万点位地图的渲染、实时轨迹追踪、多点交互配置以及底图切换这类核心场景。作为一个技术项目,它的直接价值是解决了我所在业务系统里的地图可视化需求;作为一套可复用的设计思路,它还埋了一些值得继续挖掘的扩展点。
比如离屏 Canvas 不仅可以用在点位图层的缓存渲染上,还可以在"区域路径预计算"的场景里做热力预渲染,让动态热力图和静态热力底的切换变得极为顺畅。又比如我现在用的动态聚合算法,后续可以把它抽成一个独立的聚合模块,不仅可以聚合点位,还可以聚合路径片段、区域边界,这样在业务需要"聚类路线走向"的报表视图时,底层能力不需要重复搭建。
另外一个我想着手优化的方向是把当前的事件机制迁移到基于消息总线的模式。现在地图内部各个模块之间是直接调用,从短期看是可维护的,但进一步扩展自定义插件体系时,插件之间如果要共享数据或响应同一事件,消息总线会比直接调用更灵活。这个重构不着急,但架构上我会预留对应的扩展点。
如果你也想在类似的地图可视化项目里尝试这套思路,我有一个很务实的建议:不要追求一步到位的大架构,先把坐标系转换、图层模型、渲染协议这三层地基打好,再逐步往上加交互和业务图层。地基好了,后面的功能都是搭积木;地基不稳,每加一个功能都要来回改。
我实际维护下来,最舒服的一点是,当底层的分层设计足够稳定时,新业务的接入成本会低到让我有更多时间去打磨地图交互的细节,而不是反复在相同的基础问题上耗时间。这种正向循环,是一个地图组件库开发过程中最值得追求的体验。