OpenSpec + Vue3 + Mapbox GL + Node.js + GeoServer:完整 WebGIS 项目实战

从规范驱动开发到全栈地图平台落地,带你构建一个具备地图展示、图层管理、空间查询和数据管理能力的 WebGIS 应用。

一、前言:AI 时代,如何更高效地开发 WebGIS?

随着 AI 编程工具的快速发展,开发一个 WebGIS 应用的门槛正在降低。过去需要开发者手动编写大量地图组件、后端接口和数据处理逻辑,现在可以借助 AI 编程助手完成不少重复性工作。

但问题也随之而来。

当项目从简单的地图展示扩展到图层管理、空间查询、数据导入和服务发布时,仅仅依靠一句提示词让 AI 生成代码,往往很难保证整个系统的架构一致性。前端接口可能与后端实现不匹配,地图服务和空间数据库的职责可能混淆,新增功能还可能破坏已有模块。

因此,真正值得探索的,不是如何让 AI 一次性生成更多代码,而是如何建立一套规范清晰、职责明确、可持续迭代的开发流程。

本文将围绕以下技术栈,构建一个完整 WebGIS 平台:

  • OpenSpec:规范驱动开发,管理需求、设计、任务与验收。

  • Vue3 + TypeScript:构建前端应用与业务组件。

  • Mapbox GL JS:实现地图渲染、交互和图层可视化。

  • Node.js + Express:提供业务 API、参数校验和数据管理。

  • PostgreSQL + PostGIS:存储空间数据并执行空间查询。

  • GeoServer:发布标准化地图服务。

  • Docker:提供可复现的开发与部署环境。

最终,我们将搭建一个名为 GeoInsight WebGIS 的空间数据工作台,覆盖从地图展示到空间查询的核心业务流程,并通过 OpenSpec 将开发过程组织成可检查、可测试、可持续演进的工程项目。

二、项目总体设计:我们要构建什么?

2.1 核心功能

本项目不是一个简单的地图展示页面,而是一套具备基础业务能力的 WebGIS 平台。

功能一:地图展示

支持基础底图加载、地图缩放与平移、比例尺和导航控件,以及行政区划、道路、兴趣点等业务图层的叠加展示。

功能二:图层管理

支持图层动态添加与移除、显示与隐藏、透明度控制、图层顺序调整和图层信息查看。

功能三:空间查询

支持地图点击识别、属性条件筛选、矩形范围查询,以及基于 PostGIS 的空间关系判断。

功能四:空间数据管理

支持数据集列表、属性表查看、GeoJSON 数据上传与校验,并为后续扩展 Shapefile、GeoPackage 等数据格式预留接口。

功能五:工程化管理

利用 OpenSpec 组织需求与任务,通过统一的 API 契约、错误处理、自动化测试和容器化配置,降低系统维护成本。

2.2 技术架构

整个系统可以划分为四个层次。

这套架构中,每种技术都有明确的职责。

Mapbox GL JS 负责浏览器中的地图显示和交互;GeoServer 负责将空间数据发布为标准地图服务;Node.js 负责业务逻辑和接口访问控制;PostGIS 负责空间数据存储与空间计算。

OpenSpec 则位于开发流程层面,帮助开发者和 AI 编程助手明确需求、设计和验收标准。

需要强调的是,OpenSpec 并不是地图服务组件,也不参与地图渲染。它的作用是让整个项目的开发过程更加有序。

三、为什么选择 OpenSpec?从提示词开发到规范驱动开发

3.1 传统 AI 编程的局限

假设我们直接向 AI 编程助手提出需求:

"帮我开发一个 Vue3 + Mapbox GL 的 WebGIS 平台,支持图层管理、空间查询和数据上传。"

AI 很可能快速生成一批代码,但这些代码未必构成一个完整、可靠的系统。

