如何在Qwik中创建GoogleMapsHook实现API加载与功能复用?
解决方案:Qwik中可复用的Google Maps Hook(单次API加载)
核心问题分析
你当前的实现违反Hook规则的原因是:每次调用useGoogleMaps都会创建独立的signal状态,导致多个组件调用时重复触发脚本加载逻辑,并且重复初始化Map/Autocomplete实例。要解决这个问题,必须全局共享API加载状态,确保脚本只加载一次,同时让所有依赖组件复用同一个初始化上下文。
理想Hook结构设计
- 全局状态共享:使用Qwik的
useSingleton$创建全局唯一的状态容器,跟踪API加载状态、Map/Autocomplete实例。 - 单次脚本加载:仅在全局状态标记为未加载时,动态插入Google Maps脚本,并监听加载完成/失败事件。
- 封装可复用组件:在Hook中返回预配置的Map和Autocomplete组件,这些组件会自动等待API加载完成后初始化实例。
- 配置传递:支持传入自定义配置(如地图中心、缩放级别、Autocomplete选项),让组件更灵活适配业务场景。
完整实现代码
import { useSingleton$, useVisibleTask$, useSignal, component$, $ } from "@builder.io/qwik"; // 定义全局共享的Google Maps状态类型 type GoogleMapsState = { isLoaded: boolean; isLoading: boolean; mapInstance?: google.maps.Map; autocompleteInstance?: google.maps.places.Autocomplete; }; export function useGoogleMaps() { // 创建全局唯一的状态容器,所有hook调用共享同一状态 const state = useSingleton$<GoogleMapsState>(() => ({ isLoaded: false, isLoading: false, })); // 仅在浏览器端执行脚本加载逻辑 useVisibleTask$(() => { if (state.value.isLoaded || state.value.isLoading) return; state.value.isLoading = true; const apiKey = import.meta.env.PUBLIC_GOOGLE_MAPS_API_KEY; const script = document.createElement("script"); script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places`; script.async = true; script.onload = () => { state.value.isLoaded = true; state.value.isLoading = false; }; script.onerror = () => { console.error("Google Maps API加载失败"); state.value.isLoading = false; }; document.head.appendChild(script); }); // 封装Map组件:接收自定义配置 const MapComponent = component$<{ center?: google.maps.LatLngLiteral; zoom?: number; className?: string; }>((props) => { const mapRef = useSignal<HTMLDivElement>(); useVisibleTask$(({ track }) => { track(() => state.value.isLoaded); track(() => props.center); track(() => props.zoom); if (state.value.isLoaded && mapRef.value && !state.value.mapInstance) { state.value.mapInstance = new window.google.maps.Map(mapRef.value, { center: props.center || { lat: -33.8688, lng: 151.2195 }, zoom: props.zoom || 13, }); } }); return <div ref={mapRef} className={props.className || "w-full h-[400px]"} />; }); // 封装Autocomplete组件:接收自定义配置 const AutocompleteComponent = component$<{ options?: google.maps.places.AutocompleteOptions; className?: string; placeholder?: string; onPlaceChanged$?: () => void; }>((props) => { const inputRef = useSignal<HTMLInputElement>(); useVisibleTask$(({ track }) => { track(() => state.value.isLoaded); if (state.value.isLoaded && inputRef.value && !state.value.autocompleteInstance) { state.value.autocompleteInstance = new window.google.maps.places.Autocomplete( inputRef.value, props.options ); // 绑定地点选择事件(如果传入) if (props.onPlaceChanged$) { state.value.autocompleteInstance.addListener("place_changed", props.onPlaceChanged$); } } }); return ( <input ref={inputRef} type="text" className={props.className || "rounded-l-full w-full py-4 px-6 text-gray-700 leading-tight focus:outline-none"} placeholder={props.placeholder || "Enter a location"} /> ); }); return { MapComponent, AutocompleteComponent, isLoaded: state.value.isLoaded, isLoading: state.value.isLoading, mapInstance: state.value.mapInstance, autocompleteInstance: state.value.autocompleteInstance, }; }
使用示例
import { useGoogleMaps } from './useGoogleMaps'; export default component$(() => { const { MapComponent, AutocompleteComponent } = useGoogleMaps(); const handlePlaceChanged$ = $(() => { // 处理地点选择逻辑 const place = useGoogleMaps().autocompleteInstance?.getPlace(); console.log("选中地点:", place); }); return ( <div className="space-y-4"> <AutocompleteComponent placeholder="输入目标地址" onPlaceChanged$={handlePlaceChanged$} /> <MapComponent center={{ lat: 39.9042, lng: 116.4074 }} zoom={12} className="w-full h-[500px] rounded-lg shadow-md" /> </div> ); });
关键优化点说明
- 全局状态共享:通过
useSingleton$确保所有组件调用useGoogleMaps时共享同一个API加载状态和实例,彻底避免重复加载脚本。 - 脚本加载安全:添加
onerror监听处理加载失败的情况,同时标记isLoading状态防止重复触发脚本插入。 - 组件封装灵活性:Map和Autocomplete组件支持传入自定义配置、样式和事件回调,适配不同业务场景。
- 遵守Hook规则:所有状态管理和副作用逻辑都在Qwik官方Hook中处理,完全符合Hook使用规范。
内容的提问来源于stack exchange,提问作者Ignacio Porte Stefoni
相关产品推荐
相关产品推荐

