Skip to content

React 组件(@geoverse/react)

@geoverse/react 是 GeoVerse 的官方 React 组件层:把 core 的命令式 API 包装成声明式组件树命令式 hooks,让你用 JSX 与 props/回调驱动地图。它是一层薄绑定——不复制任何地图逻辑,坐标 / 聚合 / 绘制等全部留在 core。

@geoverse/vue 组件清单、事件映射、受控约定 1:1 对齐;差异只在框架习惯(React 用 hooks / props+回调,Vue 用 composable / v-model)。

安装

shell
pnpm add @geoverse/react geoverse ol

geoverseolreactreact-dom 均为 peerDependencies(与宿主共享实例)。要素变换组件 <GvTransform> 额外依赖可选 peer ol-ext,用到时再装 pnpm add ol-ext。支持 React 18 与 19。

快速上手(声明式)

tsx
import { useState } from 'react';
import { GvMap, GvVectorLayer, GvMarker, GvInfoWindow } from '@geoverse/react';

export default function MapDemo() {
  // React 无 v-model,受控值用「值 + onChange」对
  const [center, setCenter] = useState([118.18, 24.49]);
  const [zoom, setZoom] = useState(12);
  const [open, setOpen] = useState(false);

  return (
    <GvMap
      style={{ height: 480 }}
      base="gd-vec"
      center={center}
      zoom={zoom}
      onCenterChange={setCenter}
      onZoomChange={setZoom}
      scaleLine
    >
      <GvVectorLayer>
        <GvMarker
          options={{ position: [118.18, 24.49], color: 'red', size: 8 }}
          onClick={() => setOpen(true)}
        />
      </GvVectorLayer>

      <GvInfoWindow open={open} position={[118.18, 24.49]}>
        {/* 气泡外观由内置默认皮肤提供,如需自定义覆盖 .geoverse-info-window */}
        children 内容,经 createPortal 投递
      </GvInfoWindow>
    </GvMap>
  );
}
  • <GvMap>useEffect(浏览器侧)创建 GMap就绪后才渲染子组件并经 MapContext 下发;
  • center / zoom 受控:传入值 + onCenterChange/onZoomChange 回写;底图 base 变化走 switchBase
  • 要素组件必须置于 <GvVectorLayer> 内,否则抛出清晰错误。

命令式(hook)

需要完全控制时用 useGeoVerseMap(),在自己的容器 ref 上创建 / 销毁地图:

tsx
import { useRef } from 'react';
import { useGeoVerseMap } from '@geoverse/react';

export default function ImperativeMap() {
  const el = useRef<HTMLDivElement>(null);
  const map = useGeoVerseMap(
    { base: 'gd-vec', center: [118.18, 24.49], zoom: 12 },
    el,
  );

  function locate() {
    // map.current 是 GMap 实例,可直接调用任意 core API
    map.current?.panTo({ zoom: 14 });
  }

  return <div ref={el} style={{ height: 480 }} />;
}

<GvMap> 子树内则用 useMapContext() 取父地图(缺失时抛错)。

组件一览

类别组件
容器GvMap
图层GvVectorLayerGvHeatLayerGvImageLayerGvCustomBaseLayerGvTrafficLayerGvClusterLayerGvSuperClusterLayerGvMassLayerGvNameLayer
要素GvMarkerGvCircleGvPolygonGvPolyline
交互GvDrawGvMeasureGvFeatureEditorGvTransform@experimental
控件 / 弹窗GvOverviewGvInfoWindow
轨迹GvPathSimplifier

约定:图层 / 要素 / 工具组件的 options prop = 对应 core 类的构造选项map / view 由组件自动注入;事件经回调 props 转发(如要素的 onClick、绘制的 onComplete、轨迹的 nodeClick)。每个组件都用 forwardRef 暴露底层实例。

实例访问

ref 拿到底层 core 实例(forwardRef + useImperativeHandle),或 <GvMap onReady> 回调:

tsx
import { useRef } from 'react';
import type { GMap } from '@geoverse/core-ol';
import { GvMap } from '@geoverse/react';

const mapRef = useRef<GMap>(null);
<GvMap ref={mapRef} onReady={(map) => console.log('ready', map)} />;
// mapRef.current 就绪后即为 GMap 实例

