GeoJSONcn
GeoJSONcn / 技术专栏 / 省市区县边界 GeoJSON 秒…

省市区县边界 GeoJSON 秒出预览缩略图:Canvas 离屏渲染 + 图块缓存实战

发布于 2026-08-26 · GeoJSONcn 技术团队 · 阅读约 10 分钟

> 写稿用正文(下)。

做全国省市区县geojson数据下载整理的时候,光有数据页还不够——下载列表每个行政区要一张小小的预览图,不然访客根本分不清这个县的边界长什么样。一开始我图省事,直接用后端 GDAL 一张张 render,结果区县近六千个、加上乡镇上万个,边缘一刷 CPU 直接烤到冒烟,生成一次要几小时。后来我把「出图」这件事从请求期整个挪到了生成期,用 Node + Canvas 离屏渲染一次性烘焙到位,配图块缓存,成本直接压到可以忽略。

这篇不堆理论,讲我怎么用 Node + Canvas 离线批量渲染缩略图,为什么比后端画图更省心,以及怎么把它接进静态站的分发链路。懂这套,你也能把任意一份行政区划 GeoJSON下载任务变成一串规整的 PNG/WebP 预览图,前端零计算、边缘零渲染。

缩略图生成期 vs 请求期:为什么预览图要「烤」出来

GeoJSON 出缩略图有两条路。请求期:前端拿到坐标现画,灵活但对低频访问的下载页是浪费;生成期:构建时统一渲染成 PNG 图块,边缘只做分发。我们这种"静态站点 + 边缘加速"的架构,天生应该选生成期。

维度请求期渲染生成期渲染(本项目)
触发时机每次请求现算构建时一次性烘焙
边缘 CPU每次都要算,成本高只取文件,零计算
一致性版本不同可能串色全部同源、可校验
缓存Cache-Control 难设文件名带版本直接长缓存
离线预览依赖 CDN 撑图块本地化,内网也能看

对「全国村级geojson数据下载」这类低频、量大的页面,生成期烘焙缩略图的好处是实实在在的:文件数可控、更新原子化、边缘节点纯分发不耗算力。反过来,如果你只是想要一份能直接分析的全国省市区县区划边界数据下载,缩略图只是门面,真正的体积大头还是得靠坐标精度瘦身 + 压缩那套去扛。

Node 里用 Canvas 离屏绘制省市区县边界缩略图

Node 端画 GeoJSON 最顺手的组合是 @napi-rs/canvas(原生 Canvas 绑定)+ 一套简单的墨卡托投影到像素坐标。核心逻辑就三步:读 GeoJSON → 算 bbox → 把经纬度线性映射到画布像素,再填充多边形。代码如下:

import fs from 'node:fs'
import { createCanvas } from '@napi-rs/canvas'

// 一线墨卡托投影:经纬度 → 画布像素
const proj = (lon, lat) => [ 
  (lon + 180) / 360 * CANVAS_W,
  (1 - Math.log(Math.tan(Math.PI/4 + lat*Math.PI/360)) / Math.PI) / 2 * CANVAS_H,
]

function renderThumb(geojson, { w = 400, h = 400, out }) {
  const canvas = createCanvas(w, h)
  const ctx = canvas.getContext('2d')
  // 先按数据 bbox 校准缩放,避免边界跑出画面
  const b = geojson.bbox || computeBbox(geojson)
  const s = Math.min(w / (b[2]-b[0]), h / (b[3]-b[1]))
  ctx.fillStyle = '#eef3fa'
  ctx.fillRect(0, 0, w, h)
  for (const f of geojson.features) {
    ctx.beginPath()
    walkPolygon(f.geometry, proj, s, b)
    ctx.closePath()
    ctx.fillStyle = '#86b7ef'   // 颜色从 properties 映射,可区分省市区县级别
    ctx.fill()
    ctx.lineWidth = 1; ctx.strokeStyle = '#fff'; ctx.stroke()
  }
  fs.writeFileSync(out, canvas.toBuffer('image/png'))
}

三个坑我踩过,先跟你说:

另外提醒一句:如果你渲染的是省市区县区划边界数据下载用的大分省文件,几何必须 make_valid 兜底,存在自相交的环画出来常常一片黑糊,QGIS 里看是 geometry、缩略图就露馅了。

WebP 输出 + 图块化:让缩略图在静态站里跑得爽

PNG 太占地方,缩略图我会用 canvas.toBuffer('image/webp') 输出,同样的画质体积能小 60% 以上。再把每个省、市、区县按 adcode 前缀先分块、再渲染成独立文件,文件名带版本号和 SHA-256 后 8 位当锚点。这样边缘节点按文件名长缓存,改数据只换文件、不串旧图,之前的 SHA-256 校验和方案直接复用。

访问首页/详情页时,边缘 Worker 只做「按 URL 取文件 + 回正确 Cache-Control」两件事,图块内容全是生成期烘焙好的,单一真相源在生成脚本、不在边缘。预览免费、导出按次计费(¥1.99/次)的边界也清清楚楚:缩略图是公开预览,完整 GeoJSON/PNG/SVG/EMF 下载才走付费。顺带一提,这套图块化思路和我们站点给全国村级geojson数据下载做预览是同一套路——低频、量大,就值得为它多花一次构建时间,换取边缘零渲染的长期省心。

小结

让省市区县边界 GeoJSON 秒出预览图,诀窍是别在请求期现画,而是生成期用 Canvas 离屏烘焙成 WebP 图块,文件名带版本 + 校验和,边缘只做分发。前端拿到的是轻量图块,桌面端/内网离线也能看。想让全国省市级geojson数据下载和村级geojson数据下载都有清晰预览,这套「烘焙 + 图块 + 静态分发」的组合基本可以照抄。

> 本文属于 GeoJSONcn 技术专栏,覆盖全国省市区县geojson数据下载与预览。站点为静态生成 + 边缘加速架构,GeoJSON/PNG/SVG/EMF 预览免费,导出按次计费 ¥1.99/次。