从规范驱动开发到全栈地图平台落地,带你构建一个具备地图展示、图层管理、空间查询和数据管理能力的 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 通过规范驱动开发,将开发过程组织为一系列可追踪的变更。
一个典型的工作流程包括:
-
分析需求,明确业务目标。
-
创建变更提案,描述本次要实现的能力。
-
编写规格,明确系统应当表现出的行为。
-
设计技术方案,确定模块、数据和接口。
-
拆分任务,形成可执行的实现清单。
-
根据任务实现代码并运行测试。
-
同步规格、检查结果并归档变更。
这种方式尤其适合 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,并完成以下操作:
-
创建工作空间
geoinsight。 -
创建指向 PostGIS 的数据存储。
-
发布行政区划数据表
public.districts。 -
确认图层名称为
geoinsight:districts。 -
检查图层坐标参考系、边界范围和访问权限。
示例 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 辅助软件工程实践。