mapbox-web-integration-patterns

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Mapbox Integration Patterns Skill

Mapbox集成方案技能

This skill provides official patterns for integrating Mapbox GL JS into web applications across different frameworks. These patterns are based on Mapbox's
create-web-app
scaffolding tool and represent production-ready best practices.
本技能提供了在不同Web框架中集成Mapbox GL JS的官方方案,这些方案基于Mapbox的
create-web-app
脚手架工具,代表了生产环境就绪的最佳实践。

Version Requirements

版本要求

Mapbox GL JS

Mapbox GL JS

Recommended: v3.x (latest)
  • Minimum: v3.0.0
  • Why v3.x: Modern API, improved performance, active development
  • v2.x: Still supported but deprecated patterns (see migration notes below)
Installing via npm (recommended for production):
bash
npm install mapbox-gl@^3.0.0    # Installs latest v3.x
CDN (for prototyping only):
html
<!-- Replace VERSION with latest v3.x from https://docs.mapbox.com/mapbox-gl-js/ -->
<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"
/>
⚠️ Production apps should use npm, not CDN - ensures consistent versions and offline builds.
推荐版本: v3.x(最新版)
  • 最低版本: v3.0.0
  • 选择v3.x的原因: 现代化API、性能提升、持续活跃开发
  • v2.x: 仍受支持但属于已弃用方案(详见下方迁移说明)
通过npm安装(生产环境推荐):
bash
npm install mapbox-gl@^3.0.0    # 安装最新v3.x版本
CDN方式(仅用于原型开发):
html
<!-- 将VERSION替换为https://docs.mapbox.com/mapbox-gl-js/中的最新v3.x版本号 -->
<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 - 确保版本一致并支持离线构建。

Framework Requirements

框架要求

React:
  • Minimum: 19+ (current implementation in create-web-app)
  • Recommended: Latest 19.x
Vue:
  • Minimum: 3.x (Composition API recommended)
  • Vue 2.x: Use Options API pattern (mounted/unmounted hooks)
Svelte:
  • Minimum: 5+ (current implementation in create-web-app)
  • Recommended: Latest 5.x
Angular:
  • Minimum: 19+ (current implementation in create-web-app)
  • Recommended: Latest 19.x
Next.js:
  • Minimum: 13.x (App Router)
  • Pages Router: 12.x+
React:
  • 最低版本:19+(create-web-app当前实现版本)
  • 推荐版本:最新19.x
Vue:
  • 最低版本:3.x(推荐使用组合式API)
  • Vue 2.x:使用选项式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

Mapbox Search JS

Required for search integration:
bash
npm install @mapbox/search-js-react@^1.0.0      # React
npm install @mapbox/search-js-web@^1.0.0        # Other frameworks
搜索集成所需依赖:
bash
npm install @mapbox/search-js-react@^1.0.0      # React框架
npm install @mapbox/search-js-web@^1.0.0        # 其他框架

Version Migration Notes

版本迁移说明

Migrating from v2.x to v3.x:
  • accessToken
    can now be passed to Map constructor (preferred)
  • Improved TypeScript types
  • Better tree-shaking support
  • No breaking changes to core initialization patterns
Example:
javascript
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: '...'
});
从v2.x迁移到v3.x:
  • accessToken
    现在可直接传入Map构造函数(推荐方式)
  • 改进了TypeScript类型定义
  • 更好的Tree-shaking支持
  • 核心初始化方案无破坏性变更
示例:
javascript
const token = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN; // 生产环境使用环境变量

// v2.x方案(在v3.x中仍可使用)
mapboxgl.accessToken = token;
const map = new mapboxgl.Map({ container: '...' });

// v3.x方案(推荐)
const map = new mapboxgl.Map({
  accessToken: token,
  container: '...'
});

Core Principles

核心原则

Every Mapbox GL JS integration must:
  1. Initialize the map in the correct lifecycle hook
  2. Store map instance in component state (not recreate on every render)
  3. Always call
    map.remove()
    on cleanup
    to prevent memory leaks
  4. Handle token management securely (environment variables)
  5. Import CSS:
    import 'mapbox-gl/dist/mapbox-gl.css'
