
最近在做微信小程序的数据可视化项目需要在首页展示一张中国地图用来呈现各区域的业务数据分布情况。这个需求看着简单真动手才发现坑不少——小程序环境和网页不一样没有DOM节点Echarts的好多用法都要换一套思路来写。这篇文章我把在小程序里用Echarts加载中国地图的完整步骤重新捋一遍从组件引入、GeoJSON数据处理到option配置、交互联动都给出能直接复制的代码和踩坑记录。1. 整体思路为什么在微信小程序里用Echarts画中国地图1.1 需求场景与方案对比小程序里做数据可视化最常见的需求之一就是地图。业务上通常会把订单量、用户数、销售额这些指标按省份汇总然后投到一张中国地图上用颜色深浅表达数值大小。这种图放在数据大屏、管理后台、运营报表里都非常直观。但在小程序里实现地图可视化方案其实没有网页端那么随意。我梳理了一下主流的选择主要有三种方案优点缺点微信原生map组件系统级渲染原生组件性能好有marker、polyline、polygon内置底图样式固定自定义绘制能力弱不支持区域色块填充这种地图统计图uCharts / F2等轻量图表库体积小启动快适合折线图柱状图地图支持很弱F2的地图能力需要额外折腾文档少Echartsecharts-for-weixin生态成熟地图Series、visualMap、markPoint都能用社区案例多包体积大canvas渲染性能要自己优化Echarts在小程序里能用靠的是官方和社区维护的echarts-for-weixin这个适配层。它把Echarts依赖的DOM操作封装成小程序canvas能理解的方式然后用一套近乎原生的API来初始化图表。这意味着你在网页端写过的配置项、数据格式、交互事件在小程序里基本都能复用学习成本几乎为零。1.2 小程序里Echarts的运行机制网页端用Echarts核心是echarts.init(document.getElementById(main))因为浏览器能直接操作DOM。小程序不行没有document组件渲染靠的是canvas两层结构——一个离屏canvas负责绘制一个展示canvas负责显示。echarts-for-weixin就干了这么一件事把init方法的第一个参数从DOM节点换成小程序canvas实例内部替你处理了坐标系、事件绑定、动画调度这些差异。所以你会看到编写代码时初始化变成了这样function initChart(canvas, width, height, dpr) { const chart echarts.init(canvas, null, { width: width, height: height, devicePixelRatio: dpr }); canvas.setChart(chart); chart.setOption({...}); return chart; }这里的canvas不是你在wxml里写死的那个canvas标签而是ec-canvas组件通过ec属性回调给你的一个封装对象。这个封装对象内部维护了Echarts实例的完整生命周期你只需要在回调里拿到它然后正常setOption就行。理解这一层你才不会被各种报错搞晕。很多新手遇到map is not defined或者canvas未初始化其实就是没有搞清楚ec-canvas的初始化机制或者把网页端直接echarts.init的代码搬过来用了。1.3 实现地图渲染的完整链路在小程序里用Echarts加载中国地图整个流程可以拆成四段引入echarts-for-weixin组件到小程序项目并完成页面级配置。准备中国地图的GeoJSON数据也就是省份边界经纬度数据。把GeoJSON注册为名为china的地图资源。使用type: map的series配合data数据渲染出地图。听起来不复杂但每一段都有细节尤其是GeoJSON的获取和注册方式很多人在这块翻车。接下来我按顺序一步步拆开讲。2. 环境准备把echarts-for-weixin组件装进项目2.1 获取echarts-for-weixin组件文件第一步是拿到封装好的组件文件。Echarts官方仓库维护了一个微信小程序分支核心文件就两个ec-canvas/ec-canvas.js组件逻辑负责canvas生命周期管理ec-canvas/ec-canvas.wxmlec-canvas.wxssec-canvas.json组件模板和样式ec-canvas/echarts.js压缩后的Echarts核心库拿到这些文件最稳的方式是直接去官方仓库下载对应版本然后整个ec-canvas目录拷到你项目根目录下。如果你使用的微信开发者工具支持npm构建也可以走npm流程安装对应包但目录结构要大写自己核对因为后续引用的路径一旦写错编译期就会报组件未找到。这里提醒一下echarts.js这个文件体积不小全量引入大约1MB左右。小程序主包有2MB限制很多项目一开始就超了。后面第6章我会专门讲体积优化的办法。2.2 在页面中注册并引用组件假设你的项目结构是├── ec-canvas/ │ ├── ec-canvas.js │ ├── ec-canvas.json │ ├── ec-canvas.wxml │ ├── ec-canvas.wxss │ └── echarts.js ├── pages/ │ └── map/ │ ├── map.wxml │ ├── map.js │ ├── map.json │ └── map.wxss你需要在map.json里声明使用这个组件{ usingComponents: { ec-canvas: ../../ec-canvas/ec-canvas } }然后在map.wxml里放一个容器里面嵌入ec-canvas组件view classmap-container ec-canvas idchina-map canvas-idchina-map ec{{ ec }}/ec-canvas /view这里的ec必须是一个包含onInit函数的对象在map.js里定义import * as echarts from ../../ec-canvas/echarts; function initChart(canvas, width, height, dpr) { const chart echarts.init(canvas, null, { width: width, height: height, devicePixelRatio: dpr }); canvas.setChart(chart); chart.setOption({}); // 后续在这里填地图配置 return chart; } Page({ data: { ec: { onInit: initChart } } });注意几个关键点。canvas-id不能和页面里其他canvas重复否则真机上可能渲染错乱。ec对象不是响应式的不需要在视图层做数据绑定它只负责在组件初始化时把回调传进去。另外ec-canvas的高度高度由其外层容器决定所以map-container必须给一个明确的宽度和高度不能是0否则图表不会显示。2.3 组件初始化的细节与常见误区ec-canvas组件支持两种初始化模式一种是页面加载时就自动初始化也就是上面这种ec.onInit的方式另一种是懒加载适合数据通过接口异步获取、需要手动控制加载时机的场景。懒加载要在组件上多加一个lazy-load属性然后在需要的时候手动调用this.selectComponent(#china-map).init((canvas, width, height, dpr) { const chart echarts.init(canvas, null, { width: width, height: height, devicePixelRatio: dpr }); canvas.setChart(chart); this.chart chart; return chart; });两种模式二选一不要混用。我见过有人又传了onInit又调用init结果canvas初始化了两次报There is a chart instance already initialized on the canvas的错。遇到这个报错检查一下你是不是重复初始化了。还有一点开发者工具模拟器上一切正常、真机上白屏的问题多半出在canvas类型上。现在是2024年了新项目建议直接用小程序基础库支持的canvas 2D模式也就是在ec-canvas组件上加type2d初始化方式略有差异但渲染性能和清晰度都更好。如果触屏事件或者动画性能有要求2D模式是首选。3. 中国地图数据准备GeoJSON获取与注册3.1 GeoJSON是什么Echarts为什么离不开它Echarts的地图series本身不带任何地图边界数据。你要画中国地图首先得告诉它中国的省份边界长什么样——这就是GeoJSON干的事。GeoJSON是一种基于JSON的地理数据格式里面用FeatureCollection组织多个区域。以中国地图为例每个省就是一个Feature它的geometry字段存了该省边界上所有点的经纬度坐标properties里通常还带着省份名称和adcode编码。{ type: FeatureCollection, features: [ { type: Feature, properties: { name: 北京市, adcode: 110000 }, geometry: { type: MultiPolygon, coordinates: [...] } } ] }Echarts拿到这份数据后内部会做一次坐标投影转换把经纬度变成canvas上的xy像素坐标然后根据你把name字段匹配到series的data里决定每个区域填充什么颜色、响应什么事件。所以GeoJSON是地图渲染的地基地基不牢后面全白搭。3.2 获取中国地图GeoJSON的渠道获取这份数据的渠道不少但我实际操作下来最推荐的是阿里云DataV.GeoAtlas。它提供公开的中国及各省份GeoJSON服务更新的频次也还行而且已经帮你做过一些简化处理。访问DataV.GeoAtlas页面后在右侧有个中国的地图预览可以直接下载一份完整的中国GeoJSON文件。下载下来的数据是标准GeoJSON格式直接用就行。如果你要的是省级下钻数据——比如点击广东再显示广东省各市的地图——DataV也提供每个省的独立GeoJSONURL一般形如https://geo.datav.aliyun.com/areas_v3/bound/100000_full.json其中100000是中国的adcode换成省份的adcode就能拿到对应省份的数据比如广东的adcode是440000。还有一点要注意下载下来的GeoJSON里面的properties.name可能是北京市广东省这种带市/省后缀的但你的业务数据里可能只写了北京广东。Echarts的data匹配是基于name字段精确匹配的所以要么在GeoJSON里做name映射要么在series配置里用nameMap做别名转换series: [{ type: map, map: china, nameMap: { 北京市: 北京, 广东省: 广东 }, data: [ { name: 北京, value: 100 }, { name: 广东, value: 200 } ] }]这种细节业务数据一多匹配不上时地图上就会全是空白排查起来还挺隐蔽的。3.3 本地引入GeoJSON的两种方式拿到GeoJSON文件后怎么在小程序里喂给Echarts有两种常见做法。第一种是把GeoJSON保存成china.js文件放在项目里比如/utils/map/china.js路径下然后用CommonJS导出module.exports { type: FeatureCollection, features: [...] };在页面JS里引入并注册const chinaMap require(../../utils/map/china.js); echarts.registerMap(china, chinaMap);这种方式的好处是不依赖网络请求稳定、离线可用。缺点是文件本身有几百KB会占用包体积。所以很多人会再对GeoJSON做简化比如删除不必要的精度位数、简化边界坐标点数量把几百KB压缩到100KB以内。第二种是把GeoJSON放在服务器上然后用wx.request请求wx.request({ url: https://your-domain.com/map/china.json, success(res) { echarts.registerMap(china, res.data); // 再 setOption } });这种方式的优点是不占包体积缺点是必须在微信公众平台配置request合法域名而且每次进入地图页都要等网络请求完成交互上会有一个白屏等待期。解决等待期的办法是先用空白canvas占位数据加载完再调用setOption或者把GeoJSON缓存到本地存储。考虑到小程序“包体积寸土寸金”的特点我的建议是如果只是展示一张中国地图而且项目没有分包计划那就用本地引入但务必做坐标简化如果地图数据会频繁更新或者本身就要做省市两级下钻那远程加载再加缓存方案更合适。3.4 registerMap的调用时机与重复注册echarts.registerMap必须在setOption之前调用因为setOption里引用了map: china时Echarts会立刻去内存里寻找已经注册过的地图资源。如果没找到界面不会报错但series渲染出来是空白的甚至连网格都看不到。另外注意不要重复注册同一个地图名字。有些人的代码在onLoad里注册一次initChart里又注册一次控制台会打印一条Map china already exists的警告。虽然这种警告不影响运行但说明你的代码里有重复执行的地方还是理顺比较好。4. 核心实现配置option渲染中国地图4.1 页面布局与canvas初始化假设你已经把ec-canvas组件接入好了wxml里有一个.map-container容器view classmap-container ec-canvas idchina-map canvas-idchina-map force-use-old-canvastrue ec{{ ec }}/ec-canvas /view对应wxss.map-container { width: 100%; height: 600rpx; padding: 20rpx; box-sizing: border-box; }注意height: 100%在页面里经常因为父容器高度没撑起来而变成0所以最好给固定高度或rpx高度。调试时可以先写死600rpx确认渲染正常后再换成你真正需要的布局方式。4.2 一个完整可跑的地图option在initChart里填入下面这组配置就能看到第一张中国地图function initChart(canvas, width, height, dpr) { const chart echarts.init(canvas, null, { width: width, height: height, devicePixelRatio: dpr }); canvas.setChart(chart); const chinaMap require(../../utils/map/china.js); echarts.registerMap(china, chinaMap); chart.setOption({ tooltip: { trigger: item, formatter: function(params) { if (params.value) { return params.name br/数值 params.value; } return params.name; } }, visualMap: { min: 0, max: 1000, left: 20, bottom: 20, text: [高, 低], inRange: { color: [#e0f3f8, #74add1, #4575b4] } }, series: [{ type: map, map: china, roam: true, label: { show: true, fontSize: 10 }, data: [ { name: 北京, value: 100 }, { name: 上海, value: 200 }, { name: 广东, value: 500 }, { name: 四川, value: 300 } ] }] }); return chart; }这个配置里visualMap负责把数据值映射成颜色数据库中值越大颜色越深。series.data里的name要和注册地图的properties.name匹配匹配不上的区域不显示数据颜色会变成灰色空白。roam: true允许用户在真机上通过手势缩放和拖动地图做数据大屏时常开的选项。运行起来你应该能看到一张带省份边界、有颜色深浅的中国地图。到这里最核心的步骤已经打通了。4.3 异步数据下的延迟setOption实际项目中中国地图的轮廓数据可以本地引入但业务数据比如各省的销售额通常需要通过接口异步获取。做法是在initChart里先只渲染地图轮廓然后等接口返回后再调用chart.setOption更新数据。ec-canvas的onInit回调拿到chart实例后你需要把它挂到this或页面实例上方便后续更新。Page({ data: { ec: { onInit: initChart } }, onLoad() { wx.request({ url: https://your-domain.com/api/province-data, success: (res) { const data res.data.map(item ({ name: item.provinceName, value: item.value })); this.chart.setOption({ series: [{ data: data }] }); } }); } }); function initChart(canvas, width, height, dpr) { const chart echarts.init(canvas, null, { width: width, height: height, devicePixelRatio: dpr }); canvas.setChart(chart); const chinaMap require(../../utils/map/china.js); echarts.registerMap(china, chinaMap); chart.setOption({ series: [{ type: map, map: china, roam: true, label: { show: true } }] }); // 挂到页面实例 const pages getCurrentPages(); const currentPage pages[pages.length - 1]; currentPage.chart chart; return chart; }这种方式的好处是页面秒开地图轮廓先呈现数据回来再局部更新用户体验比较流畅。4.4 数据格式对不上的排查思路如果你发现某个省份始终不显示数据先从这两个方向排查第一看省份名是否完全一致。一个特别容易踩的坑是GeoJSON里的name是广西壮族自治区业务数据里写的是广西Echarts明确不会做模糊匹配必须完全相等。第二看数据是字符串还是数字。value字段如果传成了字符串visualMap有可能不认因为数值比较时需要数字类型。接口返回的字段经常是字符串记得先Number()转一下。value: Number(item.value)这两个问题解决之后90%的地图数据空白问题都能搞定。5. 进阶交互markPoint、tooltip与省份联动5.1 在指定城市上打标记点markPoint很多需求会在地图上再叠加一些关键城市标记比如总部所在地、重点门店位置。实现方式有两种看你想要的效果。第一种是直接在map系列的markPoint里配置series: [{ type: map, map: china, markPoint: { symbol: pin, symbolSize: 22, label: { show: true, formatter: {b} }, data: [ { name: 北京, coord: [116.4, 39.9] }, { name: 广州, coord: [113.26, 23.13] } ] } }]coord直接填城市的经纬度Echarts会处理坐标映射。这种方案适合标记点数量不多、只是做静态展示的场景。第二种是叠加一个独立的scatter散点系列series: [ { type: map, map: china, data: provinceData }, { type: scatter, coordinateSystem: geo, data: [ { name: 北京, value: [116.4, 39.9, 100] }, { name: 广州, value: [113.26, 23.13, 200] } ], symbolSize: function(val) { return val[2] / 50; } } ]scatter系列的好处是可以利用symbolSize做气泡大小映射视觉上更直观还能把经纬度数据统一维护在一个数组里后续做动态更新更方便。很多人会纠结markPoint里的coord到底该填什么。记住一个原则Echarts的map坐标系里coord的数字顺序是[经度纬度]不要倒过来写。这个顺序和你在高德地图、百度地图里看到的经纬度习惯一致但如果你从某些地理转换函数里拿数据很容易出现[纬度经度]的反转造成的现象就是标记点跑到海上去。5.2 tooltip定制与事件绑定tooltip是用户交互的第一入口。在小程序里Echarts的tooltip是可以正常显示的但有一点要注意formatter函数内只能返回字符串不能操作DOM因为小程序环境本身就是无DOM的。tooltip: { trigger: item, formatter: function(params) { if (params.seriesType map) { return params.name (params.value || 0); } if (params.seriesType scatter) { return params.name 数值 (params.value[2] || 0); } return ; } }事件绑定方面常规做法是chart.on(click, function(params) { console.log(点击了, params.name); });但我实际在真机测试时发现某些版本和iOS环境下chart.on(click)对地图区域的点击事件不够稳定尤其是地图被拖拽或缩放后点击事件容易丢。一个更稳妥的替代方案是把click事件挂到底层zrender对象上chart.getZr().on(click, function(event) { const pixel { x: event.offsetX, y: event.offsetY }; const point chart.convertFromPixel({ seriesIndex: 0 }, pixel); // point 是经纬度数组然后自己判断落在哪个省份内 });不过convertFromPixel方案需要你自己维护省份边界判断逻辑复杂一些。我的经验是如果是静态地图、无roam交互直接用chart.on(click)没问题如果开了roam最好用click加节流的方式或者干脆把roam关掉换成一键缩放按钮交互更可控。5.3 省份下钻的实现思路地图下钻是高频需求点广东省跳转到一个展示广东各市地图的页面。我的做法是把省地图GeoJSON也提前准备好点击事件里用params.name拿到省份名然后跳转页面并把省份名作为参数传过去chart.on(click, function(params) { wx.navigateTo({ url: /pages/province-map/province-map?province encodeURIComponent(params.name) }); });在省份地图页面根据省份名找到对应的adcode请求或加载对应省份的GeoJSON注册成新的地图名再正常渲染。整个逻辑和中国地图完全一致区别只在数据源。有一个体验上的细节要注意如果用户点的是南海诸岛这种特殊区块GeoJSON里通常会有一块表示南海诸岛的区域name常常是南海诸岛或空字符串。跳转前最好做一个白名单过滤否则用户点到海上没反应或跳到一个空白页体验就很差。if ([南海诸岛, ].includes(params.name)) { return; }5.4 项目使用uni-app或其他框架时的适配思路热词里有不少人在搜uni-app微信小程序相关的内容。如果你是用uni-app开发再在小程序端用Echarts思路是类似的但有几处适配需要注意。uni-app 2.8.1之后官方推荐使用renderjs配合echarts实现网页端类似的DOM渲染这样能绕开小程序canvas的很多限制。不过在小程序端renderjs实际上也是映射到canvas实现的。更通用的做法是继续使用ec-canvas组件把它作为静态资源放到uni-app项目的wxcomponents目录下然后在页面中通过usingComponents注册。uni-app里很多人在H5端和App端用vue-echarts而小程序端用echarts-for-weixin这是两套写法。要统一的话可以在页面组件里做一层封装对外暴露同一个renderChart方法内部判断运行环境H5走DOM初始化小程序走ec-canvas初始化。这样业务代码不用改只是底层适配不同。6. 常见问题排查与性能优化实录6.1 常见问题速查表下面这些是我在开发过程中实际遇到过的问题以及对应的排查手段整理成一张表方便你对照使用。问题现象根本原因解决办法地图空白只有灰色网格GeoJSON未注册或map名字和注册名不一致检查echarts.registerMap(china, data)和series[0].map china部分省份不显示数据颜色name匹配不上或value是字符串核对省份名用Number()转换数值真机不渲染模拟器正常canvas类型问题或基础库版本太低切换canvas 2D模式升级基础库tooltip不弹出ec-canvas封装导致的触摸事件冲突检查是否在canvas上叠加了遮罩层尝试移除disableTouch之类的属性iOS上拖动地图卡顿canvas性能不足动画开销大关掉动画animation: false降低devicePixelRatio地图初始渲染一闪而过初始化前容器尺寸为0给容器固定宽高或用wx.createSelectorQuery获取实际尺寸后再init报错mapView is not defined注册地图时数据格式错误确认GeoJSON是标准的FeatureCollection结构页面切后台再回来白屏图表实例被销毁或canvas重建在onShow里重新初始化或把option暂存后恢复6.2 包体积优化与性能调优前面提到echarts全量js体积不小针对这个问题有三个常用手段。第一用自定义构建。Echarts官网的在线定制工具可以勾选你需要的模块比如只保留地图、柱状图、折线图、散点图和visualMap相关模块构建出来的echarts.min.js可以压到400KB以内。如果你的项目只用地图甚至能压到更小。第二把地图数据做简化。GeoJSON里的坐标点通常非常密很多精度位对显示没有影响。你可以用一些地图坐标简化工具将坐标精度从6位小数降到2位小数再删掉一些冗余点。我试过一个中国全量GeoJSON从700KB压到200KB视觉上几乎没有差异写几个工具脚本一跑就行。第三使用分包。地图页在微信小程序里适合放进分包因为地图数据体积大、打开频率低分包可以显著降低主包加载压力。微信分包最大支持单分包2MB整个分包不超过30MB放一个地图页加echarts库绰绰有余。性能方面在地图上做大量动画是不推荐的canvas在真机上的绘制能力有限。比如设置roam开启时拖动和缩放的过程中如果tooltip频繁刷新会明显卡顿。我的处理办法是tooltip: { confine: true, animation: false }, series: [{ type: map, roam: true, scaleLimit: { min: 1, max: 3 } }]scaleLimit限制了缩放范围避免用户拉到特别大导致渲染压力陡增animation: false关掉tooltip动画交互响应更快。6.3 真机兼容性与iOS渲染注意事项热词里反复出现iOS、渲染机制相关的关键词这里集中说几个我在真机上的经验。第一iOS上canvas是WebGL还是2D的选择会影响性能。ec-canvas默认走2D canvas在iOS上如果遇到加载卡顿可以试试把devicePixelRatio传小一点比如2减少像素填充量。const chart echarts.init(canvas, null, { width: width, height: height, devicePixelRatio: 2 });dpr过大会导致canvas的实际像素宽高非常大内存和绘制压力都会成倍上升。第二地图上如果有大量的label在iOS低端机比如iPhone 8或更老可能会出现文字重叠或模糊。解决方法是适当调大label.fontSize或者在缩放级别较小时关掉labellabel: { show: true, formatter: function(params) { return params.name.length 2 ? params.name.slice(0, 2) : params.name; } }第三如果页面包含原生组件map、video、textarea等和echarts canvas重叠在iOS上原生组件会覆盖canvas产生层级错乱。这是因为微信原生组件默认是独立同层渲染普通cover-view和canvas都不一定能盖住它。解决办法是彻底避免让原生组件与图表重叠或者在布局上让两者分开。6.4 调试工具与断点定位技巧在小程序里调试Echarts地图比网页端麻烦的地方在于没有浏览器DevTools的Elements面板查看canvas内容。我习惯用这几个手段配合定位问题。一是把echarts实例暴露到全局在控制台直接执行chart.getOption()查看当前option是否正常确认是否有重复设置、数据是否更新进去。二是在registerMap后面打印一下注册的地图数据量console.log(map features:, chinaMap.features.length);如果这里打印出来是0说明GeoJSON加载失败后面渲染必然空白。三是用wx.setStorageSync缓存调试数据这样模拟器或真机每次冷启动时可以先从缓存里取数据方便复现问题。写在最后的一点经验这套流程我前后在几个项目里反复用过最大的感受是微信小程序里用Echarts加载中国地图本质上没有特别高深的技术难点卡壳的地方几乎都在数据准备和canvas初始化上。GeoJSON格式不对、name不匹配、容器高度为0、初始化重复这几个问题占了日常排查的八成以上。我个人在实际项目中的习惯是先把地图渲染做通再叠加业务数据最后才做交互效果。每一步验证通过再走下一步而不是一次性写完一大段代码再回去找bug。地图渲染这种涉及数据、canvas、配置三方的功能分层验证能省下大量调试时间。还有一个建议做好之后把整个地图页封装成一个通用组件传入省份数据和配置项就能复用。我在两个项目之间迁移时直接把组件拷过去改一下数据源就上线了省了不少重复工作。如果你后续还有地图下钻、自适应缩放、动态轮播高亮这些需求底层思路都是一样的——先把GeoJSON数据和setOption之间的通路摸清楚后面就是在option里加配置的问题了。