例如:

  • 前端调用的接口在后端不存在。

  • 地图图层的 ID 与业务数据集 ID 混用。

  • 空间查询没有统一的坐标参考系约定。

  • 上传接口没有文件大小和几何有效性检查。

  • 修改某个组件时意外破坏其他功能。

  • 项目缺少可验证的验收标准。

这说明,AI 生成代码的速度并不等于项目交付的效率。

3.2 OpenSpec 的核心思路

OpenSpec 通过规范驱动开发,将开发过程组织为一系列可追踪的变更。

一个典型的工作流程包括:

  1. 分析需求,明确业务目标。

  2. 创建变更提案,描述本次要实现的能力。

  3. 编写规格,明确系统应当表现出的行为。

  4. 设计技术方案,确定模块、数据和接口。

  5. 拆分任务,形成可执行的实现清单。

  6. 根据任务实现代码并运行测试。

  7. 同步规格、检查结果并归档变更。

这种方式尤其适合 WebGIS 项目,因为地图应用通常涉及多个技术层:浏览器、业务 API、地图服务和空间数据库。只有把接口契约与职责边界提前明确,才能降低不同模块之间的集成风险。

3.3 初始化 OpenSpec

首先创建项目目录:

复制代码
mkdir geoinsight-webgis
cd geoinsight-webgis

git init

按照 OpenSpec 官方安装说明安装 CLI,然后执行:

复制代码
openspec init

根据提示选择正在使用的 AI 编程工具,完成项目初始化。

官方文档:

https://openspec.dev/docs/quickstart

初始化完成后,应先让 AI 分析项目需求,而不是立即生成所有业务代码。

可以使用下面这段提示词:

复制代码
你是一名资深 WebGIS 架构师,熟悉 Vue3、
TypeScript、Mapbox GL JS、Node.js、PostGIS
和 GeoServer。

请为 GeoInsight WebGIS 设计开发方案。

项目功能:
1\. 地图展示
2\. 图层管理
3\. 空间查询
4\. 属性表查看
5\. GeoJSON 数据导入与管理

技术约束:
- 前端使用 Vue3 + TypeScript
- 地图引擎使用 Mapbox GL JS
- 后端使用 Node.js + Express
- 空间数据库使用 PostgreSQL + PostGIS
- 地图服务使用 GeoServer
- API 必须进行参数校验
- SQL 查询必须使用参数化方式
- 不允许向浏览器暴露数据库凭据

请先分析需求、数据模型、接口契约、
技术风险和验收标准。

暂时不要编写业务代码。

通过这种方式,先让 AI 形成对项目的整体理解,再进入具体模块的开发。

3.4 按功能拆分变更

对于本项目,建议将开发拆分成四个核心变更:

| 变更 | 主要内容 |

| add-map-viewer | 地图初始化、控件、WMS 图层加载 |

| add-layer-management | 图层显隐、透明度、图层顺序 |

| add-spatial-query | 属性筛选、范围查询、结果高亮 |

| add-data-management | 数据集管理、属性表、GeoJSON 导入 |

在支持对应命令的 OpenSpec 工作流中,可以使用类似下面的命令创建变更:

复制代码
/opsx:propose add-map-viewer

审查提案和任务清单后,再执行相应的实现工作流,例如:

复制代码
/opsx:apply

具体命令以当前安装的 OpenSpec 版本和所选工作流为准。

关键原则是:先让规格明确,再让代码实现;先完成一个模块,再进入下一个模块。

四、搭建 Vue3 + Mapbox GL 前端

4.1 创建前端工程

使用 Vite 创建 Vue3 + TypeScript 项目:

复制代码
npm create vite@latest frontend -- --template vue-ts

cd frontend
npm install

npm install mapbox-gl axios pinia
npm install -D @types/geojson

项目结构如下:

复制代码
frontend/
├── src/
│   ├── api/
│   ├── components/
│   │   ├── MapView.vue
│   │   ├── LayerPanel.vue
│   │   └── AttributeTable.vue
│   ├── composables/
│   │   └── useMap.ts
│   ├── stores/
│   ├── types/
│   ├── App.vue
│   └── main.ts
├── .env.example
└── package.json

