免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

基于Hadoop+Spark的全国天气可视化系统实践

基于Hadoop+Spark的全国天气可视化系统实践 简介全国天气数据可视化分析系统是一套覆盖数据采集、存储、计算与展示全流程的大数据实战项目主要面向正在学习Hadoop、Spark生态的技术人员以及需要搭建完整离线分析项目的开发者。资源完整提供Python爬虫、Hadoop集群搭建脚本、Hive数仓建表SQL、Spark分析代码以及基于Spring Boot与ECharts的前后端可视化工程可帮助读者从零构建天气数据的采集入库与多维分析展示。包内共757个文件压缩后约7.5MB其中java源文件296个是后端业务与Spark任务核心html、js、vue等前端文件近260个负责图表交互与页面渲染另有xml、yml等配置类文件便于环境部署sql脚本可快速初始化数据库。已有207人学习下载。对希望以完整项目串联大数据组件、理解从数据产生到可视化呈现整个链路的中高级学习者这套代码框架具备直接参考与二次开发价值。1. 这套系统到底在解决什么问题做一个全国天气可视化系统技术栈从 Hadoop 一路排到 ECharts听起来像把所有热门组件叠一起但真正动手时你会发现难的不是哪个组件单独用而是让它们在一个链路里协同起来。数据要落 HDFS清洗和聚合要交给 Spark最后通过 Spring Boot 把结果以接口形式吐给前端ECharts 再画成中国地图上的色块和折线。任何一个环节的版本不匹配、内存配置不合理、数据格式对不上整条链路都会卡住。这个项目在高校课程设计和中小企业数据平台里出现频率很高核心诉求是用分布式存储和计算处理一批规模不小的天气历史数据再通过 Web 界面让用户按省份、城市、时间维度查看气温、降水、AQI 等指标。别把它想成生产级气象平台它的价值在于让你完整走一遍「采集 → 存储 → 计算 → 服务化 → 可视化」的数据工程流程。你可以不跑集群用伪分布式或本地模式就把业务跑通但每一层该做什么、参数怎么调、坑在哪里这篇文章按我会动手做的顺序完整讲清楚。2. Hadoop Spark 的组合逻辑与集群搭建2.1 为什么需要两套分布式组件而不是一个 Spark 全搞定先说结论Spark 自己能读本地文件也能处理小数据但一旦数据量到了 GB 级、需要多节点并行读数据HDFS 的块存储和副本机制就体现出价值。HDFS 负责「存得住、不丢数据」Spark 负责「算得快、算得明白」。Hadoop 生态里真正被这个项目用到的核心是 HDFS 和 YARN。NameNode 管理元数据DataNode 存实际数据块默认副本数 3块大小 128MB。这些参数在单机伪分布式下要调小否则本地磁盘吃不消。Spark 则是一个内存计算框架它不负责存储而是从 HDFS 拉数据、在内存里做转换、把结果写回 HDFS 或直接写数据库。常见的项目误区是只写了 Spark 程序但数据放在本地文件系统这样完全没利用到 Hadoop 的分布式存储能力。我的做法是原始数据文件先上传到 HDFSSpark 通过hdfs://路径读取计算结果再用 DataFrame 的write方法落地。这样整条链路才是通的面试或答辩时也能说清楚每一层的作用。2.2 伪分布式与集群模式的选择这一步选型直接决定你后续踩坑的深度。如果你本机内存小于 8GB建议直接搭 Hadoop 伪分布式模式加 Spark Standalone 模式如果内存有 16GB 以上可以用 3 个虚拟机或容器搭一个真正的小集群压力测试效果更好。伪分布式的典型配置如下core-site.xml指定 NameNode 地址configuration property namefs.defaultFS/name valuehdfs://localhost:9000/value /property property namehadoop.tmp.dir/name value/home/hadoop/data/tmp/value /property /configurationhdfs-site.xml里把副本数改为 1块大小设为 64MB避免 IDEA 里调试时频繁触发网络传输configuration property namedfs.replication/name value1/value /property property namedfs.blocksize/name value67108864/value /property property namedfs.namenode.name.dir/name value/home/hadoop/data/name/value /property property namedfs.datanode.data.dir/name value/home/hadoop/data/data/value /property /configuration这两个配置文件是所有 Hadoop 搭建教程都会提到的但有一个坑经常会碰到hadoop.tmp.dir默认指向/tmp目录Linux 重启后临时文件被清空再次启动start-dfs.sh时 NameNode 会因为找不到元数据而启动失败。所以务必将 name 和 data 目录指向持久化路径。格式化 NameNode 只需要一次。重复格式化会导致 clusterID 不一致DataNode 无法注册表现是jps能看到进程但 Web 界面显示 DataNode 为 0 个节点。遇到这个情况正确做法是删除 data 和 name 目录下所有内容重新hdfs namenode -format再start-dfs.sh。2.3 Spark 与 Hadoop 的版本兼容性版本问题是最容易卡住初学者的环节。Spark 2.x 默认编译时对应的 Hadoop 版本是 2.7 或 3.2如果你用的是 Hadoop 3.3.x下载 Spark 时要选spark-xxx-bin-hadoop3的包。版本不匹配最常见的问题是org.apache.hadoop.fs.FileSystem类找不到或NoSuchMethodError这些错误通常在SparkContext初始化时抛出。启动 Spark 集群前先在spark-env.sh中配置环境变量把自己本机的 Java 路径和 Hadoop 配置路径写明确export JAVA_HOME/usr/local/jdk1.8 export HADOOP_HOME/usr/local/hadoop export SPARK_MASTER_HOSTlocalhost export SPARK_WORKER_MEMORY2g export SPARK_WORKER_CORES2配置完成后启动顺序有讲究先启动 Hadoop再启动 Spark。Hadoop 用start-dfs.sh和start-yarn.shSpark 用$SPARK_HOME/sbin/start-all.sh。如果选择 Spark 跑在 YARN 上则不需要启动 Spark 的 Master 和 Worker直接通过spark-submit --master yarn提交任务即可。伪分布式环境建议把 Spark 跑在 Standalone 模式排除 YARN 资源调度的干扰让精力集中在程序本身。3. Spark 数据清洗与统计分析的核心实现3.1 数据模型与 HDFS 目录规划做全国天气分析数据来源通常是公开气象站点的历史观测记录或爬虫抓取的气象数据。不要纠结所谓官方权威数据源重点是把字段设计好。我的设计是每个文件包含以下字段字段名类型说明provinceString省份名称cityString城市名称dateString日期格式为 yyyy-MM-ddtemp_maxDouble当日最高气温temp_minDouble当日最低气温weatherString天气现象如晴、多云、小雨wind_directionString风向wind_levelInteger风力等级aqiInteger空气质量指数humidityInteger相对湿度数据文件按日期分区存放HDFS 目录结构为/weather/data/2024/01/,/weather/data/2024/02/这样后续查询可以借助分区裁剪减少扫描量。上传数据用一行命令即可hdfs dfs -mkdir -p /weather/data/2024/01 hdfs dfs -put /home/hadoop/weather_2024_01.csv /weather/data/2024/01/注意如果上传后 Spark 读不到数据先检查文件权限执行hdfs dfs -chmod -R 777 /weather大多数权限问题都出现在这里。3.2 用 Spark SQL 完成 ETL 工作数据清洗最常用的手段是 Spark SQL 配合 DataFrame API。建议直接写 Scala 代码因为 Java 与 Spark 结合时闭包序列化问题太多Scala 能避开大量Task not serializable异常。下面是读取 HDFS 上 CSV 并做类型转换的完整示例import org.apache.spark.sql.SparkSession import org.apache.spark.sql.types._ val spark SparkSession.builder() .appName(WeatherETL) .master(spark://localhost:7077) .config(spark.sql.shuffle.partitions, 4) .getOrCreate() val schema StructType(Array( StructField(province, StringType, true), StructField(city, StringType, true), StructField(date, StringType, true), StructField(temp_max, DoubleType, true), StructField(temp_min, DoubleType, true), StructField(weather, StringType, true), StructField(wind_direction, StringType, true), StructField(wind_level, IntegerType, true), StructField(aqi, IntegerType, true), StructField(humidity, IntegerType, true) )) val rawDF spark.read .option(header, true) .schema(schema) .csv(hdfs://localhost:9000/weather/data/2024/*/*.csv) val cleanDF rawDF .filter($temp_max.isNotNull $aqi.isNotNull) .withColumn(year, substring($date, 1, 4)) .withColumn(month, substring($date, 6, 2)) .withColumn(season, when(substring($date, 6, 2).between(03, 05), spring) .when(substring($date, 6, 2).between(06, 08), summer) .otherwise(winter)) cleanDF.write .mode(overwrite) .parquet(hdfs://localhost:9000/weather/clean)这段代码做什么定义 schema 是为了避免 Spark 自动推断类型时把数值列推断成 String后面做MAX、AVG聚合时会报类型错误。过滤空值是为了防止某个站点数据缺失导致整行脏数据进入指标计算。季节字段在后面的分析任务中会用到是写论文和做图表的高频维度。最后落地成 Parquet 格式列式存储压缩率高后续查询速度比直接读 CSV 快将近一倍。需要重点解释的参数是spark.sql.shuffle.partitions。默认值 200 是给大规模集群设计的如果你的数据只有几百万行200 个分区反而会产生大量小文件拖慢 Reduce 阶段的效率。本地环境调到 4 到 10 个分区即可。数据量小时这个参数的调整对性能的影响比调大 Executor 内存还要明显。3.3 多维度指标聚合统计聚合逻辑是这个系统的核心产物后端接口的数据全部来自这些结果。统计目标包括各省月平均气温、各城市年降水量排名、全国极端高温 TOP10、季节性气候变化趋势。用 DataFrame API 的 groupBy 聚合是最直观的写法val monthlyTemp cleanDF .groupBy(province, year, month) .agg( avg(temp_max).alias(avg_temp_max), avg(temp_min).alias(avg_temp_min), max(temp_max).alias(extreme_high), min(temp_min).alias(extreme_low) ) .orderBy(province, year, month) monthlyTemp.show(20, false) val cityAQIRank cleanDF .groupBy(city) .agg( avg(aqi).alias(avg_aqi), count(when($aqi 200, true)).alias(heavy_pollution_days) ) .orderBy(desc(avg_aqi)) .limit(20) cityAQIRank.show(20, false)聚合结果直接写入 MySQL 供 Spring Boot 查询写库的 JDBC 配置需要指定驱动类和连接参数monthlyTemp.write .mode(overwrite) .jdbc(jdbc:mysql://localhost:3306/weather_db?useSSLfalseserverTimezoneAsia/Shanghai, monthly_temp, props)props中写入user和password。注意一个坑如果结果数据集较大直接 write 会产生 200 个并发连接同时写库MySQL 默认 max_connections 很容易被打满。我的做法有两种要么先用repartition(4)合并分区再写要么通过option(batchsize, 1000)控制单批写入量。生产环境里我会配合 Sqoop 或 DataX 做批量导出但在课程设计层面repartition 就够了。3.4 Spark 作业日志分析与内存调参提交任务时不要直接用 IDEA 里的 Run 按钮而是通过 spark-submit 提交这样能看到完整日志并控制资源参数。推荐命令如下spark-submit \ --class com.weather.ETLJob \ --master spark://localhost:7077 \ --executor-memory 2g \ --driver-memory 1g \ --total-executor-cores 2 \ /home/hadoop/weather-analysis.jar如果任务跑着跑着报java.lang.OutOfMemoryError: Java heap space先不要急着加内存去看 Spark Web UI 的 Storage 和 Stages 页面确认是不是数据倾斜。数据倾斜的显著特征是某个 Stage 中一个 Task 的执行时间远超其他 Task通常是 groupBy 的 key 分布不均。这一现象在按省份聚合时很常见原因是北京、上海的数据量远大于西藏、青海。排查时用df.groupBy(province).count().orderBy(desc(count)).show()确认分布再用repartition或加盐salting方式缓解。内存参数设置上Executor 内存分为 execution 和 storage 两部分默认各占一半。对于纯 ETL 任务把spark.memory.fraction调低可以给 RDD 缓存留更多空间对于聚合计算反而应该调高 execution 比例。关键参数归纳如下参数配置项建议值堆内内存spark.executor.memory2g-4g堆外内存spark.executor.memoryOverhead512m缓存比例spark.memory.storageFraction0.5序列化器spark.serializerKryoSerializer动态分配spark.dynamicAllocation.enabledfalse4. Spring Boot 后端架构与数据库设计4.1 四层架构的具体落地方式后端是整个系统的中枢向上对 ECharts 提供 JSON 数据向下从 MySQL 读取 Spark 计算好的聚合结果。采用 Spring Boot 经典四层架构Controller 层接收请求、Service 层处理业务逻辑、Mapper 层负责数据库操作、Model 层定义实体映射。项目目录遵循controller / service / dao / pojo划分方式com.weather ├── controller │ ├── WeatherDataController.java │ ├── CityRankController.java │ └── MapController.java ├── service │ ├── WeatherDataService.java │ └── impl │ └── WeatherDataServiceImpl.java ├── mapper │ ├── WeatherDataMapper.java │ └── xml │ └── WeatherDataMapper.xml └── pojo ├── MonthlyTemp.java └── CityAQI.javaController 只做参数接收和结果封装不写任何 SQL 相关逻辑。以省份月均气温查询为例RestController RequestMapping(/api/weather) public class WeatherDataController { Autowired private WeatherDataService weatherDataService; GetMapping(/province/monthly) public Result getMonthlyTemp(RequestParam String province, RequestParam String year) { ListMonthlyTemp list weatherDataService.getMonthlyTemp(province, year); return Result.success(list); } }Service 层检查参合调用 Mapper核心代码不复杂但体现事务边界Service public class WeatherDataServiceImpl implements WeatherDataService { Autowired private WeatherDataMapper weatherDataMapper; Override public ListMonthlyTemp getMonthlyTemp(String province, String year) { if (province null || province.isEmpty()) { throw new IllegalArgumentException(省份不能为空); } return weatherDataMapper.selectMonthlyTemp(province, year); } }四层架构的价值在这套系统里体现得很实际如果后续把数据源从 MySQL 换成 ClickHouse只需要改 Mapper 层和 SQLController 与 Service 层完全不动。对于课程设计和答辩来说这也是一个可以主动展示的亮点。4.2 MySQL 表设计要点Spark 计算结果落库的表设计要面向查询而不是面向存储规范化。天气数据分析场景以读为主写入是一次性的所以不要刻意做三范式反而要做一定的冗余。两张核心表结构如下CREATE TABLE monthly_temp ( id INT PRIMARY KEY AUTO_INCREMENT, province VARCHAR(50) NOT NULL, year SMALLINT NOT NULL, month TINYINT NOT NULL, avg_temp_max DECIMAL(5,2), avg_temp_min DECIMAL(5,2), extreme_high DECIMAL(5,2), extreme_low DECIMAL(5,2), UNIQUE KEY uk_province_month (province, year, month) ); CREATE TABLE city_aqi ( id INT PRIMARY KEY AUTO_INCREMENT, city VARCHAR(50) NOT NULL, avg_aqi INT, heavy_pollution_days INT, rank_no INT, stat_year SMALLINT );city_aqi表中的rank_no是 Spark 计算完成后直接写入的排名结果。按城市查排行榜时直接用SELECT city, avg_aqi, rank_no FROM city_aqi WHERE stat_year 2024 ORDER BY rank_no ASC;如果单独建一张城市信息表再关联省份表查询时要多一次 JOIN对于前端自动轮播展示来说完全没有必要。垂直分表在这个数据量级是过度设计水平分表更不需要。MySQL 单表千万行内用普通索引完全扛得住。4.3 Spring Boot Actuator 暴露端口的风险开发调试时很多同学习惯把 Actuator 暴露到公网这样才能在浏览器看到/actuator/env、/actuator/heapdump。但这在真实部署时会让攻击者拿到环境变量、数据库密码、堆内存快照中的敏感信息。正确做法是在application.yml中将 Actuator 限定到内网地址并只暴露最少端点management: endpoints: web: exposure: include: health,info server: address: 127.0.0.1 port: 8081这样 Spring Boot Actuator 未授权访问问题在源头上被堵住。如果确实需要远程查看指标走带鉴权的监控系统比如 Prometheus 拉取指标时通过 basic auth 保护。4.4 前后端联调时的跨域处理Spring Boot 后端跑在 8080 端口前端页面如果用file://协议打开或跑在 8081必然遇到跨域问题。处理方案是添加一个 CORS 配置类Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOrigins(http://localhost:8081) .allowedMethods(GET, POST, PUT, DELETE) .maxAge(3600); } }注意allowedOrigins不要写成*因为和allowCredentials(true)一起用时浏览器会拒绝请求。如果你前端用了 Axios 且开启了withCredentials这里的 origin 必须明确指定。5. ECharts 全国地图与可视化组件应用5.1 ECharts 中国地图的版本兼容与注册ECharts 从 4.9 版本之后不再默认内置中国地图5.x 版本需要手动注册 GeoJSON 数据。很多教程里写echarts.registerMap(china, chinaJson)但没说chinaJson从哪来。我的做法是下载中国各省份 GeoJSON 文件放进项目静态资源目录地图几何数据可控且加载稳定。具体的 Vue 或原生 HTML 引入方式import * as echarts from echarts; import china from /assets/china.geo.json; echarts.registerMap(china, china);注册完成后绘制全国气温分布图的核心配置const chart echarts.init(document.getElementById(weatherMap)); chart.setOption({ tooltip: { trigger: item, formatter: function(params) { return params.name br/平均气温 params.value ℃; } }, visualMap: { min: -20, max: 35, left: left, top: bottom, text: [高温, 低温], inRange: { color: [#313695, #4575b4, #74add1, #fee090, #f46d43, #a50026] } }, series: [{ type: map, map: china, roam: true, label: { show: false }, data: provinceTempData }] });这个配置涉及的参数里visualMap是颜色映射的核心min和max要根据数据实际范围设置。地图组件显示为长方形或不显示常见原因是容器 div 没有高度比如.chart-box只设置了宽度没设置高度。另一个坑是 GeoJSON 数据量过大导致地图渲染缓慢可以在初始化时设置animation: false去掉过渡动画首屏加载会明显快起来。如果发现地图上省份名称显示为拼音而不是中文检查 GeoJSON 文件中properties.name字段是否为中文。部分第三方 GeoJSON 使用拼音 name 或缺失该字段需要在registerMap前对数据进行 format 处理。5.2 折线图与饼图的联动设计地图展示全国概览点击某个省份后需要下钻到该省份的月度气温走势。我用折线图展示月均气温变化饼图展示不同天气现象占比。点击省份事件绑定myChart.on(click, function(params) { if (params.componentType series params.seriesType map) { const province params.name; fetch(/api/weather/province/monthly?province${province}year2024) .then(res res.json()) .then(data { renderLineChart(data); renderPieChart(province); }); } });折线图配置中X 轴用月份Y 轴用气温两个数据系列分别对应最高温和最低温。需要注意yAxis的scale: true会把数据基线自适应折线图不要设成startFromZero: true因为 0 度在温度语义下没有意义option { xAxis: { type: category, data: [1月, 2月, 3月, 4月, 5月, 6月, 7月, 8月, 9月, 10月, 11月, 12月] }, yAxis: { type: value, scale: true }, series: [ { name: 最高气温, type: line, smooth: true, data: maxTemps, areaStyle: { opacity: 0.2 } }, { name: 最低气温, type: line, smooth: true, data: minTemps } ] };饼图的坑主要在数据格式。ECharts 饼图要求data是{name: , value: }的对象数组而后端返回的可能是两个平行数组。这需要前端做一次转换const pieData weatherNames.map((name, index) ({ name: name, value: weatherCounts[index] }));5.3 从 Spark 计算结果到图表数据的类型对接前后端联调遇到最多的怪问题是图表显示NaN或undefined。排查经验是检查 Java 后端返回的字段是 BigDecimal 或 LongJSON 序列化后变成了avg_temp_max: 23.50字符串或 0.1 精度丢失。解决办法是在 Spring Boot 的application.yml中做全局 Jackson 配置spring: jackson: generator: write_numbers_as_strings: false serialization: write-bigdecimal-as-plain: true同时保证前端解析时使用parseFloat而不是parseInt处理气温字段。AQI 是整数所以用parseInt这两者的类型匹配是代码审查时要留意的地方。另一个隐蔽的问题是 MySQL 中 DECIMAL 字段在 JDBC 驱动中返回 BigDecimal如果 POJO 中定义成了 Double低版本驱动会直接序列化失败。POJO 中统一使用 BigDecimal 并在 JSON 返回时给前端Number类型最靠谱。6. 数据一致性校验与调度自动化技巧6.1 Spark 结果与源数据的交叉验证写完整个链路后第一件要做的事不是调图表样式而是验证数据准确性。大量项目在跨框架传递后出现数据偏差原因是 HDFS 数据读取过滤条件、Spark 聚合逻辑、SQL 查询条件三处对「月份」字段的格式处理不一致。我的验证方法是设计一组对比查询先在 Spark 里跑一次全量聚合保存结果再直接对原始数据用 Spark SQL 的groupBy算一个总数val originCount spark.sql(SELECT COUNT(*) FROM cleanData WHERE province 广东 AND year 2024) val aggregatedCount monthlyTemp.filter($province 广东 $year 2024) .select(sum(cnt))如果两者不一致最常见原因是 CSV 中某行的省份字段带空格或中文引号导致 groupBy 的 key 不等于预期的值。清洗阶段用trim($province)再做一次标准化这类问题基本消失。MySQL 侧的校验是对比业务表与统计表的条数SELECT COUNT(*) FROM monthly_temp WHERE province 广东 AND year 2024; SELECT COUNT(DISTINCT CONCAT(year, month)) FROM monthly_temp WHERE province 广东;第一条 SQL 是验证聚合行数是否符合预期第二条是验证没有重复写入。Spark 写 MySQL 时使用mode(overwrite)会造成旧数据被清空再写入如果写入阶段失败表会处于空状态。实际跑批中我会改为先写临时表成功后通过两条 SQL 原子性切换表名。6.2 用 Shell 脚本串联整个调度链路人工手动执行 HDFS 上传、Spark 作业、打包部署 Spring Boot每次重复会消耗大量时间。我一般会写一个调度脚本把整个流程串成一条命令#!/bin/bash # 天气数据每日更新脚本 HDFS_INPUT/weather/data HDFS_OUTPUT/weather/clean # 1. 上传当日新增数据 hdfs dfs -put /home/hadoop/collect/weather_$(date %Y%m%d).csv $HDFS_INPUT/$(date %Y/%m)/ # 2. 启动 Spark ETL 作业 spark-submit \ --class com.weather.ETLJob \ --master spark://localhost:7077 \ --executor-memory 2g \ /home/hadoop/weather-analysis.jar # 3. 重启 Spring Boot 应用 PID$(ps -ef | grep weather-server.jar | grep -v grep | awk {print $2}) if [ -n $PID ]; then kill -9 $PID fi nohup java -jar /home/hadoop/weather-server.jar /home/hadoop/logs/server.log 21 echo 数据更新完成: $(date)脚本的关键点在第 3 步。由于 Spring Boot 服务在启动时会把查询结果缓存到内存Spark 写完新数据后后端需要重启才能加载最新数据。如果加上 Spring Cache 注解还可以通过 Redis 缓存键过期来避免重启服务。脚本用kill -9强杀进程是因为 Spring Boot 的优雅停机需要额外配置spring.lifecycle.timeout-per-shutdown-phase没配时kill命令会卡住不推荐但本地环境用起来最省事。6.3 性能瓶颈的快速定位方法整套系统跑完一遍后如果觉得响应速度不理想按以下顺序排查能快速定位瓶颈。查看 Spark Web UI 的 Job 耗时。主页上会清晰列出每个 Stage 的耗时、Shuffle 读写的字节量。如果某一 Stage 耗时占比超过 80%基本就是数据倾斜或计算逻辑庞大。Shuffle 写入量大时优先加spark.sql.autoBroadcastJoinThreshold或改用 Broadcast 变量而不是盲目加 Executor。查看 MySQL 慢查询日志。Spring Boot 中打开 SQL 输出logging: level: com.weather.mapper: debug然后在控制台观察每条 SQL 的执行时间。如果带ORDER BY的排行查询走全表扫描检查 SQL 中是否有索引列。city_aqi表上给rank_no加一个索引就能让排行查询从几百毫秒降到几十毫秒。查看前端 Network 面板。ECharts 地图一次请求数据量不建议超过 1MB。后端返回的数据过重时最常见原因是查询了不必要的字段比如前端只需要城市名和 AQISQL 却 SELECT 了全部列。在 Mapper 层明确列名而不是用SELECT *这是最便宜的性能优化。ECharts 地图的 GeoJSON 文件也值得二次压缩。用mapshaper工具降低坐标精度文件体积可以从 2MB 降到 500KB加载和缩放流畅度会有明显改善尤其在地图拖拽和缩放的交互场景下。本文还有配套的精品资源点击获取
返回列表