受控属性与响应式更新

  • <GvMap>center / zoom 受控:传入值变化 → 同步到视图;交互变化 → onCenterChange/onZoomChange 回调(自行维护 state 形成受控环)。底图 base 变化触发 switchBase(切投影并重投影既有图层/要素)。
  • 要素几何受控<GvMarker options>position<GvCircle>center<GvPolygon>/<GvPolyline>path 变化时,组件会按当前底图投影自动重投影并更新几何。坐标统一以 WGS-84 经纬度传入。
    • 几何更新按「值」比较触发(内部对坐标做 JSON 比较),因此即使用内联 options={ ... }(每次渲染新对象),切底图等无关重渲染也不会误重置几何。
  • 集合属性(如 GvMassLayergraphicsGvClusterLayermarkers):建议用 useMemo 保持稳定引用,避免每次渲染产生新数组导致抖动。
  • 其余「构造即生效」的选项(样式、聚合 distance 等)变化时不自动响应,需要时经组件 ref 取实例命令式更新,或改变 key 触发重建。

React 特有注意事项

  • StrictMode 双挂载:React 18+ 在开发期 <StrictMode> 下会「挂载→卸载→再挂载」,effect 跑两次。本库所有创建类副作用都在 useEffect新建实例、cleanup 完整销毁(destroy() / removeLayer+dispose() / unByKey+removeFeature),双挂载正确收敛为一张地图,不会叠加。你自己写 effect 创建实例时也请保证对称 cleanup。
  • ol 实例不进 state:地图 / 图层 / 要素一律存 useRef(实例自身可变,放进 useState 既触发无谓 re-render 又无法反映内部变化);state 只放可序列化配置(center/zoom/样式 JSON)。
  • 回调用 latest-ref:事件 handler(onClick 等)由组件内部以 latest-ref 模式只订阅一次,规避把 handler 放进依赖数组导致的「反复重订阅」或「闭包捕获旧值」。你无需 useCallback 包裹也不会重订阅。
  • 依赖原始值而非引用:受控值(center/zoom)的同步按原始值比较,内联对象 props 不会误触发重建。
  • SSR / Next.js:所有组件是客户端组件,入口已标注 'use client',实例化只在 useEffect(浏览器侧)。Next.js App Router 下,使用地图的组件需 'use client',或用 next/dynamic + { ssr: false } 懒加载。
  • 卸载顺序:React 保证子组件 cleanup 先于父,自动对称移除(要素先 removeFeature、图层后 removeLayer、地图最后 destroy());定时器 / 监听 / overlay 由 core 的 destroy() 完整回收。

属性操作(编辑 / 过滤 / 定位)

配合编辑器三包(@geoverse/editor-core + @geoverse/editor-ol),@geoverse/react 另提供一组headless hooks无样式参考组件(RFC-0007)。属性编辑可撤销可同步、过滤高亮、按 id 定位,全部围绕一个 EditSession 展开。

Headless hooks(零样式,自己渲染 UI):

hook作用
useFieldSchema字段表(声明 ∪ 推断),随编辑实时重算
useFeatureAttributes单要素草稿副本 + setField/save/reset(save 走可撤销命令)
useAttributeFilter谓词过滤状态 + matchedIds + apply/clear
useFeatureLocatelocate(ids) → core extentOf → 适配器 fit

无样式参考组件(功能完整、可整体替换):AttributeForm / AttributeFilter / FeatureListAttributeForm 支持 render-prop 接管渲染。

tsx
import { useEffect, useRef, useState } from 'react';
import { GvMap, AttributeForm, AttributeFilter, FeatureList } from '@geoverse/react';
import type { GMap } from '@geoverse/core-ol';
import { EditSession } from '@geoverse/editor-core';
import { OlEditorAdapter } from '@geoverse/editor-ol';

