向 React 应用添加地图和标记

概览

本教程介绍了如何使用 @googlemaps/react-wrapper 向 React 应用添加地图和标记,并将地图和标记集成到应用状态。

安装 @googlemaps/react-wrapper

安装并使用 @googlemaps/react-wrapper 库,以便在渲染组件时动态加载 Maps JavaScript API。

npm install @googlemaps/react-wrapper

此库可以通过以下代码导入和使用:

import { Wrapper, Status } from "@googlemaps/react-wrapper";

此组件的基本用途是封装依赖于 Maps JavaScript API 的子级组件。Wrapper 组件还接受 render 属性,用于渲染加载组件或处理加载 Maps JavaScript API 时出现的错误。

const render = (status: Status) => {
  return <h1>{status}</h1>;
};

<Wrapper apiKey={"YOUR_API_KEY"} render={render}>
  <YourComponent/>
</Wrapper>

添加地图组件

用于渲染地图的基本功能组件可能会利用 useRefuseStateuseEffect React 钩子

初始地图组件的签名如下:

const Map: React.FC<{}> = () => {};

由于 google.maps.Map 需要 Element 作为构造函数参数,因此需要使用 useRef 来维护在该组件的生命周期内持续存在的可变对象。以下代码段会在 Map 组件正文内的 useEffect 钩子内实例化地图。

TypeScript

const ref = React.useRef<HTMLDivElement>(null);
const [map, setMap] = React.useState<google.maps.Map>();

React.useEffect(() => {
  if (ref.current && !map) {
    setMap(new window.google.maps.Map(ref.current, {}));
  }
}, [ref, map]);

JavaScript

const ref = React.useRef(null);
const [map, setMap] = React.useState();

React.useEffect(() => {
  if (ref.current && !map) {
    setMap(new window.google.maps.Map(ref.current, {}));
  }
}, [ref, map]);

只有在 ref 发生更改时,上述 useEffect 钩子才会运行。Map 组件现在会返回以下代码:

return <div ref={ref} />

使用其他属性扩展地图组件

若要扩展基本地图组件,您需要将地图选项、事件监听器和样式的额外属性应用于包含地图的 div。以下代码展示了此功能组件的扩展接口。

interface MapProps extends google.maps.MapOptions {
  style: { [key: string]: string };
  onClick?: (e: google.maps.MapMouseEvent) => void;
  onIdle?: (map: google.maps.Map) => void;
}

const Map: React.FC<MapProps> = ({
  onClick,
  onIdle,
  children,
  style,
  ...options
}) => {}

style 对象可以直接传递,并在已渲染的 div 上设置为属性。

return <div ref={ref} style={style} />;

onClickonIdlegoogle.maps.MapOptions 需要采用 useEffect 钩子,强制将更新应用于 google.maps.Map

TypeScript

// because React does not do deep comparisons, a custom hook is used
// see discussion in https://github.com/googlemaps/js-samples/issues/946
useDeepCompareEffectForMaps(() => {
  if (map) {
    map.setOptions(options);
  }
}, [map, options]);

JavaScript

// because React does not do deep comparisons, a custom hook is used
// see discussion in https://github.com/googlemaps/js-samples/issues/946
useDeepCompareEffectForMaps(() => {
  if (map) {
    map.setOptions(options);
  }
}, [map, options]);

当作为属性传递的处理程序更新完成后,事件监听器需要略微复杂的代码来清除现有的监听器。

TypeScript

React.useEffect(() => {
  if (map) {
    ["click", "idle"].forEach((eventName) =>
      google.maps.event.clearListeners(map, eventName)
    );

    if (onClick) {
      map.addListener("click", onClick);
    }

    if (onIdle) {
      map.addListener("idle", () => onIdle(map));
    }
  }
}, [map, onClick, onIdle]);

JavaScript

React.useEffect(() => {
  if (map) {
    ["click", "idle"].forEach((eventName) =>
      google.maps.event.clearListeners(map, eventName)
    );
    if (onClick) {
      map.addListener("click", onClick);
    }

    if (onIdle) {
      map.addListener("idle", () => onIdle(map));
    }
  }
}, [map, onClick, onIdle]);

构建标记组件

标记组件使用的模式与包含 useEffectuseState 钩子的地图组件类似。

TypeScript