所有Mapbox GL JS集成必须满足:
  1. 在正确的生命周期钩子中初始化地图
  2. 将地图实例存储在组件状态中(不要在每次渲染时重新创建)
  3. 清理时务必调用
    map.remove()
    以防止内存泄漏
  4. 安全处理令牌管理(使用环境变量)
  5. 导入CSS:
    import 'mapbox-gl/dist/mapbox-gl.css'

Framework-Specific Patterns

各框架专属集成方案

React Integration

React集成

Pattern: useRef + useEffect with cleanup
Note: These examples use Vite (the bundler used in
create-web-app
). If using Create React App, replace
import.meta.env.VITE_MAPBOX_ACCESS_TOKEN
with
process.env.REACT_APP_MAPBOX_TOKEN
. See the Token Management Patterns section for other bundlers.
jsx
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' }} />;
}
Key points:
  • Use
    useRef
    for both map instance and container
  • Initialize in
    useEffect
    with empty deps
    []
  • Always return cleanup function that calls
    map.remove()
  • Never initialize map in render (causes infinite loops)
React + Search JS:
jsx
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' }} />
    </>
  );
}

方案:useRef + 带清理逻辑的useEffect
注意: 以下示例使用Vite(create-web-app使用的打包工具)。如果使用Create React App,请将
import.meta.env.VITE_MAPBOX_ACCESS_TOKEN
替换为
process.env.REACT_APP_MAPBOX_TOKEN
。其他打包工具的令牌获取方式请查看令牌管理方案章节。
jsx
import { useRef, useEffect } from 'react';
import mapboxgl from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css';

function MapComponent() {
  const mapRef = useRef(null); // 存储地图实例
  const mapContainerRef = useRef(null); // 存储DOM引用

  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
    });

    // 关键:清理逻辑防止内存泄漏
    return () => {
      mapRef.current.remove();
    };
  }, []); // 空依赖数组 = 仅在组件挂载时执行一次

  return <div ref={mapContainerRef} style={{ height: '100vh' }} />;
}
关键点:
  • 使用
    useRef
    存储地图实例和容器引用
  • useEffect
    中初始化,依赖数组为空
    []
  • 务必返回清理函数 调用
    map.remove()
  • 绝不要在渲染阶段初始化地图(会导致无限循环)
React + Search JS集成:
jsx
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 Integration

Vue集成

Pattern: mounted + unmounted lifecycle hooks
vue
<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>
Key points:
  • Initialize in
    mounted()
    hook
  • Access container via
    this.$refs.mapContainer
  • Store map as
    this.map
  • Always implement
    unmounted()
    hook
    to call
    map.remove()

方案:mounted + unmounted生命周期钩子
vue
<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
    });

    // 将地图实例赋值给组件属性
    this.map = map;
  },

  // 关键:组件卸载时执行清理
  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 Integration

Svelte集成

Pattern: onMount + onDestroy
svelte
<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>
Key points:
  • Use
    onMount
    for initialization
  • Bind container with
    bind:this={mapContainer}
  • Always implement
    onDestroy
    to call
    map.remove()
  • Can pass
    accessToken
    directly to Map constructor in Svelte

方案:onMount + onDestroy
svelte
<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
    })
  })

  // 关键:组件销毁时执行清理
  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 Integration

Angular集成

Pattern: ngOnInit + ngOnDestroy with SSR handling
typescript
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();
    }
  }
}
Template (map.component.html):
html
<div #mapContainer style="height: 100vh; width: 100%"></div>
Key points:
  • Use
    @ViewChild
    to reference map container
  • Check
    isPlatformBrowser
    before initializing
    (SSR support)
  • Dynamically import
    mapbox-gl
    to avoid SSR issues
  • Initialize in
    ngOnInit()
    lifecycle hook
  • Always implement
    ngOnDestroy()
    to call
    map.remove()
  • Handle errors with
    map.on('error', ...)

