mcpField
M
mapbox-integration-patterns

mapbox-integration-patterns

github.com/mapbox开发6173 次安装2 天前更新
代码仓库

关于 mapbox-integration-patterns

为在 Web 应用中集成 Mapbox GL JS 提供跨框架模式指南,面向开发者,涵盖生命周期管理、令牌配置和搜索集成。

主要功能

  1. 01React、Vue、Svelte 与 Angular 集成模式
  2. 02地图实例生命周期与清理
  3. 03环境变量令牌管理
  4. 04Mapbox Search JS 集成
  5. 05Next.js 与 SSR 注意事项

适用场景

  1. 01在 React 应用中添加交互式地图
  2. 02为不同前端框架实现地图初始化
  3. 03安全配置 Mapbox 访问令牌
  4. 04处理 Angular 服务端渲染中的地图加载

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/mapbox/mcp-devkit-server --skill mapbox-integration-patterns

安装后在 Claude Code 中输入/mapbox-integration-patterns确认已加载。

详细介绍

Mapbox 集成模式技能

此技能提供了将 Mapbox GL JS 集成到不同框架的 Web 应用中的官方模式。这些模式基于 Mapbox 的 create-web-app 脚手架工具,代表了适用于生产环境的最佳实践。

版本要求

Mapbox GL JS

推荐: v3.x(最新版本)

  • 最低版本: v3.0.0
  • 为什么选择 v3.x: 现代 API、性能提升、持续开发
  • v2.x: 仍受支持,但包含已弃用的模式(参见下方迁移说明)

通过 npm 安装(生产环境推荐):

npm install mapbox-gl@^3.0.0    # Installs latest v3.x

CDN(仅用于原型开发):


<script src="https://api.mapbox.com/mapbox-gl-js/vVERSION/mapbox-gl.js"></script>
<link
  href="https://api.mapbox.com/mapbox-gl-js/vVERSION/mapbox-gl.css"
  rel="stylesheet"
/>

⚠️ 生产应用应使用 npm,而非 CDN——以确保版本一致并支持离线构建。

框架要求

React:

  • 最低版本:19+(create-web-app 中的当前实现)
  • 推荐:最新的 19.x

Vue:

  • 最低版本:3.x(推荐使用 Composition API)
  • Vue 2.x:使用 Options API 模式(mounted/unmounted 钩子)

Svelte:

  • 最低版本:5+(create-web-app 中的当前实现)
  • 推荐:最新的 5.x

Angular:

  • 最低版本:19+(create-web-app 中的当前实现)
  • 推荐:最新的 19.x

Next.js:

  • 最低版本:13.x(App Router)
  • Pages Router:12.x+
Mapbox Search JS

搜索集成必需:

npm install @mapbox/search-js-react@^1.0.0      # React
npm install @mapbox/search-js-web@^1.0.0        # Other frameworks
版本迁移说明

从 v2.x 迁移到 v3.x:

  • 现在可以将 accessToken 传递给 Map 构造函数(推荐)
  • 改进了 TypeScript 类型
  • 提供了更好的 tree-shaking 支持
  • 核心初始化模式没有破坏性变更

示例:

const token = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN; // Use env vars in production

// v2.x pattern (still works in v3.x)
mapboxgl.accessToken = token;
const map = new mapboxgl.Map({ container: '...' });

// v3.x pattern (preferred)
const map = new mapboxgl.Map({
  accessToken: token,
  container: '...'
});

核心原则

每个 Mapbox GL JS 集成都必须:

  1. 在正确的生命周期钩子中初始化地图
  2. 将地图实例存储在组件状态中(不要在每次渲染时重新创建)
  3. 清理时始终调用 map.remove(),以防止内存泄漏
  4. 安全地管理令牌(使用环境变量)
  5. 导入 CSS:import 'mapbox-gl/dist/mapbox-gl.css'

框架专属模式

React 集成

模式:使用 useRef + useEffect 并清理