function Demo() {
  const [map, setMap] = useState<GMap | null>(null);
  const [session, setSession] = useState<EditSession | null>(null);
  const adapterRef = useRef<OlEditorAdapter | null>(null);
  const [selectedId, setSelectedId] = useState<string | null>(null);

  useEffect(() => {
    if (!map) return;
    const crs = map.getView().getProjection().getCode();
    const s = new EditSession({ crs, features: /* … */ });
    const a = new OlEditorAdapter(map, {
      crs,
      onSelect: (id, additive) => {
        if (id) s.select([id], additive);
        else if (!additive) s.clearSelection();
        a.setSelection(s.getSelected());
        setSelectedId(id);
      },
    });
    const detach = s.attach(a);
    const off = s.onFilterChange(({ matchedIds, scopedIds }) =>
      a.applyFilter(matchedIds.length ? matchedIds : null, scopedIds),
    );
    adapterRef.current = a;
    setSession(s);
    return () => { off(); detach(); a.dispose(); setSession(null); };
  }, [map]);

  return (
    <>
      <GvMap base="gd-vec" center={[118.1, 24.55]} zoom={11} onReady={setMap} />
      {session && (
        <>
          <AttributeFilter session={session} />
          <FeatureList session={session} locator={adapterRef.current ?? undefined} />
          <AttributeForm session={session} featureId={selectedId} />
        </>
      )}
    </>
  );
}

关于 session 与 StrictMode

EditSession / 适配器等编辑实例同样useState/useRef 而非进 state 深比较,并在 effect cleanup 中对称 dispose。属性 hooks 遵循 latest-ref / 受控值按原值比较(同主组件约定)。详见编辑器 · 属性操作

在线示例:仓库 examples/react/pnpm dev:examples/react/ 基础组件、/react/attributes.html 属性操作)。

MapLibre 引擎(@geoverse/react/libre 子路径)

根入口 @geoverse/react 绑定 OpenLayers 引擎(geoverse/ol)。要用 MapLibre GL JS 渲染,改从子路径 @geoverse/react/libre 导入——它绑定 geoverse/libre,且不把 OpenLayers 打进 bundle。组件树、「值 + onChange」受控、createPortal 弹窗、StrictMode 双挂载收敛等心智与 OL 版一致,差异见下。

tsx
import { useState } from 'react';
import {
  GvMap,
  GvVectorLayer,
  GvMarker,
  GvEditSession,
} from '@geoverse/react/libre';

function MapView() {
  const [center, setCenter] = useState<[number, number]>([118.18, 24.49]);
  const [zoom, setZoom] = useState(11);
  return (
    <GvMap
      base="osm"
      center={center}
      zoom={zoom}
      onCenterChange={setCenter}
      onZoomChange={setZoom}
    >
      <GvVectorLayer>
        <GvMarker options={{ position: [118.18, 24.49], color: 'red' }} />
      </GvVectorLayer>
    </GvMap>
  );
}

与 OL 版的差异

  • 组件是 core-libre 能力的子集:仅 GvMapGvVectorLayerGvVectorTileLayerGvMarkerGvCircleGvPolygonGvPolylineGvInfoWindowGvEditSession没有 Heat/Mass/Cluster/Traffic/Image/CustomBase 图层、Measure/Draw/Transform 交互、Overview 等(core-libre 无对应能力)。
  • 就绪门控<GvMap> 在 MapLibre style 加载完成后才经 ready state 渲染子树并触发 onReady
  • 无重投影:libre 要素坐标恒为 WGS-84,直接存储,切底图不漂移。
  • 事件命名随 MapLibreonClick / onMouseMove / onMoveEnd(非 OL 的 onSingleClick / onPointerMove)。
  • 样式:请引入 maplibre-gl/dist/maplibre-gl.css

编辑器<GvEditSession> 走框架无关编辑引擎 + 自绘适配器(EditSession + LibreEditorAdapter),经 EditSessionContext 把会话下发给子树。同一套无样式属性/历史 hooks 与参考组件(useFeatureAttributesAttributeFormHistoryPanel 等)在 libre 子路径同样可用(本就引擎无关)。

peerDependencies 按引擎可选:只用 libre 时安装 @geoverse/core-libre + maplibre-gl(编辑再加 @geoverse/editor-libre),无需 ol / @geoverse/core-ol

在线示例:仓库 examples/react-libre/pnpm dev:examples/react-libre/);Vue 版见 examples/vue-libre/