方案:ngOnInit + ngOnDestroy 结合SSR处理
typescript
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> {
    // 重要:检查是否在浏览器环境运行(而非SSR)
    if (!isPlatformBrowser(this.platformId)) {
      return;
    }

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

  private async initializeMap(): Promise<void> {
    // 动态导入以避免SSR问题
    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
    });

    // 处理地图错误
    this.map.on('error', (e: any) => console.error('Map error:', e.error));
  }

  // 关键:组件销毁时执行清理
  ngOnDestroy(): void {
    if (this.map) {
      this.map.remove();
    }
  }
}
模板文件(map.component.html):
html
<div #mapContainer style="height: 100vh; width: 100%"></div>
关键点:
  • 使用
    @ViewChild
    获取地图容器引用
  • 初始化前检查
    isPlatformBrowser
    (支持SSR)
  • 动态导入
    mapbox-gl
    以避免SSR问题
  • ngOnInit()
    生命周期钩子中初始化
  • 务必实现
    ngOnDestroy()
    调用
    map.remove()
  • 通过
    map.on('error', ...)
    处理地图错误

Vanilla JavaScript (with Vite)

原生JavaScript(结合Vite)

Pattern: Module imports with initialization function
javascript
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:
html
<div id="map-container" style="height: 100vh;"></div>
Key points:
  • Store map in module-scoped variable
  • Initialize immediately or on DOMContentLoaded
  • Listen for 'load' event for post-initialization actions

方案:模块导入 + 初始化函数
javascript
import mapboxgl from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
import './main.css';

// 设置访问令牌
mapboxgl.accessToken = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN;

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');
  });
}

// 脚本运行时初始化地图
initMap();
HTML:
html
<div id="map-container" style="height: 100vh;"></div>
关键点:
  • 将地图实例存储在模块作用域变量中
  • 立即初始化或在DOMContentLoaded事件触发时初始化
  • 监听'load'事件以执行初始化后的操作

Vanilla JavaScript (No Bundler - CDN)

原生JavaScript(无打包工具 - CDN方式)

Pattern: Script tag with inline initialization
⚠️ Note: This pattern is for prototyping only. Production apps should use npm/bundler for version control and offline builds.
html
<!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>

    <!-- Mapbox GL JS CSS -->
    <!-- Replace 3.x.x with latest version from https://docs.mapbox.com/mapbox-gl-js/ -->
    <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>

    <!-- Mapbox GL JS -->
    <!-- Replace 3.x.x with latest version from https://docs.mapbox.com/mapbox-gl-js/ -->
    <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>
Key points:
  • ⚠️ Prototyping only - not recommended for production
  • Replace
    3.x.x
    with specific version (e.g.,
    3.7.0
    ) from Mapbox docs
  • Don't use
    /latest/
    - always pin to specific version for consistency
  • Initialize after script loads (bottom of body)
  • For production: Use npm + bundler instead
Why not CDN for production?
  • ❌ Network dependency (breaks offline)
  • ❌ No version locking (CDN could change)
  • ❌ Slower (no bundler optimization)
  • ❌ No tree-shaking
  • ✅ Use npm for production:
    npm install mapbox-gl@^3.0.0

方案:脚本标签 + 内联初始化
⚠️ 注意: 此方案仅用于原型开发。生产环境应用应使用npm+打包工具进行版本控制和离线构建。
html
<!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>

    <!-- Mapbox GL JS CSS -->
    <!-- 将3.x.x替换为https://docs.mapbox.com/mapbox-gl-js/中的最新版本 -->
    <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>

    <!-- Mapbox GL JS -->
    <!-- 将3.x.x替换为https://docs.mapbox.com/mapbox-gl-js/中的最新版本 -->
    <script src="https://api.mapbox.com/mapbox-gl-js/v3.x.x/mapbox-gl.js"></script>

    <script>
      // 设置访问令牌
      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');
        });
      }

      // 页面加载完成后初始化
      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 Management Patterns

令牌管理方案

Environment Variables (Recommended)

环境变量(推荐方式)