**注意:**这些示例使用 Vite(create-web-app 使用的打包工具)。如果使用 Create React App,请将 import.meta.env.VITE_MAPBOX_ACCESS_TOKEN 替换为 process.env.REACT_APP_MAPBOX_TOKEN。其他打包工具请参阅令牌管理模式部分。

import { useRef, useEffect } from 'react';
import mapboxgl from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css';

function MapComponent() {
  const mapRef = useRef(null); // Store map instance
  const mapContainerRef = useRef(null); // Store DOM reference

  useEffect(() => {
    mapboxgl.accessToken = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN;

    mapRef.current = new mapboxgl.Map({
      container: mapContainerRef.current,
      center: [-71.05953, 42.3629],
      zoom: 13
    });

    // CRITICAL: Cleanup to prevent memory leaks
    return () => {
      mapRef.current.remove();
    };
  }, []); // Empty dependency array = run once on mount

  return <div ref={mapContainerRef} style={{ height: '100vh' }} />;
}

要点:

  • 使用 useRef 保存地图实例和容器
  • 在依赖项为空的 useEffect 中初始化 []
  • 始终返回清理函数,调用 map.remove()
  • 切勿在渲染期间初始化地图(会导致无限循环)

React + Search JS:

import { useRef, useEffect, useState } from 'react';
import mapboxgl from 'mapbox-gl';
import { SearchBox } from '@mapbox/search-js-react';
import 'mapbox-gl/dist/mapbox-gl.css';

const accessToken = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN;
const center = [-71.05953, 42.3629];

function MapWithSearch() {
  const mapRef = useRef(null);
  const mapContainerRef = useRef(null);
  const [inputValue, setInputValue] = useState('');

  useEffect(() => {
    mapboxgl.accessToken = accessToken;

    mapRef.current = new mapboxgl.Map({
      container: mapContainerRef.current,
      center: center,
      zoom: 13
    });

    return () => {
      mapRef.current.remove();
    };
  }, []);

  return (
    <>
      <div
        style={{
          margin: '10px 10px 0 0',
          width: 300,
          right: 0,
          top: 0,
          position: 'absolute',
          zIndex: 10
        }}
      >
        <SearchBox
          accessToken={accessToken}
          map={mapRef.current}
          mapboxgl={mapboxgl}
          value={inputValue}
          proximity={center}
          onChange={(d) => setInputValue(d)}
          marker
        />
      </div>
      <div ref={mapContainerRef} style={{ height: '100vh' }} />
    </>
  );
}

Vue 集成

模式:使用 mounted + unmounted 生命周期钩子

<template>
  <div ref="mapContainer" class="map-container"></div>
</template>

<script>
import mapboxgl from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css';

mapboxgl.accessToken = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN;

export default {
  mounted() {
    const map = new mapboxgl.Map({
      container: this.$refs.mapContainer,
      style: 'mapbox://styles/mapbox/standard',
      center: [-71.05953, 42.3629],
      zoom: 13
    });

    // Assign map instance to component property
    this.map = map;
  },

  // CRITICAL: Clean up when component is unmounted
  unmounted() {
    this.map.remove();
    this.map = null;
  }
};
</script>

<style>
.map-container {
  width: 100%;
  height: 100%;
}
</style>

要点:

  • 在 mounted() 钩子中初始化
  • 通过 this.$refs.mapContainer 访问容器
  • 将地图存储为 this.map
  • 始终实现 unmounted() 钩子,以调用 map.remove()

Svelte 集成

模式:使用 onMount + onDestroy

<script>
  import { Map } from 'mapbox-gl'
  import 'mapbox-gl/dist/mapbox-gl.css'
  import { onMount, onDestroy } from 'svelte'

  let map
  let mapContainer

  onMount(() => {
    map = new Map({
      container: mapContainer,
      accessToken: import.meta.env.VITE_MAPBOX_ACCESS_TOKEN,
      center: [-71.05953, 42.36290],
      zoom: 13
    })
  })

  // CRITICAL: Clean up on component destroy
  onDestroy(() => {
    map.remove()
  })