我们将地图实例管理、图层控制、业务查询和数据展示分别放到独立模块中,避免把所有逻辑集中在一个 Vue 组件内。

4.2 配置环境变量

在 frontend/.env.example 中定义:

复制代码
VITE_MAPBOX_TOKEN=your_mapbox_public_token
VITE_MAPBOX_STYLE=mapbox://styles/mapbox/streets-v12
VITE_GEOSERVER_WMS_URL=/geoserver/ows
VITE_API_BASE_URL=/api

复制为本地 .env.local,并填入实际配置。

需要注意:VITE_ 前缀的变量会暴露给浏览器。因此只能放置适合客户端使用的配置,例如公开地图令牌,不能放置数据库密码或 GeoServer 管理员凭据。

4.3 实现地图组件

创建 src/components/MapView.vue:

复制代码
<script setup lang="ts">
import { onMounted, onBeforeUnmount, ref } from 'vue'
import mapboxgl from 'mapbox-gl'
import 'mapbox-gl/dist/mapbox-gl.css'

const container = ref<HTMLDivElement | null>(null)

let map: mapboxgl.Map | undefined

const token = import.meta.env.VITE_MAPBOX_TOKEN
const style = import.meta.env.VITE_MAPBOX_STYLE

onMounted(() => {
  if (!container.value) return

  if (!token) {
    console.error('请配置 VITE_MAPBOX_TOKEN')
    return
  }

  mapboxgl.accessToken = token

  map = new mapboxgl.Map({
    container: container.value,
    style,
    center: [121.4737, 31.2304],
    zoom: 10,
  })

  map.addControl(
    new mapboxgl.NavigationControl(),
    'top-right',
  )

  map.addControl(
    new mapboxgl.ScaleControl(),
    'bottom-left',
  )

  map.on('load', () => {
    console.info('地图初始化完成')
  })

  map.on('error', event => {
    console.error('地图运行异常', event.error)
  })
})

onBeforeUnmount(() => {
  map?.remove()
  map = undefined
})
</script>

<template>
  <div ref="container" class="map-container"></div>
</template>

<style scoped>
.map-container {
  width: 100%;
  height: 100%;
  min-height: 500px;
}
</style>

这里有三个重要细节。

第一,地图初始化放在 onMounted 中,确保 DOM 容器已经存在。

第二,在组件卸载时调用 map.remove(),避免重复进入页面时残留地图资源。

第三,地图实例由普通变量保存,而不是直接放进深度响应式对象中,避免不必要的响应式代理。

4.4 创建工作台布局

在 App.vue 中引入地图:

复制代码
<script setup lang="ts">
import MapView from './components/MapView.vue'
</script>

<template>
  <main class="workspace">
    <header class="toolbar">
      <strong>GeoInsight WebGIS</strong>
      <span>空间数据工作台</span>
    </header>

    <section class="map-area">
      <MapView />
    </section>
  </main>
</template>

<style>
* {
  box-sizing: border-box;
}

html,
body,
#app {
  margin: 0;
  min-height: 100%;
}

.workspace {
  height: 100vh;
  display: flex;
  flex-direction: column;
}

.toolbar {
  height: 56px;
  padding: 0 20px;
  display: flex;
  align-items: center;
  gap: 20px;
}

.map-area {
  flex: 1;
  min-height: 0;
}
</style>

运行:

复制代码
npm run dev

如果令牌和地图样式配置正确,浏览器中就能显示基础地图。

至此,我们已经完成前端地图引擎的基本接入。

五、接入 GeoServer:展示真实 GIS 业务图层

只有底图还不足以构成业务型 WebGIS 平台。接下来,我们通过 GeoServer 加载实际的空间数据。

5.1 配置 GeoServer