Different frameworks use different prefixes for client-side environment variables:
Framework/BundlerEnvironment VariableAccess Pattern
Vite
VITE_MAPBOX_ACCESS_TOKEN
import.meta.env.VITE_MAPBOX_ACCESS_TOKEN
Next.js
NEXT_PUBLIC_MAPBOX_TOKEN
process.env.NEXT_PUBLIC_MAPBOX_TOKEN
Create React App
REACT_APP_MAPBOX_TOKEN
process.env.REACT_APP_MAPBOX_TOKEN
Angular
environment.mapboxAccessToken
Environment files (
environment.ts
)
Vite .env file:
bash
VITE_MAPBOX_ACCESS_TOKEN=pk.YOUR_MAPBOX_TOKEN_HERE
Next.js .env.local file:
bash
NEXT_PUBLIC_MAPBOX_TOKEN=pk.YOUR_MAPBOX_TOKEN_HERE
Important:
  • ✅ Always use environment variables for tokens
  • ✅ Never commit
    .env
    files to version control
  • ✅ Use public tokens (pk.*) for client-side apps
  • ✅ Add
    .env
    to
    .gitignore
  • ✅ Provide
    .env.example
    template for team
.gitignore:
.env
.env.local
.env.*.local
.env.example:
bash
VITE_MAPBOX_ACCESS_TOKEN=your_token_here