</script>

<div class="map" bind:this={mapContainer}></div>

<style>
  .map {
    position: absolute;
    width: 100%;
    height: 100%;
  }
</style>

要点:

  • 使用 onMount 进行初始化
  • 使用 bind:this={mapContainer} 绑定容器
  • 始终实现 onDestroy,以调用 map.remove()
  • 在 Svelte 中可以将 accessToken 直接传递给 Map 构造函数

Angular 集成

模式:使用 ngOnInit + ngOnDestroy 并处理 SSR

import {
  Component,
  ElementRef,
  OnDestroy,
  OnInit,
  ViewChild,
  inject
} from '@angular/core';
import { isPlatformBrowser, CommonModule } from '@angular/common';
import { PLATFORM_ID } from '@angular/core';
import { environment } from '../../environments/environment';

@Component({
  selector: 'app-map',
  standalone: true,
  imports: [CommonModule],
  templateUrl: './map.component.html',
  styleUrls: ['./map.component.scss']
})
export class MapComponent implements OnInit, OnDestroy {
  @ViewChild('mapContainer', { static: false })
  mapContainer!: ElementRef<HTMLDivElement>;

  private map: any;
  private readonly platformId = inject(PLATFORM_ID);

  async ngOnInit(): Promise<void> {
    // IMPORTANT: Check if running in browser (not SSR)
    if (!isPlatformBrowser(this.platformId)) {
      return;
    }

    try {
      await this.initializeMap();
    } catch (error) {
      console.error('Failed to initialize map:', error);
    }
  }

  private async initializeMap(): Promise<void> {
    // Dynamically import to avoid SSR issues
    const mapboxgl = (await import('mapbox-gl')).default;

    this.map = new mapboxgl.Map({
      accessToken: environment.mapboxAccessToken,
      container: this.mapContainer.nativeElement,
      center: [-71.05953, 42.3629],
      zoom: 13
    });

    // Handle map errors
    this.map.on('error', (e: any) => console.error('Map error:', e.error));
  }

  // CRITICAL: Clean up on component destroy
  ngOnDestroy(): void {
    if (this.map) {
      this.map.remove();
    }
  }
}

模板(map.component.html):

<div #mapContainer style="height: 100vh; width: 100%"></div>

要点:

  • 使用 @ViewChild 引用地图容器
  • 初始化前检查 isPlatformBrowser(支持 SSR)
  • 动态导入 mapbox-gl,以避免 SSR 问题
  • 在 ngOnInit() 生命周期钩子中初始化
  • 始终实现 ngOnDestroy(),以调用 map.remove()
  • 使用 map.on('error', ...) 处理错误

原生 JavaScript(使用 Vite)

模式:使用模块导入和初始化函数

import mapboxgl from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
import './main.css';

// Set access token
mapboxgl.accessToken = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN;

let map;

/**
 * Initialize the map
 */
function initMap() {
  map = new mapboxgl.Map({
    container: 'map-container',
    center: [-71.05953, 42.3629],
    zoom: 13
  });

  map.on('load', () => {
    console.log('Map is loaded');
  });
}

// Initialize when script runs
initMap();

HTML:

<div id="map-container" style="height: 100vh;"></div>

要点:

  • 将地图存储在模块作用域变量中
  • 立即初始化,或在 DOMContentLoaded 时初始化
  • 监听 'load' 事件,以执行初始化后的操作

Vanilla JavaScript(无打包器 - CDN)

模式:使用脚本标签进行内联初始化

