
1. 从零搭建一个轻量级地图可视化组件lingbot-map 项目拆解地图可视化这件事做过前端或者数据展示的人应该都不陌生。不管是物流轨迹、门店分布、设备点位还是区域热力只要跟“位置”沾边最后大概率都要落到一张地图上。但真到动手的时候问题就来了大厂的地图 SDK 功能全但体积大、接入流程繁琐开源方案灵活但文档稀碎、坑多到让人怀疑人生。我前阵子接手一个内部数据看板的需求核心诉求就三条——能画点、能连线、能交互最好还能离线跑。翻了一圈现成的轮子要么太重要么太丑要么定制起来比从头写还费劲。于是干脆自己撸了一个轻量级的地图可视化组件项目代号就叫lingbot-map。这个项目本质上是一个面向数据展示场景的轻量级地图渲染与交互组件它不追求做成一个完整的地图引擎而是聚焦在“把业务数据准确地落到地图上并且让用户能顺畅地看和点”这一件事上。它解决的核心问题是在不需要复杂地理信息分析、不需要三维渲染、不需要实时导航的前提下如何用最小的接入成本把点位、轨迹、区域这些常见数据形态清晰地呈现出来。适合谁参考呢我觉得有三类人一是前端开发者手头有地图展示需求但不想被重型 SDK 绑架二是数据分析或运营同学想快速把表格里的经纬度数据变成一张能看的图三是对地图可视化感兴趣、想了解底层实现逻辑的初学者拿这个项目当练手素材刚刚好。我写这个项目的出发点很简单够用就好别整那些花里胡哨的。所以整个组件的设计原则就是三个词——轻量、可控、易扩展。下面我会从整体设计思路、核心细节、实操过程到踩坑记录完整地拆一遍尽量把每个决策背后的“为什么”讲清楚让你看完能直接抄作业也能根据自己的场景做调整。2. 整体设计与思路拆解2.1 为什么不用现成的地图 SDK这个问题我被问过很多次。每次我说“自己写了一个地图组件”对方第一反应都是“为啥不用某某地图的 JS API”答案其实不复杂主要是三个维度的权衡。第一是体积和依赖。主流地图 SDK 动辄几百 KB 起步加载之后还会异步拉取瓦片、字体、图标等一堆资源。对于一个只是想在管理后台里画几个点的页面来说这个成本太高了。lingbot-map 的核心目标是把渲染层做到极致轻量最终打包出来的核心代码控制在 30KB 以内gzip 后更小不依赖任何外部地图服务瓦片可以走本地缓存或者简单的静态服务。第二是定制自由度。现成 SDK 的样式和交互逻辑是封装好的你想改一个点位的 hover 效果可能得翻半天文档最后发现只能用它提供的有限配置项。而 lingbot-map 把渲染层和交互层完全暴露出来点位长什么样、点击之后干什么、连线用什么颜色全部由使用方决定。这种“白盒”设计在业务变化快的场景下优势非常明显。第三是离线可用性。有些内部系统部署在隔离环境里根本连不上外部地图服务。lingbot-map 的设计从一开始就考虑了离线场景瓦片源可以是本地文件系统、内网静态服务甚至可以用 Canvas 直接绘制简化的底图。这一点是很多重型 SDK 做不到或者做起来很别扭的。当然自己写也有代价。比如地理投影的精度、瓦片加载的调度、大规模点位的性能优化这些都需要自己处理。所以 lingbot-map 的定位很明确不做通用地图引擎只做业务数据的可视化载体。如果你的需求是导航、路径规划、三维地形那还是老老实实用专业 SDK但如果只是“把数据画到地图上”那这个项目就是为你准备的。2.2 核心架构三层分离lingbot-map 的架构我改过好几版最终定型为三层分离的结构数据层、渲染层、交互层。这么分的好处是每一层可以独立替换和测试不会牵一发而动全身。数据层负责接收原始数据通常是经纬度数组或者 GeoJSON做坐标转换、数据过滤、聚合计算。这里的关键是坐标系的统一。业务数据给过来的经纬度可能是 WGS84、GCJ02 或者 BD09如果不做统一转换点位就会偏移几百米甚至几公里。lingbot-map 内置了一个轻量的坐标转换模块支持常见坐标系之间的互转转换算法用的是公开的偏移量计算方式精度足够业务展示使用。渲染层负责把数据画到屏幕上。我选择了 Canvas 2D 作为主要渲染方式而不是 SVG 或者 WebGL。原因很简单Canvas 在绘制大量点位时性能更好而且控制粒度更细。SVG 虽然 DOM 操作方便但点位一多就会卡WebGL 性能最强但学习曲线陡峭对于“轻量”这个定位来说有点杀鸡用牛刀。Canvas 2D 在几千个点位的场景下完全够用配合离屏渲染和分层绘制性能可以进一步优化。交互层负责处理用户的鼠标、触摸事件包括点击、悬停、拖拽、缩放等。这一层的关键是事件命中检测。Canvas 不像 DOM 那样自带事件系统需要自己计算鼠标位置和图形的位置关系。lingbot-map 用了空间索引简单的网格划分来加速命中检测避免每次事件都遍历所有图形。三层之间通过一个轻量的事件总线通信数据层更新后通知渲染层重绘交互层捕获事件后通知数据层做过滤或高亮。这种解耦设计让每一层的逻辑都很清晰也方便后续扩展。比如你想换一种渲染方式只需要替换渲染层数据层和交互层不用动。2.3 技术选型为什么是 Canvas TypeScript技术选型这块我纠结过一阵。候选方案有三个纯 Canvas、SVG D3、WebGL Three.js。最后选了 Canvas TypeScript理由如下。Canvas 的优势在于绘制性能可控。地图上的元素通常包括底图瓦片、点位、连线、区域填充、文字标注等这些用 Canvas 的绘制指令可以精确控制。而且 Canvas 支持离屏渲染可以把不常变化的底图缓存起来只重绘变化的部分。相比之下SVG 每个元素都是一个 DOM 节点点位多了之后浏览器的布局和重绘开销会明显上升。我实测过在 2000 个点位的场景下SVG 方案的帧率会掉到 20fps 以下而 Canvas 方案还能保持在 50fps 以上。TypeScript 的选择更多是出于工程维护的考虑。地图组件的 API 比较多数据结构也比较复杂用 TypeScript 可以把类型定义清楚使用方接入时能有更好的提示和校验。而且 TypeScript 的泛型能力在处理不同数据格式时非常有用比如点位数据可能是{lng, lat}也可能是{longitude, latitude}用泛型约束可以做到既灵活又安全。至于为什么不用 WebGL主要是考虑到目标场景的数据量级。lingbot-map 定位是“轻量级”服务的业务场景通常是几百到几千个点位Canvas 2D 完全能扛住。WebGL 虽然能支持十万级点位但带来的复杂度着色器编写、矩阵运算、上下文管理对于这个项目的目标用户来说是不必要的负担。当然如果后续有超大规模数据的需求渲染层是可以替换成 WebGL 的这也是三层分离架构的好处。3. 核心细节解析与实操要点3.1 坐标转换别让点位偏移毁了一切坐标转换是地图可视化里最容易被忽视、但出问题最致命的一环。我见过太多案例开发阶段用测试数据看着好好的一上真实数据就发现点位整体偏移了几百米。原因就是数据源用的坐标系和地图底图用的坐标系不一致。国内常见的坐标系有三种WGS84 是 GPS 原始坐标GCJ02 是经过偏移加密的坐标国内地图服务普遍使用BD09 是在 GCJ02 基础上又做了一次偏移某搜索引擎地图使用。这三种坐标系之间的转换不是简单的线性变换而是有一套公开的偏移算法。lingbot-map 内置了这些转换函数使用的时候只需要在初始化时指定数据源的坐标系和底图的坐标系组件会自动完成转换。具体实现上WGS84 到 GCJ02 的转换用的是一个基于椭球参数的近似算法核心逻辑是判断坐标是否在国内范围内如果是则应用偏移量。这个算法的精度在米级对于业务展示完全够用。GCJ02 到 BD09 的转换则是在 GCJ02 的基础上再做一次极坐标偏移。反向转换GCJ02 到 WGS84用的是迭代逼近法一般迭代两三次就能收敛到足够精度。注意坐标转换只在数据进入渲染层之前做一次不要每次重绘都转换否则会有性能浪费。转换后的坐标可以缓存在数据层后续直接使用。实操中还有一个坑边界判断。有些偏移算法需要判断坐标是否在特定范围内才应用偏移如果判断逻辑写错了边界附近的点位会出现“跳变”。我的做法是把边界判断做成可配置的默认使用一个稍大的范围避免边缘数据被漏掉。另外如果数据源本身就是 GCJ02而底图也是 GCJ02那就不需要任何转换直接使用即可这时候一定要把转换开关关掉否则会“负负得正”导致偏移。3.2 瓦片加载与缓存策略地图底图通常是由一张张瓦片拼接而成的。lingbot-map 的瓦片加载模块做了几件事按需加载、多级缓存、失败重试。按需加载的意思是只加载当前视口内可见的瓦片视口外的瓦片不加载或者延迟加载。实现上组件会根据当前的中心点坐标和缩放级别计算出需要哪些瓦片每个瓦片有唯一的 z/x/y 编号然后只请求这些瓦片。当用户拖拽或缩放时重新计算可见瓦片列表新增的加载移出的可以保留在缓存里以备快速回退。多级缓存分为内存缓存和持久化缓存。内存缓存用 Map 存储已经加载过的瓦片图片设置一个上限比如 200 张超过之后按 LRU 策略淘汰。持久化缓存可以用 IndexedDB 或者 localStorage 存储瓦片的 base64 数据这样用户刷新页面后不需要重新下载。不过持久化缓存要注意容量控制瓦片图片虽然不大但积累多了也会占用不少空间建议设置一个过期时间或者容量上限。失败重试机制是很多轻量方案容易忽略的。网络请求总会有失败的时候如果不做重试用户就会看到一块块空白。lingbot-map 的做法是每个瓦片请求失败后延迟一段时间自动重试最多重试三次三次都失败则用一个占位图代替并在控制台输出警告。重试的延迟时间采用指数退避策略避免同时大量重试压垮服务。提示如果部署在离线环境可以把瓦片预先下载好放在静态目录里然后把瓦片请求的 URL 模板指向本地路径。lingbot-map 支持自定义瓦片 URL 模板格式是{z}/{x}/{y}的占位符替换。还有一个细节是瓦片的淡入效果。瓦片加载是异步的如果直接显示会有闪烁感。lingbot-map 在瓦片加载完成后做了一个简单的透明度动画从 0 渐变到 1视觉上会平滑很多。这个动画用 requestAnimationFrame 驱动持续时间 200ms 左右不会影响性能。3.3 点位渲染与性能优化点位渲染看起来简单就是画个圆或者图标但数据量上去之后问题就来了。lingbot-map 在点位渲染上做了几层优化。第一层是视口裁剪。只渲染当前视口内的点位视口外的直接跳过。这个判断很快就是比较一下坐标是否在视口范围内。对于大规模数据这一层能过滤掉大部分不可见的点位。第二层是聚合显示。当缩放级别较小、点位密集时把相邻的点位合并成一个聚合点显示数量。聚合的算法用的是网格划分把视口划分成若干网格每个网格内的点位合并成一个。网格大小根据缩放级别动态调整缩放级别越小网格越大聚合程度越高。这样既保证了视觉清晰度又避免了大量重叠点位的绘制开销。第三层是分层绘制。把点位按照重要性或者类型分成不同的层重要的层后绘制保证显示在最上面。同时对于静态的点位不经常变化的可以绘制到一个离屏 Canvas 上缓存起来每次重绘只需要把离屏 Canvas 贴过来不需要重新绘制每个点。这个优化在点位数量多、但变化不频繁的场景下效果非常明显。第四层是图标复用。如果点位用的是图片图标不要每个点位都去加载和绘制图片而是把图标预先绘制到一个小 Canvas 上然后通过 drawImage 复用。对于同一种类型的点位图标只需要加载一次。实测下来在 5000 个点位的场景下经过这几层优化lingbot-map 的渲染帧率可以稳定在 40fps 以上拖拽和缩放基本感觉不到卡顿。当然如果数据量到了几万甚至几十万那就需要考虑 WebGL 方案了这是 Canvas 2D 的物理上限。3.4 交互事件处理命中检测怎么做Canvas 没有 DOM 的事件系统所有的点击、悬停都需要自己计算。lingbot-map 的命中检测方案是空间索引 精确判断。空间索引用的是简单的网格法把视口划分成固定大小的网格比如 50x50 像素每个网格记录落在其中的图形索引。当鼠标事件发生时先计算出鼠标所在的网格然后只检查这个网格内的图形大大减少了需要判断的图形数量。对于点位这种小图形网格法已经足够高效对于区域这种大图形可能需要更复杂的索引结构但业务场景中区域数量通常不多直接遍历也可以接受。精确判断根据图形类型不同而不同。点位通常是圆形或者矩形判断鼠标位置到圆心的距离是否小于半径即可。连线需要判断点到线段的距离用的是向量投影的方法。区域多边形用的是射线法判断点是否在多边形内部。悬停效果的处理有一个小技巧不要每次鼠标移动都重绘整个画布而是用一个单独的“高亮层”来绘制悬停效果。主画布保持不动高亮层只绘制当前悬停的图形这样性能会好很多。鼠标移出时清空高亮层即可。注意触摸设备上的事件处理和鼠标不同需要处理 touchstart、touchmove、touchend 等事件并且要注意多点触控的情况。lingbot-map 对触摸事件做了兼容处理单指拖拽移动地图双指捏合缩放点击用 tap 事件模拟。还有一个容易被忽略的点是事件节流。鼠标移动事件触发频率很高如果每次触发都做完整的命中检测和重绘性能压力会很大。lingbot-map 对 mousemove 事件做了节流处理每 16ms约一帧最多处理一次既保证了流畅度又避免了不必要的计算。4. 实操过程与核心环节实现4.1 环境准备与项目初始化动手之前先把环境搭好。lingbot-map 是一个纯前端项目不依赖任何后端服务所以环境准备很简单。首先确保本地有 Node.js 环境版本建议 16 以上。然后创建一个新的项目目录初始化 npmmkdir lingbot-map cd lingbot-map npm init -y接着安装开发依赖。lingbot-map 用 TypeScript 编写用 Vite 作为构建工具测试用 Vitestnpm install -D typescript vite vitest types/node然后创建tsconfig.json配置编译选项。关键配置包括target设为 ES2018module设为 ESNextstrict设为 true 开启严格模式declaration设为 true 生成类型声明文件。这些配置的目的是保证代码质量同时让使用方能有完整的类型提示。项目目录结构我习惯这样组织src/ core/ # 核心逻辑 coordinate.ts # 坐标转换 tile.ts # 瓦片加载 renderer.ts # 渲染器 interaction.ts # 交互处理 layers/ # 图层实现 pointLayer.ts lineLayer.ts polygonLayer.ts utils/ # 工具函数 eventBus.ts spatialIndex.ts types/ # 类型定义 index.ts index.ts # 入口文件这种按功能划分的方式让代码职责清晰后续维护和扩展都方便。入口文件只做导出不写具体逻辑。4.2 核心渲染循环的实现渲染循环是地图组件的“心脏”。lingbot-map 的渲染循环不是一直运行的而是按需触发只有当数据变化、视口变化或者交互状态变化时才触发一次重绘。这样避免了不必要的性能消耗。渲染循环的核心逻辑是这样的class Renderer { private needsRender false; private rafId: number | null null; requestRender() { if (this.needsRender) return; this.needsRender true; this.rafId requestAnimationFrame(() this.render()); } private render() { this.needsRender false; this.clear(); this.drawTiles(); this.drawLayers(); this.drawInteractionLayer(); } }requestRender方法做了防抖处理如果已经有一帧在等待渲染就不重复请求。这样即使短时间内多次调用requestRender也只会触发一次实际渲染。绘制顺序很重要先画底图瓦片再画数据图层最后画交互高亮层。底图瓦片可以缓存到离屏 Canvas数据图层根据变化频率决定是否缓存交互高亮层每次都需要重绘因为它是动态的。视口变换是渲染循环里的关键计算。地图的拖拽和缩放本质上是对视口变换矩阵的修改。lingbot-map 用一个简单的{center, zoom}结构来表示视口状态渲染时根据这个状态计算出每个地理坐标对应的屏幕坐标。缩放级别和瓦片层级是对应的缩放级别变化时可能需要加载新的瓦片。4.3 点位图层的完整实现点位图层是最常用的图层类型我以它为例把完整实现过程走一遍。首先定义点位的数据结构interface PointData { id: string; lng: number; lat: number; type?: string; [key: string]: any; }id是唯一标识用于交互时定位数据lng和lat是经纬度type用于区分不同类型的点位渲染时可以给不同的样式其余字段是业务自定义数据组件不关心但会保留。点位图层的初始化需要传入配置项interface PointLayerOptions { data: PointData[]; style: PointStyle | ((point: PointData) PointStyle); onClick?: (point: PointData) void; onHover?: (point: PointData) void; }style可以是一个固定样式对象也可以是一个函数根据点位数据动态返回样式。这种设计让使用方可以灵活控制每个点位的外观比如根据业务状态显示不同颜色。渲染点位时先做视口裁剪和聚合计算然后遍历需要绘制的点位逐个绘制。绘制圆形的代码很简单ctx.beginPath(); ctx.arc(screenX, screenY, radius, 0, Math.PI * 2); ctx.fillStyle style.color; ctx.fill(); ctx.strokeStyle style.borderColor; ctx.lineWidth style.borderWidth; ctx.stroke();如果点位有图标就用drawImage绘制图标。图标需要预先加载加载完成后触发一次重绘。lingbot-map 维护了一个图标缓存相同 URL 的图标只加载一次。聚合点位的绘制稍微复杂一些。聚合点通常显示为一个较大的圆中间显示聚合的数量。聚合的半径根据数量动态调整数量越多半径越大。聚合点的点击事件会触发地图缩放放大到可以看清具体点位的级别。实操心得点位图层的性能瓶颈通常在绘制阶段而不是计算阶段。如果发现卡顿优先检查是不是每次重绘都重新创建了 Canvas 上下文或者重新加载了图标。把不变的部分缓存起来性能会有立竿见影的提升。4.4 交互功能的接入与调试交互功能是地图组件的“灵魂”没有交互的地图就是一张图片。lingbot-map 的交互功能包括拖拽平移、滚轮缩放、点击选中、悬停高亮。拖拽平移的实现逻辑是记录鼠标按下时的位置和视口中心点鼠标移动时计算偏移量更新视口中心点触发重绘。这里要注意的是拖拽过程中不要做瓦片加载的完整计算只做视口变换和重绘等拖拽结束后再加载新瓦片。这样可以避免拖拽过程中的卡顿。滚轮缩放需要处理缩放的中心点。默认以鼠标位置为中心缩放这样用户体验最好。缩放的级别是离散的通常每次滚动缩放一级。lingbot-map 支持连续缩放但瓦片层级是离散的所以缩放过程中会有一个“过渡态”瓦片可能会暂时模糊等缩放结束后加载新层级的瓦片。点击选中的实现依赖命中检测。点击事件触发后先做命中检测找到被点击的图形然后触发对应的回调。如果没有命中任何图形则触发地图的空白点击回调。选中的图形可以有一个高亮样式这个样式绘制在交互层上。悬停高亮和点击选中类似但触发更频繁。前面提到的节流处理在这里很重要。另外悬停高亮的状态需要维护鼠标移出图形时要清除高亮。调试交互功能时我习惯在开发模式下把命中检测的结果可视化出来在鼠标位置画一个小十字在命中的图形上画一个外框。这样能直观地看到命中检测是否准确。这个调试功能在生产环境会自动关闭。5. 常见问题与排查技巧实录5.1 点位偏移问题排查点位偏移是最高频的问题没有之一。排查思路可以按照以下顺序进行。首先确认数据源的坐标系。如果数据是从 GPS 设备直接导出的通常是 WGS84如果是从国内地图服务获取的通常是 GCJ02如果是从某搜索引擎地图获取的通常是 BD09。不确定的话可以拿几个已知地点的坐标做验证比如取一个地标建筑的坐标看它落在哪里。然后确认底图的坐标系。如果底图瓦片来自国内地图服务那底图就是 GCJ02如果来自 OpenStreetMap 等国际服务底图是 WGS84。数据坐标系和底图坐标系不一致时必须做转换。最后检查转换开关是否正确设置。lingbot-map 的坐标转换配置有两个参数sourceCRS和targetCRS。如果两者相同转换会被跳过。有时候使用方设置了转换但数据本身已经是目标坐标系了结果转换之后反而偏移了。这种情况的典型表现是点位整体偏移了一个固定距离而且偏移方向一致。排查技巧在地图上找一个已知位置比如公司大楼把它的坐标用不同坐标系分别标注看哪个和底图吻合。吻合的那个就是底图的坐标系。5.2 瓦片加载失败与白屏瓦片加载失败的表现是地图上出现空白区域或者整个地图白屏。排查步骤如下。先看控制台有没有网络请求错误。如果有 404说明瓦片 URL 模板配置错了检查{z}/{x}/{y}的替换逻辑。如果有跨域错误需要在瓦片服务端配置 CORS 头或者把瓦片放到同域下。如果网络请求正常但瓦片不显示检查瓦片的坐标计算是否正确。瓦片的 z/x/y 编号是根据视口范围和缩放级别计算出来的如果计算逻辑有误请求的瓦片可能根本不存在。可以在控制台打印出请求的瓦片编号和实际可用的瓦片编号对比。还有一种情况是瓦片加载了但绘制顺序不对被其他图层盖住了。检查绘制顺序确保瓦片层在最下面。如果是在离线环境检查瓦片文件是否真的存在路径是否正确。离线瓦片通常放在静态目录下URL 模板要指向正确的相对路径。5.3 性能卡顿的定位与优化性能卡顿的排查可以用 Chrome DevTools 的 Performance 面板录制一段操作然后看火焰图。常见的卡顿原因和优化方法如下表卡顿表现可能原因优化方法拖拽时掉帧每次移动都重绘所有图层使用离屏 Canvas 缓存静态图层缩放时卡顿瓦片加载和重绘同时进行缩放过程中只做变换结束后再加载瓦片点位多时卡顿逐个绘制点位开销大视口裁剪 聚合 图标复用悬停时卡顿每次移动都做完整命中检测空间索引 事件节流内存持续增长缓存没有上限设置 LRU 缓存上限实测下来大部分卡顿问题都能通过“缓存不变的部分只重绘变化的部分”这个原则解决。地图上的底图瓦片、静态点位、区域填充这些不常变化的内容都应该缓存到离屏 Canvas 上。5.4 移动端适配的坑移动端和桌面端的差异比想象中大。首先是事件模型不同移动端用 touch 事件而且支持多点触控。lingbot-map 对触摸事件做了封装单指拖拽对应鼠标拖拽双指捏合对应滚轮缩放tap 对应 click。其次是性能差异。移动设备的 GPU 和 CPU 性能都比桌面弱同样的数据量在移动端可能就卡了。优化策略是移动端默认开启聚合降低初始缩放级别减少同时渲染的点位数量。还有一个坑是点击延迟。移动端浏览器为了判断是单击还是双击缩放会有 300ms 的点击延迟。lingbot-map 通过监听 touchstart 和 touchend 来模拟快速点击绕过了这个延迟。但要注意区分拖拽和点击如果 touchstart 和 touchend 之间移动距离超过阈值就判定为拖拽而不是点击。注意移动端的 Canvas 尺寸要乘以 devicePixelRatio否则在高清屏上会模糊。lingbot-map 在初始化时会自动处理这个缩放使用方不需要关心。6. 扩展方向与个人实践体会lingbot-map 目前实现的是最核心的点位、连线、区域三种图层以及基础的交互功能。但地图可视化的需求是多种多样的后续可以扩展的方向不少。一个方向是热力图图层。热力图适合展示密度分布比如用户分布、事件热点。实现上可以用 Canvas 的径向渐变叠加或者用简单的网格聚合后着色。热力图的性能优化关键是降低计算频率不需要每次重绘都重新计算热力值。另一个方向是轨迹动画。物流、出行场景经常需要展示移动轨迹。轨迹动画的核心是按时间插值计算当前位置然后逐帧绘制。lingbot-map 的渲染循环已经是按需触发的加一个动画驱动源就可以支持轨迹播放。还有一个方向是自定义图层。有些业务场景需要绘制特殊的图形比如风向标、雷达扫描效果。lingbot-map 可以开放一个自定义图层的接口使用方传入绘制函数组件负责调用和缓存。这样既保持了核心的轻量又提供了足够的扩展性。我个人在实际操作中的体会是地图组件的难点不在绘制而在状态管理。视口状态、图层状态、交互状态、数据状态这些状态之间的同步和更新很容易出 bug。lingbot-map 用事件总线来解耦状态更新每个模块只关心自己需要的状态状态变化时通过事件通知相关模块。这种模式在项目初期可能显得有点“重”但随着功能增多维护成本会明显降低。最后分享一个小技巧开发地图组件时准备一组“标准测试数据”非常重要。这组数据应该包含各种边界情况极值坐标、密集点位、空数据、异常数据等。每次修改代码后用这组数据跑一遍能快速发现回归问题。我用的测试数据包括一个位于坐标原点的点、一个位于极北的点、一组密集在某个区域的点、一条跨越多个区域的线、一个自相交的多边形。这些数据帮我提前发现了好几个坐标转换和命中检测的边界 bug。