不同框架/打包工具对客户端环境变量使用不同的前缀:
框架/打包工具环境变量名称获取方式
Vite
VITE_MAPBOX_ACCESS_TOKEN
import.meta.env.VITE_MAPBOX_ACCESS_TOKEN
Next.js
NEXT_PUBLIC_MAPBOX_TOKEN
process.env.NEXT_PUBLIC_MAPBOX_TOKEN
Create React App
REACT_APP_MAPBOX_TOKEN
process.env.REACT_APP_MAPBOX_TOKEN
Angular
environment.mapboxAccessToken
环境文件(
environment.ts
Vite的.env文件示例:
bash
VITE_MAPBOX_ACCESS_TOKEN=pk.YOUR_MAPBOX_TOKEN_HERE
Next.js的.env.local文件示例:
bash
NEXT_PUBLIC_MAPBOX_TOKEN=pk.YOUR_MAPBOX_TOKEN_HERE
重要提示:
  • ✅ 始终使用环境变量存储令牌
  • ✅ 绝不要将
    .env
    文件提交到版本控制系统
  • ✅ 客户端应用使用公钥(pk.*开头)
  • ✅ 将
    .env
    添加到
    .gitignore
  • ✅ 为团队提供
    .env.example
    模板
.gitignore示例:
.env
.env.local
.env.*.local
.env.example示例:
bash
VITE_MAPBOX_ACCESS_TOKEN=your_token_here

Mapbox Search JS Integration

Mapbox Search JS集成

Search Box Component Pattern

搜索框组件方案

Install dependency:
bash
npm install @mapbox/search-js-react      # React
npm install @mapbox/search-js-web        # Vanilla/Vue/Svelte
Note: Both packages include
@mapbox/search-js-core
as a dependency. You only need to install
-core
directly if building a custom search UI.
React Search Pattern:
jsx
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
/>;
Key configuration options:
  • accessToken
    : Your Mapbox public token
  • map
    : Map instance (must be initialized first)
  • mapboxgl
    : The mapboxgl library reference
  • proximity
    :
    [lng, lat]
    to bias results geographically
  • marker
    : Boolean to show/hide result marker
  • placeholder
    : Search box placeholder text
安装依赖:
bash
npm install @mapbox/search-js-react      # React框架
npm install @mapbox/search-js-web        # 原生JS/Vue/Svelte
注意: 两个包均依赖
@mapbox/search-js-core
,仅在自定义搜索UI时才需要直接安装
-core
包。
React搜索集成示例:
jsx
import { SearchBox } from '@mapbox/search-js-react';

// 组件内部使用:
<SearchBox
  accessToken={accessToken}
  map={mapRef.current} // 传入地图实例
  mapboxgl={mapboxgl} // 传入mapboxgl库引用
  value={inputValue}
  onChange={(value) => setInputValue(value)}
  proximity={centerCoordinates} // 优先返回地图中心附近结果
  marker // 为选中结果显示标记
/>;
关键配置项:
  • accessToken
    : 你的Mapbox公钥
  • map
    : 地图实例(必须先完成初始化)
  • mapboxgl
    : mapboxgl库引用
  • proximity
    :
    [经度, 纬度]
    ,用于地理范围偏好设置
  • marker
    : 布尔值,控制是否显示结果标记
  • placeholder
    : 搜索框占位文本

Positioning Search Box

搜索框定位

Absolute positioning (overlay):
jsx
<div
  style={{
    position: 'absolute',
    top: 10,
    right: 10,
    zIndex: 10,
    width: 300
  }}
>
  <SearchBox {...props} />
</div>
Common positions:
  • Top-right:
    top: 10px, right: 10px
  • Top-left:
    top: 10px, left: 10px
  • Bottom-left:
    bottom: 10px, left: 10px

绝对定位(悬浮层):
jsx
<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

Common Mistakes to Avoid

需避免的常见错误

❌ Mistake 1: Forgetting to call map.remove()

❌ 错误1:忘记调用map.remove()

javascript
// BAD - Memory leak!
useEffect(() => {
  const map = new mapboxgl.Map({ ... })
  // No cleanup function
}, [])
javascript
// GOOD - Proper cleanup
useEffect(() => {
  const map = new mapboxgl.Map({ ... })
  return () => map.remove()  // ✅ Cleanup
}, [])
Why: Every Map instance creates WebGL contexts, event listeners, and DOM nodes. Without cleanup, these accumulate and cause memory leaks.

javascript
// 错误 - 内存泄漏!
useEffect(() => {
  const map = new mapboxgl.Map({ ... })
  // 无清理函数
}, [])
javascript
// 正确 - 合理清理
useEffect(() => {
  const map = new mapboxgl.Map({ ... })
  return () => map.remove()  // ✅ 清理逻辑
}, [])
原因: 每个Map实例都会创建WebGL上下文、事件监听器和DOM节点,不清理会导致内存泄漏累积。

❌ Mistake 2: Initializing map in render

❌ 错误2:在渲染阶段初始化地图

javascript
// BAD - Infinite loop in React!
function MapComponent() {
  const map = new mapboxgl.Map({ ... })  // Runs on every render
  return <div />
}
javascript
// GOOD - Initialize in effect
function MapComponent() {
  useEffect(() => {
    const map = new mapboxgl.Map({ ... })
  }, [])
  return <div />
}
Why: React components re-render frequently. Creating a new map on every render causes infinite loops and crashes.

javascript
// 错误 - React中会导致无限循环!
function MapComponent() {
  const map = new mapboxgl.Map({ ... })  // 每次渲染都会执行
  return <div />
}
javascript
// 正确 - 在Effect中初始化
function MapComponent() {
  useEffect(() => {
    const map = new mapboxgl.Map({ ... })
  }, [])
  return <div />
}
原因: React组件会频繁重渲染,每次渲染都创建新地图会导致无限循环和崩溃。

❌ Mistake 3: Not storing map instance properly

❌ 错误3:未正确存储地图实例

javascript
// BAD - map variable lost between renders
function MapComponent() {
  useEffect(() => {
    let map = new mapboxgl.Map({ ... })
    // map variable is not accessible later
  }, [])
}
javascript
// GOOD - Store in useRef
function MapComponent() {
  const mapRef = useRef()
  useEffect(() => {
    mapRef.current = new mapboxgl.Map({ ... })
    // mapRef.current accessible throughout component
  }, [])
}
Why: You need to access the map instance for operations like adding layers, markers, or calling
remove()
.

javascript
// 错误 - 渲染间会丢失map变量
function MapComponent() {
  useEffect(() => {
    let map = new mapboxgl.Map({ ... })
    // 后续无法访问map变量
  }, [])
}
javascript
// 正确 - 使用useRef存储
function MapComponent() {
  const mapRef = useRef()
  useEffect(() => {
    mapRef.current = new mapboxgl.Map({ ... })
    // 组件生命周期内均可访问mapRef.current
  }, [])
}
原因: 你需要访问地图实例来执行添加图层、标记或调用
remove()
等操作。

❌ Mistake 4: Wrong dependency array in useEffect

❌ 错误4:useEffect依赖数组设置错误

javascript
// 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])
javascript
// 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])
Why: Map initialization is expensive. Initialize once, then use map methods to update properties.

javascript
// 错误 - 每次渲染都会重新创建地图
useEffect(() => {
  const map = new mapboxgl.Map({ ... })
  return () => map.remove()
})  // 无依赖数组

// 错误 - props变化时重新创建地图
useEffect(() => {
  const map = new mapboxgl.Map({ center: props.center, ... })
  return () => map.remove()
}, [props.center])
javascript
// 正确 - 仅初始化一次
useEffect(() => {
  const map = new mapboxgl.Map({ ... })
  return () => map.remove()
}, [])  // 空数组 = 仅执行一次

// 正确 - 直接更新地图属性
useEffect(() => {
  if (mapRef.current) {
    mapRef.current.setCenter(props.center)
  }
}, [props.center])
原因: 地图初始化成本高,应仅初始化一次,然后通过地图方法更新属性。

❌ Mistake 5: Hardcoding token in source code

❌ 错误5:在源码中硬编码令牌

javascript
// BAD - Token exposed in source code
mapboxgl.accessToken = 'pk.YOUR_MAPBOX_TOKEN_HERE';
javascript
// GOOD - Use environment variable
mapboxgl.accessToken = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN;
Why: Tokens in source code get committed to version control and exposed publicly. Always use environment variables.

javascript
// 错误 - 令牌会暴露在源码中
mapboxgl.accessToken = 'pk.YOUR_MAPBOX_TOKEN_HERE';
javascript
// 正确 - 使用环境变量
mapboxgl.accessToken = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN;
原因: 源码中的令牌会被提交到版本控制系统并公开暴露,始终使用环境变量。

❌ Mistake 6: Not handling Angular SSR

❌ 错误6:未处理Angular SSR

typescript
// BAD - Crashes during server-side rendering
ngOnInit() {
  import('mapbox-gl').then(mapboxgl => {
    this.map = new mapboxgl.Map({ ... })
  })
}
typescript
// 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({ ... })
  })
}
Why: Mapbox GL JS requires browser APIs (WebGL, Canvas). Angular Universal (SSR) will crash without platform check.

