关于 mapbox-integration-patterns
为在 Web 应用中集成 Mapbox GL JS 提供跨框架模式指南,面向开发者,涵盖生命周期管理、令牌配置和搜索集成。
主要功能
- 01React、Vue、Svelte 与 Angular 集成模式
- 02地图实例生命周期与清理
- 03环境变量令牌管理
- 04Mapbox Search JS 集成
- 05Next.js 与 SSR 注意事项
适用场景
- 01在 React 应用中添加交互式地图
- 02为不同前端框架实现地图初始化
- 03安全配置 Mapbox 访问令牌
- 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 集成都必须:
- 在正确的生命周期钩子中初始化地图
- 将地图实例存储在组件状态中(不要在每次渲染时重新创建)
- 清理时始终调用
map.remove(),以防止内存泄漏 - 安全地管理令牌(使用环境变量)
- 导入 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 管理模式
环境变量(推荐)
不同框架使用不同的前缀来设置客户端环境变量:
| 框架/打包器 | 环境变量 | 访问方式 |
|---|---|---|
| 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 文件:
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 公有 Tokenmap:地图实例(必须先初始化)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:常见地图样式模式