⚠️ **注意:**此模式仅用于原型开发。生产应用应使用 npm/打包器进行版本控制和离线构建。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Mapbox GL JS - No Bundler</title>

    
    
    <link
      href="https://api.mapbox.com/mapbox-gl-js/v3.x.x/mapbox-gl.css"
      rel="stylesheet"
    />

    <style>
      body {
        position: absolute;
        top: 0;
        right: 0;
        bottom: 0;
        left: 0;
        margin: 0;
        padding: 0;
      }
      #map-container {
        height: 100%;
        width: 100%;
      }
    </style>
  </head>
  <body>
    <div id="map-container"></div>

    
    
    <script src="https://api.mapbox.com/mapbox-gl-js/v3.x.x/mapbox-gl.js"></script>

    <script>
      // Set access token
      mapboxgl.accessToken = 'YOUR_MAPBOX_ACCESS_TOKEN_HERE';

      let map;

      function initMap() {
        map = new mapboxgl.Map({
          container: 'map-container',
          center: [-71.05953, 42.3629],
          zoom: 13
        });

        map.on('load', () => {
          console.log('Map is loaded');
        });
      }

      // Initialize when page loads
      initMap();
    </script>
  </body>
</html>

要点:

  • ⚠️ 仅用于原型开发——不建议用于生产环境
  • 将 3.x.x 替换为 Mapbox 文档中的具体版本(例如 3.7.0)
  • 不要使用 /latest/——始终固定到具体版本,以确保一致性
  • 脚本加载后再初始化(放在 body 底部)
  • 生产环境:改用 npm + 打包器

为什么生产环境不使用 CDN?

  • ❌ 依赖网络(离线时无法使用)
  • ❌ 无法锁定版本(CDN 内容可能会变化)
  • ❌ 速度较慢(没有打包器优化)
  • ❌ 不支持 tree-shaking
  • ✅ 生产环境使用 npm:npm install mapbox-gl@^3.0.0

Token 管理模式

环境变量(推荐)

不同框架使用不同的前缀来设置客户端环境变量:

框架/打包器环境变量访问方式
ViteVITE_MAPBOX_ACCESS_TOKENimport.meta.env.VITE_MAPBOX_ACCESS_TOKEN
Next.jsNEXT_PUBLIC_MAPBOX_TOKENprocess.env.NEXT_PUBLIC_MAPBOX_TOKEN
Create React AppREACT_APP_MAPBOX_TOKENprocess.env.REACT_APP_MAPBOX_TOKEN
Angularenvironment.mapboxAccessToken环境文件(environment.ts)

Vite .env 文件:

VITE_MAPBOX_ACCESS_TOKEN=pk.eyJ1...

Next.js .env.local 文件:

NEXT_PUBLIC_MAPBOX_TOKEN=pk.eyJ1...

重要提示:

  • ✅ 始终使用环境变量存储 Token
  • ✅ 切勿将 .env 文件提交到版本控制
  • ✅ 客户端应用使用公有 Token(pk.*)
  • ✅ 将 .env 添加到 .gitignore
  • ✅ 为团队提供 .env.example 模板

.gitignore:

.env
.env.local
.env.*.local

.env.example:

VITE_MAPBOX_ACCESS_TOKEN=your_token_here

Mapbox Search JS 集成

搜索框组件模式

安装依赖:

npm install @mapbox/search-js-react      # React
npm install @mapbox/search-js-web        # Vanilla/Vue/Svelte

**注意:**这两个包都将 @mapbox/search-js-core 作为依赖。只有在构建自定义搜索 UI 时,才需要直接安装 -core。

React 搜索模式:

import { SearchBox } from '@mapbox/search-js-react';

// Inside component:
<SearchBox
  accessToken={accessToken}
  map={mapRef.current} // Pass map instance
  mapboxgl={mapboxgl} // Pass mapboxgl library
  value={inputValue}
  onChange={(value) => setInputValue(value)}
  proximity={centerCoordinates} // Bias results near center
  marker // Show marker for selected result
/>;

