You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Qwik中创建GoogleMapsHook实现API加载与功能复用?

解决方案:Qwik中可复用的Google Maps Hook(单次API加载)

核心问题分析

你当前的实现违反Hook规则的原因是:每次调用useGoogleMaps都会创建独立的signal状态,导致多个组件调用时重复触发脚本加载逻辑,并且重复初始化Map/Autocomplete实例。要解决这个问题,必须全局共享API加载状态,确保脚本只加载一次,同时让所有依赖组件复用同一个初始化上下文。

理想Hook结构设计

  1. 全局状态共享:使用Qwik的useSingleton$创建全局唯一的状态容器,跟踪API加载状态、Map/Autocomplete实例。
  2. 单次脚本加载:仅在全局状态标记为未加载时,动态插入Google Maps脚本,并监听加载完成/失败事件。
  3. 封装可复用组件:在Hook中返回预配置的Map和Autocomplete组件,这些组件会自动等待API加载完成后初始化实例。
  4. 配置传递:支持传入自定义配置(如地图中心、缩放级别、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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 18:23:20