typescript
// 错误 - 服务端渲染时会崩溃
ngOnInit() {
  import('mapbox-gl').then(mapboxgl => {
    this.map = new mapboxgl.Map({ ... })
  })
}
typescript
// 正确 - 先检查运行平台
ngOnInit() {
  if (!isPlatformBrowser(this.platformId)) {
    return  // SSR阶段跳过地图初始化
  }

  import('mapbox-gl').then(mapboxgl => {
    this.map = new mapboxgl.Map({ ... })
  })
}
原因: Mapbox GL JS依赖浏览器API(WebGL、Canvas),无平台检查时Angular Universal(SSR)会崩溃。

❌ Mistake 7: Missing CSS import

❌ 错误7:缺失CSS导入

javascript
// BAD - Map renders but looks broken
import mapboxgl from 'mapbox-gl';
// Missing CSS import
javascript
// GOOD - Import CSS for proper styling
import mapboxgl from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
Why: The CSS file contains critical styles for map controls, popups, and markers. Without it, the map appears broken.

javascript
// 错误 - 地图可渲染但样式错乱
import mapboxgl from 'mapbox-gl';
// 缺失CSS导入
javascript
// 正确 - 导入CSS以保证样式正常
import mapboxgl from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
原因: CSS文件包含地图控件、弹窗和标记的关键样式,缺失会导致地图显示错乱。

Next.js Specific Patterns

Next.js专属方案

App Router (Recommended)

App Router(推荐)

typescript
'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' }} />
}
Key points:
  • Must use
    'use client'
    directive
    (maps require browser APIs)
  • Use
    process.env.NEXT_PUBLIC_*
    for environment variables
  • Type
    mapRef
    properly with TypeScript
typescript
'use client'  // 标记为客户端组件

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 (Legacy)

Pages Router(旧版)