假设已经安装 GeoServer,并完成以下操作:

  1. 创建工作空间 geoinsight。

  2. 创建指向 PostGIS 的数据存储。

  3. 发布行政区划数据表 public.districts。

  4. 确认图层名称为 geoinsight:districts。

  5. 检查图层坐标参考系、边界范围和访问权限。

示例 WMS 地址:

复制代码
http://localhost:8080/geoserver/geoinsight/wms

实际部署时,建议通过反向代理统一访问路径,避免前端直接依赖具体服务器地址。

5.2 在 Mapbox GL 中添加 WMS 图层

WMS 通常返回渲染后的地图图片,因此可以在 Mapbox GL 中通过栅格数据源展示。

在地图的 load 事件中添加:

复制代码
const wmsUrl =
  import.meta.env.VITE_GEOSERVER_WMS_URL

map.on('load', () => {
  if (!wmsUrl) return

  const params = new URLSearchParams({
    service: 'WMS',
    request: 'GetMap',
    version: '1.1.1',
    layers: 'geoinsight:districts',
    styles: '',
    format: 'image/png',
    transparent: 'true',
    srs: 'EPSG:3857',
    bbox: '{bbox-epsg-3857}',
    width: '256',
    height: '256',
  })

  map!.addSource('districts-source', {
    type: 'raster',
    tiles: [
      `${wmsUrl}?${params.toString()}`,
    ],
    tileSize: 256,
  })

  map!.addLayer({
    id: 'districts-layer',
    type: 'raster',
    source: 'districts-source',
    paint: {
      'raster-opacity': 0.75,
    },
  })
})

这段代码使用 WMS 1.1.1 的 SRS 和 BBOX 参数约定,并将服务返回的图片作为栅格图层显示。

如果使用 WMS 1.3.0,需要注意 CRS 参数及不同坐标参考系下的轴顺序差异。

需要说明的是,这个示例使用了简单的服务 URL 拼接方式,适合演示基本原理。正式项目还应考虑 URL 长度、服务端访问控制、跨域策略、异常响应和地图服务缓存。

5.3 设计统一图层模型

为了实现图层管理,我们需要将地图引擎中的图层和业务数据统一起来。

复制代码
export interface GisLayer {
  id: string
  name: string
  type: 'wms' | 'geojson'
  visible: boolean
  opacity: number
  serviceUrl: string
  layerName?: string
}

例如:

复制代码
const layers: GisLayer[] = [
  {
    id: 'districts',
    name: '行政区划',
    type: 'wms',
    visible: true,
    opacity: 0.75,
    serviceUrl: '/geoserver/geoinsight/wms',
    layerName: 'geoinsight:districts',
  },
]

图层面板负责展示名称、显隐状态和透明度;地图模块负责把这些状态变化同步到 Mapbox GL。

图层显隐可以通过 setLayoutProperty 控制,栅格图层透明度可以通过 setPaintProperty 调整。移除图层时,需要先移除图层本身,再移除其数据源。

这种设计使图层面板不必直接承担复杂的地图渲染逻辑,后续新增图层类型时也更容易扩展。

六、搭建 Node.js + Express 后端

前端已经能够显示地图和业务图层,接下来需要建立统一的业务服务。

后端主要负责:

  • 查询数据集和图层元信息。

  • 接收空间查询与属性查询请求。

  • 访问 PostgreSQL + PostGIS。

  • 管理数据导入任务。

  • 执行身份验证、权限控制和参数校验。

6.1 初始化后端

创建后端目录:

复制代码
mkdir backend
cd backend

npm init -y

npm install express cors dotenv pg zod
npm install -D typescript tsx @types/node \
  @types/express @types/cors @types/pg

npx tsc --init

开发阶段可以使用 tsx 运行 TypeScript:

复制代码
{
  "type": "module",
  "scripts": {
    "dev": "tsx watch src/app.ts",
    "build": "tsc"
  }
}

正式部署时,还需要配置 TypeScript 的输出目录和模块解析规则,并确认编译产物可以通过 Node.js 正确启动。

6.2 创建数据库连接池