const Marker: React.FC<google.maps.MarkerOptions> = (options) => {
  const [marker, setMarker] = React.useState<google.maps.Marker>();

  React.useEffect(() => {
    if (!marker) {
      setMarker(new google.maps.Marker());
    }

    // remove marker from map on unmount
    return () => {
      if (marker) {
        marker.setMap(null);
      }
    };
  }, [marker]);

  React.useEffect(() => {
    if (marker) {
      marker.setOptions(options);
    }
  }, [marker, options]);

  return null;
};

JavaScript

const Marker = (options) => {
  const [marker, setMarker] = React.useState();

  React.useEffect(() => {
    if (!marker) {
      setMarker(new google.maps.Marker());
    }

    // remove marker from map on unmount
    return () => {
      if (marker) {
        marker.setMap(null);
      }
    };
  }, [marker]);
  React.useEffect(() => {
    if (marker) {
      marker.setOptions(options);
    }
  }, [marker, options]);
  return null;
};

该组件会返回 null,因为 google.maps.Map 会管理 DOM 操纵。

添加标记作为地图的子组件

若要向地图添加标记,需要使用特殊的 children 属性将 Marker 组件传递给 Map 组件,如下所示。

<Wrapper apiKey={"YOUR_API_KEY"}>
  <Map center={center} zoom={zoom}>
    <Marker position={position} />
  </Map>
</Wrapper>

您必须对 Map 组件的输出进行细微更改,以便将 google.maps.Map 对象作为额外的属性传递给所有子级。

TypeScript

return (
  <>
    <div ref={ref} style={style} />
    {React.Children.map(children, (child) => {
      if (React.isValidElement(child)) {
        // set the map prop on the child component
        // @ts-ignore
        return React.cloneElement(child, { map });
      }
    })}
  </>
);

JavaScript

return (
  <>
    <div ref={ref} style={style} />
    {React.Children.map(children, (child) => {
      if (React.isValidElement(child)) {
        // set the map prop on the child component
        // @ts-ignore
        return React.cloneElement(child, { map });
      }
    })}
  </>
);

关联地图和应用状态

针对 onClickonIdle 回调使用上述模式时,可以扩展应用,以便完整集成用户操作(例如点击或平移地图)。

TypeScript

const [clicks, setClicks] = React.useState<google.maps.LatLng[]>([]);
const [zoom, setZoom] = React.useState(3); // initial zoom
const [center, setCenter] = React.useState<google.maps.LatLngLiteral>({
  lat: 0,
  lng: 0,
});

const onClick = (e: google.maps.MapMouseEvent) => {
  // avoid directly mutating state
  setClicks([...clicks, e.latLng!]);
};

const onIdle = (m: google.maps.Map) => {
  console.log("onIdle");
  setZoom(m.getZoom()!);
  setCenter(m.getCenter()!.toJSON());
};

JavaScript

const [clicks, setClicks] = React.useState([]);
const [zoom, setZoom] = React.useState(3); // initial zoom
const [center, setCenter] = React.useState({
  lat: 0,
  lng: 0,
});

const onClick = (e) => {
  // avoid directly mutating state
  setClicks([...clicks, e.latLng]);
};

const onIdle = (m) => {
  console.log("onIdle");
  setZoom(m.getZoom());
  setCenter(m.getCenter().toJSON());
};

您可以使用以下模式将这些钩子集成到表单元素,如纬度输入所示。

<label htmlFor="lat">Latitude</label>
<input
  type="number"
  id="lat"
  name="lat"
  value={center.lat}
  onChange={(event) =>
    setCenter({ ...center, lat: Number(event.target.value) })
  }
/>

最后,应用可以跟踪点击并在每个点击位置渲染标记。

{clicks.map((latLng, i) => (<Marker key={i} position={latLng} />))}

探索代码

若要探索完整的示例代码,您可以访问下面的在线代码园地,也可以克隆 Git 代码库。

试用示例

克隆示例

您必须拥有 Git 和 Node.js,才能在本地运行此示例。请按照相关说明安装 Node.js 和 NPM。以下命令会克隆、安装依赖项并启动示例应用。

  git clone -b sample-react-map https://github.com/googlemaps/js-samples.git
  cd js-samples
  npm i
  npm start

如需试用其他示例,您可以切换到以 sample-SAMPLE_NAME 开头的任何分支。

  git checkout sample-SAMPLE_NAME
  npm i
  npm start