概览
本教程介绍了如何使用 @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>
添加地图组件
用于渲染地图的基本功能组件可能会利用 useRef
、useState
和 useEffect
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} />;
onClick
、onIdle
和 google.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]);
构建标记组件
标记组件使用的模式与包含 useEffect
和 useState
钩子的地图组件类似。
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 }); } })} </> );
关联地图和应用状态
针对 onClick
和 onIdle
回调使用上述模式时,可以扩展应用,以便完整集成用户操作(例如点击或平移地图)。
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