新建 src/db/pool.ts:

复制代码
import 'dotenv/config'
import pg from 'pg'

const { Pool } = pg

export const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 10,
  idleTimeoutMillis: 30_000,
  connectionTimeoutMillis: 5_000,
})

使用连接池可以复用数据库连接,避免每个请求都创建新的连接。

生产环境还需要考虑数据库连接上限、查询超时、TLS、最小权限账号和优雅关闭机制。

6.3 创建基础 API

新建 src/app.ts:

复制代码
import 'dotenv/config'
import express from 'express'
import cors from 'cors'
import { pool } from './db/pool.js'

const app = express()

app.use(cors({
  origin: process.env.CORS_ORIGIN?.split(',') ?? [
    'http://localhost:5173',
  ],
}))

app.use(express.json({
  limit: '1mb',
}))

app.get('/api/health', async (_req, res, next) => {
  try {
    await pool.query('SELECT 1')

    res.json({
      success: true,
      data: {
        database: 'connected',
      },
    })
  } catch (error) {
    next(error)
  }
})

app.get('/api/datasets', async (_req, res, next) => {
  try {
    const result = await pool.query(`
      SELECT id, name, description, geometry_type
      FROM gis_datasets
      ORDER BY id
    `)

    res.json({
      success: true,
      data: result.rows,
    })
  } catch (error) {
    next(error)
  }
})

app.use((
  error: Error,
  _req: express.Request,
  res: express.Response,
  _next: express.NextFunction,
) => {
  console.error(error)

  res.status(500).json({
    success: false,
    error: {
      message: '服务器内部错误',
    },
  })
})

const port = Number(process.env.PORT ?? 3000)

app.listen(port, () => {
  console.log(`API server listening on ${port}`)
})

上面的代码实现了最小业务 API 骨架。完整项目还应增加身份认证、权限控制、请求日志、限流和统一异常处理。

七、PostGIS 空间数据库设计

WebGIS 的核心不只是地图渲染,更是空间数据的存储、管理和计算。

如果所有空间数据都只保存在浏览器中的 GeoJSON 文件里,那么随着数据规模和查询复杂度增加,系统很快会遇到性能瓶颈。

因此,我们使用 PostgreSQL + PostGIS 管理空间数据。

7.1 创建数据表

在 database/init.sql 中编写:

复制代码
CREATE EXTENSION IF NOT EXISTS postgis;