关键配置选项:

  • accessToken:你的 Mapbox 公有 Token
  • map:地图实例(必须先初始化)
  • mapboxgl:mapboxgl 库的引用
  • proximity:[lng, lat],用于根据地理位置调整结果
  • marker:布尔值,用于显示/隐藏结果标记
  • placeholder:搜索框占位文本
搜索框定位

绝对定位(叠加层):

<div
  style={{
    position: 'absolute',
    top: 10,
    right: 10,
    zIndex: 10,
    width: 300
  }}
>
  <SearchBox {...props} />
</div>

常见位置:

  • 右上:top: 10px, right: 10px
  • 左上:top: 10px, left: 10px
  • 左下:bottom: 10px, left: 10px

应避免的常见错误

❌ 错误 1:忘记调用 map.remove()
// BAD - Memory leak!
useEffect(() => {
  const map = new mapboxgl.Map({ ... })
  // No cleanup function
}, [])
// GOOD - Proper cleanup
useEffect(() => {
  const map = new mapboxgl.Map({ ... })
  return () => map.remove()  // ✅ Cleanup
}, [])

**原因:**每个 Map 实例都会创建 WebGL 上下文、事件监听器和 DOM 节点。如果不清理,这些内容会不断累积并导致内存泄漏。


❌ 错误 2:在 render 中初始化地图
// BAD - Infinite loop in React!
function MapComponent() {
  const map = new mapboxgl.Map({ ... })  // Runs on every render
  return <div />
}
// GOOD - Initialize in effect
function MapComponent() {
  useEffect(() => {
    const map = new mapboxgl.Map({ ... })
  }, [])
  return <div />
}

**原因:**React 组件会频繁重新渲染。每次渲染都创建新地图会导致无限循环并使应用崩溃。


❌ 错误 3:未妥善保存地图实例
// BAD - map variable lost between renders
function MapComponent() {
  useEffect(() => {
    let map = new mapboxgl.Map({ ... })
    // map variable is not accessible later
  }, [])
}
// GOOD - Store in useRef
function MapComponent() {
  const mapRef = useRef()
  useEffect(() => {
    mapRef.current = new mapboxgl.Map({ ... })
    // mapRef.current accessible throughout component
  }, [])
}

**原因:**你需要访问地图实例,才能执行添加图层、标记等操作,或调用 remove()。


❌ 错误 4:useEffect 中的依赖数组错误
// BAD - Re-creates map on every render
useEffect(() => {
  const map = new mapboxgl.Map({ ... })
  return () => map.remove()
})  // No dependency array

// BAD - Re-creates map when props change
useEffect(() => {
  const map = new mapboxgl.Map({ center: props.center, ... })
  return () => map.remove()
}, [props.center])
// GOOD - Initialize once
useEffect(() => {
  const map = new mapboxgl.Map({ ... })
  return () => map.remove()
}, [])  // Empty array = run once

// GOOD - Update map property instead
useEffect(() => {
  if (mapRef.current) {
    mapRef.current.setCenter(props.center)
  }
}, [props.center])

**原因:**地图初始化开销很大。只初始化一次,然后使用地图方法更新属性。


❌ 错误 5:在源代码中硬编码 Token
// BAD - Token exposed in source code
mapboxgl.accessToken = 'pk.eyJ1IjoiZXhhbXBsZSIsImEiOiJjbGV4YW1wbGUifQ.example';
// GOOD - Use environment variable
mapboxgl.accessToken = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN;

**原因:**源代码中的 Token 会被提交到版本控制并暴露给公众。始终使用环境变量。


❌ 错误 6:未处理 Angular SSR
// BAD - Crashes during server-side rendering
ngOnInit() {
  import('mapbox-gl').then(mapboxgl => {
    this.map = new mapboxgl.Map({ ... })
  })
}
// GOOD - Check platform first
ngOnInit() {
  if (!isPlatformBrowser(this.platformId)) {
    return  // Skip map init during SSR
  }

  import('mapbox-gl').then(mapboxgl => {
    this.map = new mapboxgl.Map({ ... })
  })
}

