04 地图图层封装:像管理 Photoshop 图层一样管理地图元素

04 地图图层封装:像管理 Photoshop 图层一样管理地图元素

我们要做什么

一个地图页面上可能同时有:底图、禁飞区(红色区域)、航线(橙色线)、无人机位置(动画图标)、行政边界......几十种元素。

如果每加一个元素都随手写一堆代码,很快就会变成灾难:

  • 同一个"禁飞区"在 10 个页面有 10 种写法
  • 页面关掉了,某个动画定时器还在后台跑(内存泄漏)
  • 想加一种新元素,要改一堆老代码

这一篇我们做一个图层管理器 :像 Photoshop 的图层面板一样,每种元素是一个"图层",统一添加、统一删除、统一销毁,而且图层长什么样由后端配置决定,前端不用改代码。

先懂几个概念

概念 1:图层就是"会自己画自己"的对象

每个图层只需提供两个能力:

  • render() ------ 把自己画到画布上
  • destroy() ------ 把自己用的资源(定时器、图片、监听器)清理干净

管理器不关心你是红方块还是蓝圆圈,只按顺序调用 render,删除时调用 destroy。

概念 2:注册表------类型和做法分离

"网格图层怎么画"和"什么时候加一个网格图层"是两件事。把"怎么画"提前登记到一个表里(注册),用的时候报名字就行:

css 复制代码
注册:  'grid' → 画网格的方法
使用:  addLayer({ type: 'grid' })  → 管理器查表,找到画法,创建实例

好处:新增一种图层 = 新注册一项,老代码一行不改(开闭原则)。

概念 3:配置驱动

"页面上要显示哪些图层"不硬编码在前端,而是后端接口下发一份 JSON。好处:不同客户看到的地图不一样,改数据库就行,不用发版。

整体流程图

bash 复制代码
【Node 后端】
GET /api/layers  → 返回图层配置清单:
[
  { id:'grid',  type:'grid',    zIndex:1, options:{...} },
  { id:'zone',  type:'polygon', zIndex:2, options:{...} },
  { id:'route', type:'flowline', zIndex:3, options:{...} }
]

【前端】
1. 创建图层管理器,注册三种类型的画法
2. 拉取配置 → 逐条 addLayer
3. 管理器内部:查注册表 → 创建实例 → 按 zIndex 排序 → 逐个 render
4. 页面关闭 → 管理器 destroy → 每个图层的 destroy 被调用
   (动画定时器被清掉,没有泄漏)

动手做

第 1 步:Node 后端(只负责下发配置)

新建 demos/04-layer,npm init -y && npm i express cors,新建 server.js:

js 复制代码
const express = require('express');
const cors = require('cors');
const app = express();
app.use(cors());

// 图层配置:真实项目里这存在数据库,不同客户不同配置
app.get('/api/layers', (req, res) => {
  res.json([
    { id: 'bg-grid', type: 'grid', zIndex: 1, options: { step: 50 } },
    { id: 'no-fly-zone', type: 'polygon', zIndex: 2, options: {
        points: [[100, 100], [300, 80], [350, 250], [150, 300]],
        color: 'rgba(255,0,0,0.3)' } },
    { id: 'airline', type: 'flowline', zIndex: 3, options: {
        points: [[50, 400], [200, 350], [400, 380], [600, 300]],
        color: '#ff8800' } },
  ]);
});

app.listen(3000, () => console.log('http://localhost:3000'));

第 2 步:前端图层管理器

新建 LayerManager.js(本课核心,仔细看注释):

js 复制代码
// 图层管理器:统一添加/删除/排序/销毁
export class LayerManager {
  constructor(canvas) {
    this.ctx = canvas.getContext('2d');
    this.canvas = canvas;
    this.registry = new Map(); // 类型名 → 创建方法(注册表)
    this.layers = new Map();   // id → 图层实例
  }

  // 注册一种图层的画法。factory 必须返回 { render, destroy }
  registerType(type, factory) {
    this.registry.set(type, factory);
  }

  // 添加图层(同一个 id 重复添加会先删旧再加新,防重复)
  addLayer(cfg) {
    if (this.layers.has(cfg.id)) this.removeLayer(cfg.id);
    const factory = this.registry.get(cfg.type);
    if (!factory) throw new Error('没注册过的图层类型: ' + cfg.type);
    const instance = factory(this.ctx, cfg.options || {});
    this.layers.set(cfg.id, { ...cfg, instance });
    this.render();
  }

  // 删除图层:先调 destroy 清理资源,再移除引用
  removeLayer(id) {
    const layer = this.layers.get(id);
    if (!layer) return;
    layer.instance.destroy?.();  // ?. = 允许某些图层没有东西要清
    this.layers.delete(id);
    this.render();
  }

  // 重绘整个画布:按 zIndex 从低到高画(低的在底下)
  render() {
    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
    [...this.layers.values()]
      .sort((a, b) => (a.zIndex || 0) - (b.zIndex || 0))
      .forEach(l => l.instance.render());
  }