CREATE TABLE IF NOT EXISTS gis_datasets (
    id BIGSERIAL PRIMARY KEY,
    name VARCHAR(120) NOT NULL,
    description TEXT DEFAULT '',
    geometry_type VARCHAR(30) NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE IF NOT EXISTS facilities (
    id BIGSERIAL PRIMARY KEY,
    name VARCHAR(120) NOT NULL,
    category VARCHAR(50) NOT NULL,
    geom geometry(Point, 4326) NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_facilities_geom
ON facilities USING GIST (geom);

CREATE INDEX IF NOT EXISTS idx_facilities_category
ON facilities (category);

这里建立了两张基础表:

gis_datasets 用于记录数据集的名称、描述和几何类型。

facilities 用于存储城市设施点,包括名称、类别和空间位置。

其中,geometry(Point, 4326) 表示点几何,坐标参考系为 EPSG:4326;GiST 索引用于加速空间查询。

真实项目中,应进一步为设施表添加 dataset_id 外键,并根据业务需要增加数据来源、数据版本、更新时间和创建用户等字段。

7.2 实现空间范围查询

假设用户在地图上选择一个矩形范围:

复制代码
{
  "bbox": [121.3, 31.1, 121.6, 31.4]
}

这里约定数组顺序为:

复制代码
[最小经度, 最小纬度, 最大经度, 最大纬度]

后端通过 PostGIS 查询矩形范围内的设施。

复制代码
const result = await pool.query(
  `SELECT id, name, category,
      ST_AsGeoJSON(geom)::json AS geometry
   FROM facilities
   WHERE geom && ST_MakeEnvelope(
     $1, $2, $3, $4, 4326
   )
   AND ST_Intersects(
     geom,
     ST_MakeEnvelope($1, $2, $3, $4, 4326)
   )
   ORDER BY id
   LIMIT 1000`,
  [minX, minY, maxX, maxY],
)

这里的 && 用于利用空间索引筛选候选对象,ST_Intersects 则判断几何是否相交。

对于点要素,这可以实现矩形范围内的点查询。对于线、面等几何对象,还应根据实际业务需求选择 ST_Within、ST_Contains 或其他空间关系函数。

需要注意,真实接口必须对请求参数进行严格校验,包括坐标范围、矩形边界顺序、查询面积、结果数量和请求频率,不能直接信任浏览器传入的坐标。

7.3 为什么必须使用参数化 SQL?

错误的实现方式是把用户输入直接拼接进 SQL:

复制代码
// 不推荐:直接拼接用户输入
const sql = `SELECT * FROM facilities
WHERE category = '${category}'`

这种方式可能产生 SQL 注入风险。

正确做法是使用参数化查询:

复制代码
const result = await pool.query(
  `SELECT id, name, category
   FROM facilities
   WHERE category = $1
   ORDER BY id
   LIMIT 1000`,
  [category],
)

除了参数化查询,生产系统还应使用权限受限的数据库账号,并限制可访问的数据表和操作范围。

八、实现属性查询与地图要素识别

空间查询回答的是"某个区域里有什么",属性查询回答的则是"哪些对象符合条件"。

例如,用户希望查询某个区域内的所有医院,系统可以先根据地图范围过滤,再按照设施类别筛选。

后端查询可以采用:

复制代码
SELECT id, name, category,
       ST_AsGeoJSON(geom)::json AS geometry
FROM facilities
WHERE category = $1
  AND geom && ST_MakeEnvelope(
    $2, $3, $4, $5, 4326
  )
ORDER BY id
LIMIT 1000;

其中,分类与范围参数都必须通过参数化方式传入,并且要检查查询范围是否合法。

对于前端,如果业务要素通过 GeoJSON 图层显示,可以使用 Mapbox GL JS 的 queryRenderedFeatures 识别用户点击的要素:

复制代码
map.on('click', 'facilities-layer', event => {
  const feature = event.features?.[0]

  if (!feature) return

  // 实际项目中应将要素属性交给安全的 UI 组件展示
  console.log(feature.properties)
})

如果使用的是 WMS 图片图层,浏览器并没有完整的原始矢量要素,因此不能直接按照 GeoJSON 图层的方式识别属性。

此时可以使用 GeoServer 的 WMS GetFeatureInfo,或者让 Node.js 后端根据点击位置调用 PostGIS 查询。

这体现了 WebGIS 开发中一个重要的设计原则:地图渲染方式会影响要素识别和属性查询的实现方式。

九、构建空间数据管理模块

数据管理是 WebGIS 平台从演示系统走向业务系统的重要一步。

我们可以将其拆分为四个子模块。

9.1 数据集管理

展示数据集名称、几何类型、数据说明、创建时间和发布状态。

前端通过 /api/datasets 获取数据集列表,并使用表格组件展示。

9.2 属性表

提供分页、排序、条件筛选和要素定位能力。

当用户点击表格中的某条记录时,地图可以定位到对应要素,并通过高亮图层突出显示。

9.3 GeoJSON 数据导入

第一阶段建议优先支持 GeoJSON,因为它便于浏览器解析,也适合构建轻量级的数据导入流程。

推荐的数据导入过程如下:

复制代码
用户选择 GeoJSON 文件
        ↓
检查文件大小与格式
        ↓
解析 FeatureCollection
        ↓
验证几何、坐标和属性
        ↓
写入临时数据或暂存表
        ↓
执行数据校验
        ↓
写入正式数据集
        ↓
更新元信息与图层状态

上传接口不能仅根据文件扩展名判断数据是否合法。

还应检查文件大小、JSON 结构、Feature 数量、几何类型、坐标有效性和异常复杂的几何对象。

对于生产环境,建议使用独立暂存目录或对象存储,并为导入任务提供状态、错误明细、超时和失败清理机制。

9.4 GeoServer 图层发布

GeoServer 可以将数据库中的空间表发布为 WMS、WFS 等地图服务。

如果需要实现自动发布,可以由 Node.js 后端在完成数据导入后,受控调用 GeoServer REST API。

管理员凭据必须保存在服务端,不能直接交给浏览器。发布接口还应执行用户权限检查,避免普通用户随意发布、覆盖或删除地图服务。

十、使用 Docker 搭建开发环境

当数据库、地图服务和业务 API 都运行起来后,开发环境管理就变得非常重要。

我们可以使用 Docker Compose 统一管理 PostgreSQL + PostGIS 和 GeoServer。

以下示例展示了基本结构:

复制代码
services:
  db:
    image: postgis/postgis:16-3.4
    environment:
      POSTGRES_DB: geoinsight
      POSTGRES_USER: app_user
      POSTGRES_PASSWORD: ${DB_PASSWORD:?Set DB_PASSWORD}
    ports:
      - "127.0.0.1:5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./database/init.sql:/docker-entrypoint-initdb.d/01-init.sql:ro

  geoserver:
    image: docker.osgeo.org/geoserver:2.28.x
    ports:
      - "127.0.0.1:8080:8080"
    depends_on:
      - db

volumes:
  pgdata:

注意:2.28.x 是 GeoServer 版本系列的占位写法,并非可以保证直接拉取成功的完整镜像标签。运行前需要替换为镜像仓库实际存在的、经过验证的版本。

创建本地环境变量文件并设置数据库密码后,执行:

复制代码
docker compose up -d
docker compose ps

需要注意容器网络与宿主机网络的区别。

如果 Node.js 在宿主机运行,数据库地址通常是 localhost:5432。

如果 Node.js 也运行在 Compose 容器中,则应使用服务名 db 和容器端口 5432。

GeoServer 同样应通过容器网络访问数据库,而不是使用容器内部的 localhost。

另外,Docker Compose 只负责启动基础服务,并不会自动完成 GeoServer 工作空间、数据存储和图层发布。上述配置仍需要配合初始化脚本或管理操作才能形成完整环境。

十一、利用 OpenSpec 推进持续开发

当地图展示、图层管理、空间查询和数据管理逐步实现后,OpenSpec 的价值会更加明显。

每个模块都有独立的需求规格、设计方案、任务清单和验收条件。

例如,地图展示模块可以定义以下规格:

复制代码
## Requirement: 地图初始化

系统 MUST 在地图容器有效且配置正确时初始化地图。

### Scenario: 地图加载成功
- WHEN 用户打开地图页面
- THEN 系统显示地图底图
- AND 地图支持平移与缩放

### Scenario: 地图配置无效
- WHEN 地图样式或访问配置无效
- THEN 系统显示可理解的错误提示
- AND 不得无限重复创建地图实例

这比"完成地图初始化功能"更加具体,因为每项要求都可以转化为测试或验收步骤。

在开发过程中,可以为每个模块分别创建 OpenSpec 变更,审查规格后执行任务,再运行类型检查、单元测试和构建。

如果发现现有实现与规格不一致,应先明确差异,再更新规格或修正代码,而不是不断叠加临时补丁。

建议形成如下循环:

通过这一流程,AI 不再只是一个临时的代码生成工具,而是可以参与需求梳理、模块设计、代码实现和质量检查的开发助手。

十二、项目测试与验收

完整 WebGIS 项目不能仅以"浏览器中显示了地图"作为交付标准。

至少应覆盖以下测试。

地图与图层测试

  • 地图可以正常初始化、缩放和平移。

  • WMS 图层可以加载、隐藏和重新显示。

  • 图层透明度变化能够正确反映到地图。

  • 地图组件卸载后能够正确清理资源。

空间查询测试

  • 合法的空间范围查询返回正确结果。

  • 非法坐标和错误范围被拒绝。

  • 属性筛选结果与数据库一致。

  • 大范围或高频查询受到合理限制。

数据管理测试

  • 数据集列表能够正确展示。

  • 属性表支持分页与查询。

  • 合法 GeoJSON 可以完成校验和导入。

  • 异常文件能够被拒绝,并返回明确错误信息。

工程与安全测试

  • 前后端类型检查与构建通过。

  • SQL 查询使用参数化方式。

  • API 不向客户端泄露数据库异常堆栈。

  • 浏览器无法获取数据库凭据和 GeoServer 管理凭据。

  • 用户只能执行授权范围内的查询、上传和数据操作。

在正式交付之前,还应增加接口自动化测试、浏览器端 E2E 测试、并发查询测试、数据导入压力测试和备份恢复演练。

十三、常见问题与优化方向

13.1 为什么 WMS 图层能显示,却不能直接获取要素属性?

WMS 主要返回地图图片,不是完整的矢量要素集合。

需要获取属性时,可以使用 GetFeatureInfo,也可以通过业务 API 查询 PostGIS。

如果需要大量要素交互或复杂的客户端样式,可以考虑 GeoJSON、WFS 或矢量瓦片等方式。

13.2 Mapbox GL 与 GeoServer 的坐标系如何统一?

首先要明确每个环节的坐标参考系。

Mapbox GL 的地图交互通常使用经纬度坐标;GeoServer 的地图服务可能使用 EPSG:3857 或 EPSG:4326;PostGIS 的几何字段需要声明正确的 SRID。

特别要注意,ST_SetSRID 只设置坐标参考系标识,不会自动改变坐标值。真正的坐标转换应使用 ST_Transform。

13.3 图层很多时如何提高性能?

可以从以下几个方面入手:

  • 对大规模数据使用矢量瓦片或栅格瓦片。

  • 对查询结果设置分页和数量上限。

  • 为空间字段创建合适的空间索引。

  • 对高频请求进行缓存和去重。

  • 使用 GeoServer 缓存机制减少重复渲染。

  • 将图层配置与业务数据分开管理。

13.4 为什么需要 PostGIS,而不只是 GeoServer?

GeoServer 负责发布地图服务,PostGIS 负责空间数据存储和空间计算,两者承担不同的职责。

对于小规模演示项目,可以从静态 GeoJSON 开始;对于涉及动态数据、复杂空间查询和持续数据管理的应用,PostGIS 通常是更合适的基础设施。

十四、总结:从 AI 生成代码到规范驱动的 WebGIS 工程

通过本文,我们梳理了一套完整 WebGIS 平台的设计与实现路径。

Vue3 + TypeScript 负责前端业务组织,Mapbox GL JS 负责地图交互与渲染,Node.js + Express 负责业务 API,PostgreSQL + PostGIS 负责空间数据与查询,GeoServer 负责标准地图服务发布,而 OpenSpec 则帮助我们把整个开发过程组织成可追踪、可验证的工程流程。

这套架构的核心价值,并不是简单地把多个热门技术组合起来,而是明确每个组件的职责,并通过统一的接口约定和开发规范减少集成成本。

对于 WebGIS 开发者而言,AI 时代的能力升级不只是掌握新的地图 API 或生成代码的技巧,更是学会把业务需求拆解为可执行、可测试、可维护的工程任务。

AI 可以帮助我们更快地编写代码,但真正决定项目质量的,仍然是需求理解、空间数据建模、系统架构与工程验证。

从 OpenSpec 出发,构建 Vue3 + Mapbox GL + Node.js + GeoServer 全栈 WebGIS,不仅是一套技术实战方案,也是一种值得持续探索的 AI 辅助软件工程实践。