typescript
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 />
}
Key points:
  • Use
    dynamic
    import with
    ssr: false
  • Provide loading state
  • Map component itself follows standard React pattern

typescript
import dynamic from 'next/dynamic'

// 动态导入以禁用地图组件的SSR
const Map = dynamic(() => import('../components/Map'), {
  ssr: false,
  loading: () => <p>Loading map...</p>
})

export default function HomePage() {
  return <Map />
}
关键点:
  • 使用
    dynamic
    导入并设置
    ssr: false
  • 提供加载状态
  • 地图组件本身遵循标准React集成方案

Style Configuration

样式配置

Default Center and Zoom Guidelines

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

Recommended defaults:
  • Center:
    [-71.05953, 42.36290]
    (Boston, MA) - Mapbox HQ
  • Zoom:
    13
    for city-level view
Zoom level guide:
  • 0-2
    : World view
  • 3-5
    : Continent/country
  • 6-9
    : Region/state
  • 10-12
    : City view
  • 13-15
    : Neighborhood
  • 16-18
    : Street level
  • 19-22
    : Building level
Customizing for user location:
javascript
// Use browser geolocation
if (navigator.geolocation) {
  navigator.geolocation.getCurrentPosition((position) => {
    map.setCenter([position.coords.longitude, position.coords.latitude]);
    map.setZoom(13);
  });
}

推荐默认值:
  • 中心点:
    [-71.05953, 42.36290]
    (马萨诸塞州波士顿 - Mapbox总部)
  • 缩放级别:
    13
    (城市级视图)
缩放级别参考:
  • 0-2
    : 全球视图
  • 3-5
    : 大洲/国家视图
  • 6-9
    : 地区/州级视图
  • 10-12
    : 城市视图
  • 13-15
    : 街区视图
  • 16-18
    : 街道级视图
  • 19-22
    : 建筑级视图
根据用户位置自定义:
javascript
// 使用浏览器定位API
if (navigator.geolocation) {
  navigator.geolocation.getCurrentPosition((position) => {
    map.setCenter([position.coords.longitude, position.coords.latitude]);
    map.setZoom(13);
  });
}

Testing Patterns

测试方案

Unit Testing Maps

地图单元测试

Mock mapbox-gl:
javascript
// vitest.config.js or jest.config.js
export default {
  setupFiles: ['./test/setup.js']
};
javascript
// test/setup.js
vi.mock('mapbox-gl', () => ({
  default: {
    Map: vi.fn(() => ({
      on: vi.fn(),
      remove: vi.fn(),
      setCenter: vi.fn(),
      setZoom: vi.fn()
    })),
    accessToken: ''
  }
}));
Why: Mapbox GL JS requires WebGL and browser APIs that don't exist in test environments. Mock the library to test component logic.

Mock mapbox-gl:
javascript
// vitest.config.js或jest.config.js
export default {
  setupFiles: ['./test/setup.js']
};
javascript
// 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,这些在测试环境中不存在,Mock该库可测试组件逻辑。

When to Use This Skill

何时使用本技能

Invoke this skill when:
  • Setting up Mapbox GL JS in a new project
  • Integrating Mapbox into a specific framework
  • Debugging map initialization issues
  • Adding Mapbox Search functionality
  • Implementing proper cleanup and lifecycle management
  • Converting between frameworks (e.g., React to Vue)
  • Reviewing code for Mapbox integration best practices
在以下场景调用本技能:
  • 新项目中搭建Mapbox GL JS
  • 在特定框架中集成Mapbox
  • 调试地图初始化问题
  • 添加Mapbox搜索功能
  • 实现合理的清理与生命周期管理
  • 跨框架迁移集成方案(例如React转Vue)
  • 审查Mapbox集成代码的最佳实践

Related Skills

相关技能

  • mapbox-cartography: Map design principles and styling
  • mapbox-token-security: Token management and security
  • mapbox-style-patterns: Common map style patterns
  • mapbox-cartography: 地图设计原则与样式配置
  • mapbox-token-security: 令牌管理与安全
  • mapbox-style-patterns: 常见地图样式方案

Resources

参考资源