  // 页面关闭时整体销毁
  destroy() {
    [...this.layers.keys()].forEach(id => this.removeLayer(id));
  }
}

第 3 步:注册画法 + 使用

新建 index.html:

html 复制代码
<!DOCTYPE html>
<html>
<body>
  <canvas id="map" width="800" height="500" style="border:1px solid #ccc"></canvas>
  <button id="remove">移除航线图层</button>

  <script type="module">
    import { LayerManager } from './LayerManager.js';
    const manager = new LayerManager(document.getElementById('map'));

    // 注册画法 1:网格(静态,无需清理资源)
    manager.registerType('grid', (ctx, opt) => ({
      render() {
        ctx.strokeStyle = '#eee';
        for (let x = 0; x < 800; x += opt.step) {
          ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, 500); ctx.stroke();
        }
        for (let y = 0; y < 500; y += opt.step) {
          ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(800, y); ctx.stroke();
        }
      },
      destroy() {}, // 没有资源要清
    }));

    // 注册画法 2:多边形区域
    manager.registerType('polygon', (ctx, opt) => ({
      render() {
        ctx.fillStyle = opt.color;
        ctx.beginPath();
        opt.points.forEach((p, i) => i ? ctx.lineTo(...p) : ctx.moveTo(...p));
        ctx.closePath(); ctx.fill();
      },
      destroy() {},
    }));

    // 注册画法 3:流动虚线(有动画定时器,destroy 必须清理!)
    manager.registerType('flowline', (ctx, opt) => {
      let offset = 0;
      const timer = setInterval(() => { offset -= 2; manager.render(); }, 50);
      return {
        render() {
          ctx.strokeStyle = opt.color; ctx.lineWidth = 3;
          ctx.setLineDash([12, 8]); ctx.lineDashOffset = offset;
          ctx.beginPath();
          opt.points.forEach((p, i) => i ? ctx.lineTo(...p) : ctx.moveTo(...p));
          ctx.stroke();
          ctx.setLineDash([]);
        },
        destroy() { clearInterval(timer); }, // 关键!不清就会泄漏
      };
    });

    // 图层清单完全由后端决定
    const layers = await fetch('http://localhost:3000/api/layers').then(r => r.json());
    layers.forEach(cfg => manager.addLayer(cfg));

    document.getElementById('remove').onclick = () => manager.removeLayer('airline');
    window.addEventListener('beforeunload', () => manager.destroy());
  </script>
</body>
</html>

用了 ES Module,直接双击打开会被浏览器拦。在目录里跑 npx serve . 然后访问提示的地址(同时 server.js 也要在跑)。

跑起来看看

  1. 灰色网格 + 红色半透明区域 + 一条流动的橙色虚线
  2. 点"移除航线图层"→ 虚线消失,动画定时器也被停掉了
  3. 验证泄漏:把 flowline 里的 destroy 改成空函数,重复"移除→刷新页面"几次,打开 Chrome DevTools 的 Performance 面板能看到定时器越积越多------体会 destroy 的意义

关键点回顾

知识点 一句话
图层抽象 每个图层只要会 render 和 destroy,管理器不关心你画什么
注册表 类型和做法分离,新增图层类型不改老代码
配置驱动 显示什么图层由后端 JSON 决定,前端零改动支持不同客户
资源治理 定时器/图片/监听器必须在 destroy 里清理,否则页面切多了浏览器会崩
zIndex 排序 重绘时从低到高画,实现图层上下叠放

想一想

  1. 如果业务要求"暂时隐藏航线但稍后还要显示",是 removeLayer 好还是加个 visible 标记好?为什么?(提示:创建/销毁有成本)
  2. addLayer 为什么要做"同 id 先删后加"?如果不管,业务方重复添加会发生什么?
相关推荐
去伪存真1 小时前
构建你的第一个 DevOps AI Agent:自动化捕获、分析并修复 CI 故障
前端·agent
niucloud-admin1 小时前
JAVA V6 多商户商城 开发文档——手机端前端
java·开发语言·前端
skywalk81632 小时前
deepseekharness在对话中,有四种模式,分别是极简、PTC、创造和标准模式,每种模式下挂载的插件、skill等数量不一样
前端·人工智能·实践
愛芳芳2 小时前
element-plus+vite+vue3+pinpa+mock+typeScript搭建经典的电商管理后台
前端·javascript·typescript
liangshanbo12152 小时前
面试题:常见的前端构建工具有哪些?分别适用于什么场景?
前端
linux_cfan2 小时前
videojs v10 源代码系列解读:30 · `SerialRunner` 与 `ConcurrentRunner`
前端
liangshanbo12152 小时前
面试题:页面需要发起大量 Ajax / HTTP 请求时,如何优化请求性能?
前端·http·ajax
lyz2468593 小时前
vue3页面因参数未声明而报错的常犯错误
前端·javascript·vue.js
摸爬滚打的小李3 小时前
流程图同步AI生成
前端