原因: Mapbox GL JS 需要浏览器 API(WebGL、Canvas)。如果不检查平台,Angular Universal(SSR)会崩溃。


❌ 错误 7:缺少 CSS 导入
// BAD - Map renders but looks broken
import mapboxgl from 'mapbox-gl';
// Missing CSS import
// GOOD - Import CSS for proper styling
import mapboxgl from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css';

原因: CSS 文件包含地图控件、弹出窗口和标记的关键样式。缺少它时,地图会显示异常。


Next.js 特定模式

App Router(推荐)
'use client'  // Mark as client component

import { useRef, useEffect } from 'react'
import mapboxgl from 'mapbox-gl'
import 'mapbox-gl/dist/mapbox-gl.css'

export default function Map() {
  const mapRef = useRef<mapboxgl.Map>()
  const mapContainerRef = useRef<HTMLDivElement>(null)

  useEffect(() => {
    if (!mapContainerRef.current) return

    mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_TOKEN!

    mapRef.current = new mapboxgl.Map({
      container: mapContainerRef.current,
      center: [-71.05953, 42.36290],
      zoom: 13
    })

    return () => mapRef.current?.remove()
  }, [])

  return <div ref={mapContainerRef} style={{ height: '100vh' }} />
}

要点:

  • 必须使用 'use client' 指令(地图需要浏览器 API)
  • 使用 process.env.NEXT_PUBLIC_* 设置环境变量
  • 使用 TypeScript 为 mapRef 正确指定类型
Pages Router(旧版)
import dynamic from 'next/dynamic'

// Dynamically import to disable SSR for map component
const Map = dynamic(() => import('../components/Map'), {
  ssr: false,
  loading: () => <p>Loading map...</p>
})

export default function HomePage() {
  return <Map />
}

要点:

  • 使用带有 ssr: false 的 dynamic 导入
  • 提供加载状态
  • 地图组件本身遵循标准 React 模式

样式配置

默认中心点和缩放级别指南

建议的默认值:

  • 中心点:[-71.05953, 42.36290](马萨诸塞州波士顿)- Mapbox 总部
  • 缩放级别:城市级视图使用 13

缩放级别指南:

  • 0-2:世界视图
  • 3-5:大洲/国家
  • 6-9:区域/州
  • 10-12:城市视图
  • 13-15:街区
  • 16-18:街道级
  • 19-22:建筑级

根据用户位置自定义:

// Use browser geolocation
if (navigator.geolocation) {
  navigator.geolocation.getCurrentPosition((position) => {
    map.setCenter([position.coords.longitude, position.coords.latitude]);
    map.setZoom(13);
  });
}

测试模式

地图单元测试

模拟 mapbox-gl:

// vitest.config.js or jest.config.js
export default {
  setupFiles: ['./test/setup.js']
};
// test/setup.js
vi.mock('mapbox-gl', () => ({
  default: {
    Map: vi.fn(() => ({
      on: vi.fn(),
      remove: vi.fn(),
      setCenter: vi.fn(),
      setZoom: vi.fn()
    })),
    accessToken: ''
  }
}));

原因: Mapbox GL JS 需要 WebGL 和浏览器 API,而这些在测试环境中并不存在。模拟该库,以测试组件逻辑。


何时使用此技能

在以下情况下调用此技能:

  • 在新项目中设置 Mapbox GL JS
  • 将 Mapbox 集成到特定框架中
  • 调试地图初始化问题
  • 添加 Mapbox Search 功能
  • 实现适当的清理和生命周期管理
  • 在框架之间转换(例如,从 React 转到 Vue)
  • 审查代码,检查是否符合 Mapbox 集成最佳实践

相关技能

  • mapbox-cartography:地图设计原则和样式设置
  • mapbox-token-security:令牌管理和安全性
  • mapbox-style-patterns:常见地图样式模式

资源

评论

支持 Markdown