H5 Canvas + geojson.io:从经纬度到像素的地图绘制与导入导出实战
发布时间:2026/9/26 12:18:22来源:尧图网络
简介针对前端开发与数据可视化学习者压缩包内含一套结合H5 Canvas、ECharts和GeoJSON的交互式地图绘制方案。开发者可通过Canvas API绘制地图基础轮廓利用ECharts的map系列加载并渲染GeoJSON数据再借助httpgeojson.io在线工具自由调整行政边界实现省级、市级等多级地图的自定义展示与缩放下钻。压缩包整体约1.1MB便于快速下载与二次开发。项目源码与数据示例重点演示地图展开与收缩、地理数据关联等交互逻辑并涵盖GeoJSON格式的坐标转换方法可帮助读者打通地理信息到可视化地图的完整流程。已有1630人学习/下载适合希望掌握自定义地图开发、数据可视化与前端绘图集成技巧的中级前端工程师无论是地理数据分析、业务监控还是大屏展示均可直接参考或复用。1. 从“看地图”到“画地图”H5 Canvas 接 geojson.io 的真实需求做地图可视化多数人第一反应是接 Leaflet 或 Mapbox 底图把瓦片、缩放、覆盖物全交给 SDK。换到 H5 Canvas 这条路线很多人觉得是在重复造轮子直到需求变成“地图上要能自由绘制地块、自定义样式、还要把数据导出给别的系统用”成熟 SDK 就开始束手束脚。Canvas 做地图的真正优势不是画得好看而是把地图当成一张画布GeoJSON 是数据geojson.io 是数据编辑器Canvas 是渲染层三端通过一份 GeoJSON 文本流转。这篇文章完整拆解坐标换算、点线面渲染、与 geojson.io 互相导入导出的链路适合做 H5 可视化编辑器、地块圈选、室内地图这类需要自绘地图的从业者。2. 坐标与数据先行从 GeoJSON 经纬度到 Canvas 像素的换算路径绘制 Canvas 地图前先把数据和坐标体系立住。很多新手第一步就翻车因为 GeoJSON 里的坐标是经纬度[lng, lat]而 Canvas 画的是像素[x, y]两个坐标系的原点、方向、量纲全不一样。这层关系不掰开后面的点线面绘制全是黑匣子。2.1 经纬度不是像素线性映射与投影的边界GeoJSON 默认使用 WGS-84 经纬度坐标取值范围 lng 在 -180 到 180lat 在 -90 到 90。Canvas 的坐标系原点在画布左上角x 轴向右增长y 轴向下增长单位是 CSS 像素。把经纬度画到画布上本质上要做一次从地理坐标到屏幕坐标的映射。最常见、也最容易跑通的做法是线性映射先算出整份数据的外包矩形minLng、minLat、maxLng、maxLat再把经纬度按比例投射到画布宽高上。这个做法在园区、校区、城区这种小范围内误差可控肉眼完全看不出形变。但如果是跨省、跨半球的大范围数据经度方向在真实球面上并不是等距的直接线性映射会导致高纬度地区被横向拉伸这时就得引入 Web Mercator 投影。我的建议是第一版先做线性映射把链路跑通后再决定要不要上投影。原因很简单Canvas 地图的难点从来不在投影公式而在数据从编辑端到渲染端流转时是否保持合法、一致。投影只是把经纬度变成像素的一个中间步骤替换起来成本很低但坐标层级错误、数据非法这类问题会让你连图都画不出来。2.2 一份能画起来的最简 GeoJSON从 FeatureCollection 认到坐标括号geojson.io 导出的数据最外层一定是一个 FeatureCollection。里面每个要素是一个 FeatureFeature 又包含 properties 和 geometry。写渲染函数之前先把这种嵌套结构吃透因为后面所有绘制和校验逻辑都在跟这个结构打交道。{ type: FeatureCollection, features: [ { type: Feature, properties: { name: 地块A }, geometry: { type: Polygon, coordinates: [ [ [116.391, 39.907], [116.394, 39.907], [116.394, 39.909], [116.391, 39.909], [116.391, 39.907] ] ] } } ] }这份 JSON 里最关键的是 geometry.coordinates 的嵌套层级。Point 的 coordinates 是一对数字[lng, lat]LineString 是数字对的数组[[lng, lat], [lng, lat]]Polygon 是在这个基础上再包一层表示“一个外环加若干内洞”所以是[[[lng, lat], [lng, lat]]]。你只要记住每多一种几何类型坐标就多一层括号最后一层永远是数字对。这个最小结构对整个方案意味着三件事第一Canvas 渲染函数只需要认 geometry.type 和 coordinates 这两样东西第二做数据校验时检查的就是“最后一层是不是数字对”第三从 geojson.io 复制数据到业务系统时不要动坐标系和嵌套层级保持原样流转。2.3 toCanvas把经纬度换算成屏幕坐标的最小函数先写一个通用的外包矩形计算函数。要点是把所有几何类型的坐标拉平成一个数字对数组再统一求最小最大值这样 Point、LineString、Polygon 都能复用同一套逻辑。// 递归拉平所有坐标点不区分几何类型 function flattenCoords(coords, out []) { if (typeof coords[0] number) { // 数字对说明已经到了 [lng, lat] out.push(coords); } else { // 还是数组嵌套继续向下展开 coords.forEach(c flattenCoords(c, out)); } return out; } // 计算整份 GeoJSON 的外包矩形 function calcBounds(features) { let minLng Infinity; let maxLng -Infinity; let minLat Infinity; let maxLat -Infinity; features.forEach(f { flattenCoords(f.geometry.coordinates).forEach(([lng, lat]) { minLng Math.min(minLng, lng); maxLng Math.max(maxLng, lng); minLat Math.min(minLat, lat); maxLat Math.max(maxLat, lat); }); }); return { minLng, maxLng, minLat, maxLat }; }flattenCoords 的作用是屏蔽几何类型差异让 calcBounds 不需要判断 Point 还是 Polygon。注意递归出口的判断是typeof coords[0] number因为经纬度本身就是数字这一层判断能可靠识别出“到底了”。如果你在项目里遇到 bounds 全是 Infinity基本就是某个 Feature 的 coordinates 是 null 或空数组这个函数会直接跳过。拿到 bounds 之后就可以做真正的地理坐标到像素的转换。// 经纬度转 Canvas 像素坐标 function toCanvas(lng, lat, bounds, width, height) { const x (lng - bounds.minLng) / (bounds.maxLng - bounds.minLng) * width; const y (bounds.maxLat - lat) / (bounds.maxLat - bounds.minLat) * height; return { x, y }; }x 的计算是正向比例映射lng 越小越靠左。y 的方向和经纬度相反纬度越高越靠北但在 Canvas 里 y 越小越靠上所以用maxLat - lat做翻转。如果你的地图画出来上下颠倒就是这里少了个翻转。调用时把 width 和 height 换成画布 CSS 宽度高度即可如果你的画布有 padding 需求可以在公式里再乘一个比例系数但初版不建议加先确保坐标方向正确。3. 把点线面画上画布H5 Canvas 渲染 GeoJSON 的最小实现坐标换算做完渲染就是纯粹的 Canvas 2D 操作。这一章给出点线面三分法的绘制函数再解决高清屏发虚这个几乎人人都会踩的坑最后用一个 renderMap 函数把数据和画布绑定起来。这套代码量不大但足够支撑起一个可用的地图渲染层。3.1 点线面三分法Circle、LineTo 与 Polygon 路径闭合GeoJSON 的几何类型很多但落到 Canvas 绘制上只有三种基本动作Point 画圆、LineString 画折线、Polygon 画面。MultiPoint、MultiLineString、MultiPolygon 不过是这三种动作的循环先看单类型怎么写。function drawPoint(ctx, x, y, style) { ctx.beginPath(); ctx.arc(x, y, style.radius || 4, 0, Math.PI * 2); ctx.fillStyle style.fillColor || #4a90d9; ctx.fill(); } function drawLine(ctx, points, style) { ctx.beginPath(); points.forEach((p, i) { i ? ctx.lineTo(p.x, p.y) : ctx.moveTo(p.x, p.y); }); ctx.strokeStyle style.strokeColor || #d94a4a; ctx.lineWidth style.lineWidth || 2; ctx.stroke(); } function drawPolygon(ctx, points, style) { ctx.beginPath(); points.forEach((p, i) { i ? ctx.lineTo(p.x, p.y) : ctx.moveTo(p.x, p.y); }); ctx.closePath(); ctx.fillStyle style.fillColor || rgba(74, 144, 217, 0.3); ctx.strokeStyle style.strokeColor || #4a90d9; ctx.lineWidth style.lineWidth || 1; ctx.fill(evenodd); ctx.stroke(); }drawPoint 用的是 arc 画圆半径、颜色都从 style 里取没传就用默认值。drawLine 用三元判断区分 moveTo 和 lineTo第一条点只移动不连线后面所有点依次连线。drawPolygon 在画完所有点之后必须调 closePath把最后一个点和第一个点连起来否则填充和描边会缺一条边。这里最容易忽略的是ctx.fill(evenodd)这个参数它决定多边形内部有洞时用哪种环绕规则填充后面避坑章节会细讲。如果你拿到的是 Multi 类型处理办法是拆开循环MultiPolygon 就遍历 coordinates 数组对每个 Polygon 子数组调用一次 drawPolygon。不要想着把 Multi 类型直接传给上面的函数它们接收的 points 是已经换算好的像素坐标数组和 GeoJSON 原始结构不是一回事。3.2 先调 devicePixelRatio否则高清屏地图必然发虚Canvas 在图上一旦模糊新手多半会去调画布宽高其实根因基本都在 devicePixelRatio。CSS 里的 1 个像素在 iPhone 的屏幕上可能对应 2 个甚至 3 个物理像素。Canvas 默认按 CSS 像素建画布等于用一个物理像素去画一个 CSS 像素的内容系统只能靠插值放大线条边缘自然就虚了。这个问题在 canvas 绘图场景里属于必调项不调后面导出图片也会是糊的。// 初始化画布处理高清屏 function setupCanvas(canvas, cssWidth, cssHeight) { const dpr window.devicePixelRatio || 1; canvas.width cssWidth * dpr; canvas.height cssHeight * dpr; canvas.style.width cssWidth px; canvas.style.height cssHeight px; const ctx canvas.getContext(2d); // 关键让 ctx 把 CSS 像素当成逻辑像素使用 ctx.setTransform(dpr, 0, 0, dpr, 0, 0); return ctx; }setupCanvas 做了三件事把 canvas 的绘图缓冲区设置为 CSS 尺寸乘 dpr把 CSS 尺寸写回 style让后续所有绘图代码都用 CSS 像素坐标。ctx.setTransform(dpr, 0, 0, dpr, 0, 0)是最容易漏的一句没有它你仍然需要手动给每个坐标乘 dpr代码会变得到处都是 dpr。设置完之后drawPoint、drawLine 里的 x、y 就是 CSS 像素跟浏览器里其他元素保持一致。这个函数建议放在项目公共工具里所有画布初始化都走它。我自己做移动端 H5 时会在 iOS Safari 上重点验证因为 iOS 的 devicePixelRatio 可能是 2 或 3写死任何值都会在某一台设备上翻车。用window.devicePixelRatio || 1动态取插值到弱设备上只会轻微影响性能不会出大问题。3.3 renderMap 一次重绘把数据和画布绑定起来上面都是零散函数真正把数据和画布绑起来的是一个 renderMap。它做的事情很朴素清空画布、计算 bounds、遍历 features、按几何类型分发绘制。function renderMap(canvas, data, opts {}) { const width opts.width || 750; const height opts.height || 450; const ctx setupCanvas(canvas, width, height); ctx.clearRect(0, 0, width, height); const bounds calcBounds(data.features); if (!bounds.minLng || !bounds.minLat) return; data.features.forEach(f { const geo f.geometry; if (geo.type Point) { const p toCanvas(geo.coordinates[0], geo.coordinates[1], bounds, width, height); drawPoint(ctx, p.x, p.y, opts.style); } else if (geo.type LineString) { const pts geo.coordinates.map(c toCanvas(c[0], c[1], bounds, width, height)); drawLine(ctx, pts, opts.style); } else if (geo.type Polygon) { const outer geo.coordinates[0].map(c toCanvas(c[0], c[1], bounds, width, height)); drawPolygon(ctx, outer, opts.style); } }); }renderMap 的定位是“数据变了就重绘一次”不适合直接用 requestAnimationFrame 每帧调用因为全量重绘在要素多时会卡。首个版本先把功能跑通后续优化方向是静态底图只画一次交互图层单独一层批量要素用分批渲染。参数里 width 和 height 是 CSS 尺寸style 是点线面的统一样式对象颜色、线宽、半径都可以从这里传。如果你要处理 Multi 类型在 forEach 里加一个判断当 geo.type 是 MultiPolygon 时遍历 geo.coordinates每一段都当作一个 Polygon 来调用 drawPolygon。代码会多几行但结构不变。这个 renderMap 未来会成为你自己 H5 可视化编辑器的最小渲染内核后续加鼠标事件、加绘制交互都是在这个函数外面包一层。4. 让用户自由绘制geojson.io 与 Canvas 的导入导出闭环这一章解决“自由绘制”从哪来、画完的数据怎么回。先明确 geojson.io 的角色定位再给导入导出的完整代码。核心观点是不要把 geojson.io 当成一个库集成进来而是把它的产出当成数据标准让 Canvas 去读写这份标准。4.1 先定位 geojson.io 的角色数据准备台不是运行时依赖geojson.io 是一个在线的 GeoJSON 编辑工具浏览器打开后能看到地图左边是 JSON 源码右边是地图预览可以直接在地图上拖拽画点、画线、画面还能编辑属性字段。它天然支持 http 方式访问也就是标题里写的 httpgeojson.io 这种入口。它的产出永远是一份标准 GeoJSON 文本这份文本才是我们真正需要的东西。在实际业务链路里geojson.io 通常扮演“数据准备台”这个角色。上线前业务人员用它在电脑上把地块边界、巡检路线、点位标号统统画好导出成一个 GeoJSON 文件运行时H5 页面负责读取这份文件并用 Canvas 渲染。也就是说它不是运行时依赖而是编辑期依赖。这也解释了为什么标题把 Canvas 和 geojson.io 并列Canvas 负责运行时表现geojson.io 负责编辑期数据生产两端通过 GeoJSON 文本对接。如果你拿到的是一个 .rar 打包的资源解压后通常会有示例页面、样例 GeoJSON 文件和说明文档。无论包里具体是什么链路本质都一样先用 geojson.io 画一份数据再用 Canvas 把这份数据渲染出来。先跑通这条最小闭环再去研究包里的扩展功能顺序不要反。4.2 把 geojson.io 导出的 JSON 喂给 Canvas导入动作本质上就是读取 JSON 文本、解析、再调 renderMap。因为 geojson.io 导出的一定是 FeatureCollection所以解析后第一步就检查顶层 type不合法直接抛错避免把乱数据送进渲染层。// 从 geojson.io 导入数据 function loadFromGeoJSON(jsonText) { let data; try { data JSON.parse(jsonText); } catch (e) { throw new Error(JSON 解析失败不是合法的 JSON 文本); } if (!data || data.type ! FeatureCollection) { throw new Error(数据不是 FeatureCollection请确认文件来源); } // 可选对 geometry 做合法性校验 data.features.forEach(f { if (!f.geometry || !f.geometry.type || !f.geometry.coordinates) { throw new Error(存在缺少 geometry 的 Feature); } }); const canvas document.getElementById(mapCanvas); renderMap(canvas, data, { width: 750, height: 450 }); }loadFromGeoJSON 做了两层防护JSON.parse 包了 try catch能拦掉非 JSON 内容顶层 type 检查能拦掉单 Feature 或其他 GeoJSON 类型。真正干活的就是最后两行拿到 canvas 元素后调 renderMap。如果你的数据是通过 fetch 从接口拉的把 jsonText 换成 await response.text() 即可其他逻辑完全复用。这里有一个我踩过的坑geojson.io 左侧编辑器是实时生效的你在地图上画的每一个点都会立刻写进 JSON。如果你直接从左侧复制 JSON偶尔会复制到中间状态比如一个 Polygon 还没画完coordinates 数组只有一个点。所以从浏览器复制 JSON 之后建议先贴到任意 JSON 格式化工具里看一眼层级再进代码能省不少排查时间。4.3 把 Canvas 上画的形状导出为合法 GeoJSON反向链路是用户在 H5 里自己画了形状需要把这些形状生成一份能重新拖回 geojson.io 的 GeoJSON。这里最稳妥的思路是维护一个 shapes 数组作为中间态Canvas 只负责画 shapes 的内容导出时把 shapes 转成 FeatureCollection。// 把内部绘制记录转成 GeoJSON function buildGeoJSON(shapes) { const features shapes.map(s { let geometry; if (s.type point) { geometry { type: Point, coordinates: [s.lng, s.lat] }; } else if (s.type line) { geometry { type: LineString, coordinates: s.lngLats }; } else if (s.type polygon) { geometry { type: Polygon, coordinates: [s.lngLats] }; } else { throw new Error(未知绘制类型 s.type); } return { type: Feature, properties: s.props || {}, geometry }; }); return { type: FeatureCollection, features }; } // 下载为文件 function downloadGeoJSON(data) { const blob new Blob([JSON.stringify(data, null, 2)], { type: application/geojson }); const url URL.createObjectURL(blob); const a document.createElement(a); a.href url; a.download draw-result.json; a.click(); URL.revokeObjectURL(url); }buildGeoJSON 的输入是业务里自己维护的 shapes输出永远是三个固定字段type、properties、geometry。注意 Polygon 的 coordinates 外包了一层数组写的是[s.lngLats]少这一层导出的文件在 geojson.io 里会显示不出来这一层也是新手最容易漏的。properties 即使没有业务属性也要给空对象{}因为 geojson.io 右侧表格依赖 properties 字段展示。关于 Blob很多人问“H5 的 blob 文件能上传吗”答案是能但要注意姿势。上面代码里 blob 只是用来触发浏览器下载如果要把文件传到服务端需要formData.append(file, blob, draw-result.json)再走 XHR 或 fetch。还有一个小坑iOS 上直接点下载经常把 .json 当成预览打开更稳的做法是把 GeoJSON 文本传给后端由后端生成下载链接。5. 常见问题排查Canvas 绘制地图的偏移、发虚与数据坑Canvas 地图跑通渲染容易但遇到批量数据、移动端真机、手绘数据时问题就冒出来了。这一章按现象、原因、解决三个步骤写全是做这类项目绕不开的高频坑。5.1 地图整体偏移几百米坐标系混用的典型症状现象画出来的地图和真实底图对不上整体向某个方向平移了几百米看起来像是所有点都被“挪”了一下。这种偏差在城区数据里特别明显几公里范围就能看出来。原因GeoJSON 里存的是 WGS-84 坐标而你现在拿来做底图对比的浏览器地图服务可能是 GCJ-02 坐标。两套坐标系的参考椭球和偏移算法不同经纬度数值差零点零零几度反映到地面上就是几百米。geojson.io 的地图底图本身就是一套坐标系体系如果你把它的坐标直接画到另一套体系的底图上偏差就会暴露出来。解决首先确认全链路统一坐标系。最省事的方案是让 Canvas 只做纯渲染不做任何坐标系纠偏底图和 GeoJSON 都保证是同一套坐标。如果业务里既有 GCJ-02 底图又有 WGS-84 数据就需要做坐标转换转换后再调 toCanvas。不要试图在 Canvas 里手动加减一个固定偏移值这种“看着差不多”的做法在真实项目里会反复翻车因为不同经纬度的偏移量不是线性的。5.2 高清屏线条发虚devicePixelRatio 是罪魁祸首现象同一份代码在电脑上显示清晰拉到 iPhone 上线条变模糊文字边缘有毛刺。导出图片后放到手机上看问题更明显。原因canvas 的绘图缓冲区只有 CSS 像素大小在高清屏上被浏览器物理放大系统通过插值算法补像素结果就是发虚。这个现象在 iOS Safari 上尤其严重部分机型在离屏 canvas 上还会出现导出白图的问题本质都是缓冲区尺寸不对。解决初始化画布时调用前面写的 setupCanvas让绘图缓冲区等于 CSS 尺寸乘 devicePixelRatio然后用 setTransform 把逻辑像素对齐到 CSS 像素。注意 setTransform 必须在绘制前调用如果在绘制过程中调用会清空当前画布内容。判断代码里有没有踩这个坑最快的办法是打印 canvas.width 和 canvas.clientWidth如果两者相等基本可以确定没做高清屏适配。5.3 Polygon 填充出现怪异镂空路径闭合与环绕规则的锅现象画一个回字形地块填充完发现中间本该空的地方被填了色或者本该填色的地方出现了透明条纹形状像撕坏的贴纸。原因两个合成因素。第一多边形路径没 closePathCanvas 会自动用一条直线把起点和终点连起来填充但这条边不是你预期路径的一部分视觉上会多出一条异常边。第二Polygon 内部如果有洞Canvas 默认使用 nonzero 环绕规则如果外环和洞的绘制方向不一致内外区域会被错误识别。解决画每条 polygon 路径后都调 closePath有洞时把洞的点也画进同一个 path并让洞的方向与外环相反最后用ctx.fill(evenodd)强制按奇偶规则填充。evenodd 只关心一个点被路径包围的次数被包围一次填充两次不填充不依赖绘制方向是处理带洞 Polygon 最稳的方式。// 带洞 Polygon 的绘制 function drawPolygonWithHoles(ctx, outer, holes, style) { ctx.beginPath(); outer.forEach((p, i) i ? ctx.lineTo(p.x, p.y) : ctx.moveTo(p.x, p.y)); holes.forEach(hole { // 洞的方向与外环相反配合 evenodd 使用 hole.slice().reverse().forEach((p, i) { i ? ctx.lineTo(p.x, p.y) : ctx.moveTo(p.x, p.y); }); }); ctx.closePath(); ctx.fill(evenodd); ctx.stroke(); }在 GeoJSON 结构里Polygon 的 coordinates 第一项是外环后面每项都是一个洞。实现时把外环和所有洞都放进同一个 path洞的点序反转只是为了兼容不写 evenodd 的情况写了 evenodd 后反转不是必须的但两个都做最保险。5.4 数据拖回 geojson.io 不显示GeoJSON 坐标层级先自检现象在 Canvas 里画完导出成文件拖进 geojson.io 后地图上一片空白或者某些面消失了但 JSON 里明明有数据。原因坐标层级不对。最常见的是 Polygon 少了一层括号把[[[lng, lat]]]写成了[[lng, lat]]geojson.io 解析时把这个数组当 LineString 的坐标于是显示不出来。另一种情况是最后一个点没有和第一个点相同面缺了一条闭合边geojson.io 会自动容错但 Canvas 渲染可能已经花了。解决导出前写一个校验函数用递归检查坐标嵌套层级是否匹配几何类型。点只需要一层线需要两层面需要三层这是规则。// 校验 GeoJSON 坐标层级 function getDepth(coords) { if (typeof coords[0] number) return 1; return 1 getDepth(coords[0]); } function assertValidGeoJSON(data) { data.features.forEach((f, idx) { const g f.geometry; const depth getDepth(g.coordinates); const expect { Point: 1, MultiPoint: 2, LineString: 2, MultiLineString: 3, Polygon: 3, MultiPolygon: 4 }; if (depth ! expect[g.type]) { throw new Error(Feature ${idx} 的 ${g.type} 坐标层级是 ${depth}期望 ${expect[g.type]}); } }); }getDepth 的逻辑和 flattenCoords 正好相反它只数嵌套层数不展开。校验函数放在 buildGeoJSON 之后、downloadGeoJSON 之前能拦截九成以上的数据非法问题。我自己养成的习惯是每次导出前都调一次这个校验几十个要素的项目里它能直接揪出两三个手误。5.5 大数据量卡顿掉帧别把重绘当动画循环用现象几千个点或几百个面时拖动画布或缩放明显掉帧CPU 占用率居高不下。代码里可能只是加了一个通用 resize 监听每帧都调 renderMap。原因renderMap 是全量重绘每次调用都清空整个画布重新计算所有坐标重新走一遍 Canvas 路径。数据量小感觉不到数据量上来后Canvas 路径构建和填充计算就成了瓶颈。用 requestAnimationFrame 每帧调 renderMap 会把问题放大因为 rAF 并不会帮你去重它只是保证每帧执行一次。解决分两步。第一步静态层和动态层分开底图、已经画完的地块这类不变内容只画一次交互过程中用另一个 canvas 叠在上面画临时图形两者用 CSS 定位对齐。第二步如果必须全量重绘用分批渲染让每帧只处理一部分要素避免单帧卡死。// 分批渲染示意 let renderIndex 0; const CHUNK_SIZE 200; function renderBatch(features, ctx, bounds, width, height) { const chunk features.slice(renderIndex, renderIndex CHUNK_SIZE); chunk.forEach(f { // 这里复用点线面的绘制逻辑 }); renderIndex CHUNK_SIZE; if (renderIndex features.length) { requestAnimationFrame(() renderBatch(features, ctx, bounds, width, height)); } }CHUNK_SIZE 需要根据设备调整移动端建议 100 到 200桌面端 500 到 1000。经验是每个分片的绘制耗时控制在 16ms 以内也就是一帧的预算。切记不要在 rAF 回调里做 calcBounds外包矩形坐标算一次就够了存下来给所有帧复用。6. 进阶套底图校准并把绘制结果送回 geojson.io 验收方案跑通后下一步通常是把某人提供的静态地图图片叠到底下做视觉校准以及把导出的数据送回 geojson.io 做格式仲裁。这两个技巧能让 Canvas 地图从“能画出来”升级到“能交付”。6.1 用静态图片底图做视觉对齐很多业务场景没有标准瓦片底图只有一张规划图或 CAD 导出的图片。这时可以把图片直接画到 Canvas 底部GeoJSON 数据叠在上面通过半透明观察两者是否对齐。const img new Image(); img.onload function () { // 把图片铺满画布 ctx.drawImage(img, 0, 0, canvasCssWidth, canvasCssHeight); // 再覆盖半透明的 GeoJSON 图层 ctx.globalAlpha 0.7; renderMap(canvas, data, { width: canvasCssWidth, height: canvasCssHeight }); ctx.globalAlpha 1; }; img.src base-map.png;drawImage 先画底图renderMap 后画数据数据里的经纬度按 2.3 节的公式换算成像素。如果底图和数据偏移用鼠标拖拽底图位置或微调 bounds 映射即可不要改数据本身。这个做法做出来的 H5 可视化编辑器雏形底图导入是第一功能数据图层叠加是第二功能两者都在一个 canvas 上完成。6.2 三分钟闭环验证导出、拖回 geojson.io、再重新导入验收手段比验收理论重要。我的习惯是每次导出的 GeoJSON 都拖回 geojson.io 看一眼重点确认三件事图形位置和 geojson.io 底图一致、属性表里能看见 properties 字段、导出再导入后图形没丢。这一步能抓住很多隐蔽问题尤其是坐标层级和空 properties。具体做法是buildGeoJSON 生成文件后不直接下载先拖到 geojson.io 右侧地图区域看能不能正常显示。能显示这份数据才是真正合法的。然后再把同一份 JSON 从 geojson.io 导出喂回 Canvas 的 loadFromGeoJSON对比渲染结果是否一致。两端都能通过说明这套数据流转链路是闭合的。我自己早期做地块圈选项目时跳过 geojson.io 这步“格式裁判”导出的面少了一层括号数据在业务系统里能用但合作方拿去其他工具打开就丢形状浪费了半个下午排查。后来把“导出、拖回去检查、再导回”固化成了流程动作三个项目做下来再也没在数据格式上翻过车。这种肉眼可验证的闭环比任何代码 review 都更直接。希望帮到你也建议你从第一个小项目开始就把这个习惯带上。本文还有配套的精品资源点击获取
网站建设高端定制企业官网