mapbox-web-integration-patterns by mapbox/mapbox-agent-skills
npx skills add https://github.com/mapbox/mapbox-agent-skills --skill mapbox-web-integration-patterns本技能提供了使用 React、Vue、Svelte、Angular 和原生 JavaScript 将 Mapbox GL JS 集成到 Web 应用程序中的官方模式。这些模式基于 Mapbox 的 create-web-app 脚手架工具,代表了可用于生产环境的最佳实践。
推荐版本: v3.x(最新版)
通过 npm 安装(推荐用于生产环境):
npm install mapbox-gl@^3.0.0 # 安装最新的 v3.x 版本
CDN(仅用于原型开发):
<!-- 将 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 - 以确保版本一致性和离线构建。
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
create-web-app 脚手架使用 React 19.xVue:
Svelte:
create-web-app 脚手架使用 Svelte 5.xAngular:
create-web-app 脚手架使用 Angular 19.xNext.js:
搜索集成所需:
npm install @mapbox/search-js-react@^1.0.0 # React
npm install @mapbox/search-js-web@^1.0.0 # 其他框架
从 v2.x 迁移到 v3.x:
optimizeForTerrain 选项令牌模式(适用于 v2.x 和 v3.x):
const token = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN; // 在生产环境中使用环境变量
// 全局令牌(自 v1.x 起可用)
mapboxgl.accessToken = token;
const map = new mapboxgl.Map({ container: '...' });
// 每个地图的令牌(多地图设置的首选)
const map = new mapboxgl.Map({
accessToken: token,
container: '...'
});
每个 Mapbox GL JS 集成必须:
map.remove() 以防止内存泄漏import 'mapbox-gl/dist/mapbox-gl.css'模式: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); // 存储地图实例
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' }} />;
}
要点:
useRefuseEffect 中使用空依赖数组 [] 进行初始化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' }} />
</>
);
}
模式: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
});
// 将地图实例分配给组件属性
this.map = map;
},
// 关键:在组件卸载时清理
unmounted() {
this.map.remove();
this.map = null;
}
};
</script>
<style>
.map-container {
width: 100%;
height: 100%;
}
</style>
要点:
mounted() 钩子中初始化this.$refs.mapContainer 访问容器this.mapunmounted() 钩子 来调用 map.remove()模式:onMount + onDestroy
<script>
import mapboxgl from 'mapbox-gl'
import 'mapbox-gl/dist/mapbox-gl.css'
import { onMount, onDestroy } from 'svelte'
let map
let mapContainer
onMount(() => {
map = new mapboxgl.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()accessToken 传递给 Map 构造函数模式: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> {
// 重要:检查是否在浏览器中运行(非 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):
<div #mapContainer style="height: 100vh; width: 100%"></div>
要点:
@ViewChild 引用地图容器isPlatformBrowser(支持 SSR)mapbox-gl 以避免 SSR 问题ngOnInit() 生命周期钩子中初始化ngOnDestroy() 来调用 map.remove()map.on('error', ...) 处理错误模式:使用初始化函数的模块导入
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:
<div id="map-container" style="height: 100vh;"></div>
要点:
模式:带有内联初始化的脚本标签
⚠️ 注意: 此模式仅用于原型开发。生产环境应用应使用 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>
<!-- 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 替换为特定版本(例如 3.7.0),来自 Mapbox 文档/latest/ - 始终固定到特定版本以确保一致性为什么生产环境不要使用 CDN?
npm install mapbox-gl@^3.0.0Web 组件是 W3C 标准,用于创建可重用的自定义元素,可在任何框架或无框架环境中工作。
何时使用 Web 组件:
实际示例: 一家拥有 React(主应用)、Vue(管理面板)和 Svelte(营销网站)的公司可以构建一个随处可用的 <mapbox-map> 组件。
何时改用特定框架模式:
💡 提示: 如果你正在使用 React、Vue、Svelte 或 Angular,请从上面的特定框架模式开始。它们更简单且集成更好。当你需要跨框架兼容性或正在构建原生 JavaScript 应用时,请使用 Web 组件。
模式:具有生命周期回调的标准自定义元素
Web 组件提供了一种使用 W3C Web 组件标准封装 Mapbox 地图的框架无关方式。
基础 Web 组件:
import mapboxgl from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
class MapboxMap extends HTMLElement {
constructor() {
super();
this.map = null;
}
connectedCallback() {
// 从属性获取配置
const token = this.getAttribute('access-token') || import.meta.env.VITE_MAPBOX_ACCESS_TOKEN;
const mapStyle = this.getAttribute('map-style') || 'mapbox://styles/mapbox/standard';
const center = this.getAttribute('center')?.split(',').map(Number) || [-71.05953, 42.3629];
const zoom = parseFloat(this.getAttribute('zoom')) || 13;
// 初始化地图
mapboxgl.accessToken = token;
this.map = new mapboxgl.Map({
container: this,
style: mapStyle,
center: center,
zoom: zoom
});
// 地图加载时派发自定义事件
this.map.on('load', () => {
this.dispatchEvent(
new CustomEvent('mapload', {
detail: { map: this.map }
})
);
});
}
// 关键:在元素被移除时清理
disconnectedCallback() {
if (this.map) {
this.map.remove();
this.map = null;
}
}
// 向 JavaScript 暴露地图实例
getMap() {
return this.map;
}
}
// 注册自定义元素
customElements.define('mapbox-map', MapboxMap);
在 HTML 中使用:
<!-- 基础用法 -->
<mapbox-map
access-token="pk.YOUR_TOKEN"
map-style="mapbox://styles/mapbox/dark-v11"
center="-122.4194,37.7749"
zoom="12"
></mapbox-map>
<style>
mapbox-map {
display: block;
height: 100vh;
width: 100%;
}
</style>
在 React 中使用:
import './mapbox-map-component'; // 导入以注册元素
function App() {
const mapRef = useRef(null);
useEffect(() => {
const handleMapLoad = (e) => {
const map = e.detail.map;
// 添加标记、图层等
new mapboxgl.Marker().setLngLat([-122.4194, 37.7749]).addTo(map);
};
mapRef.current?.addEventListener('mapload', handleMapLoad);
return () => {
mapRef.current?.removeEventListener('mapload', handleMapLoad);
};
}, []);
return (
<mapbox-map
ref={mapRef}
access-token={import.meta.env.VITE_MAPBOX_ACCESS_TOKEN}
map-style="mapbox://styles/mapbox/standard"
center="-122.4194,37.7749"
zoom="12"
/>
);
}
在 Vue 中使用:
<template>
<mapbox-map
ref="map"
:access-token="token"
map-style="mapbox://styles/mapbox/streets-v12"
center="-71.05953,42.3629"
zoom="13"
@mapload="handleMapLoad"
/>
</template>
<script>
import './mapbox-map-component';
export default {
data() {
return {
token: import.meta.env.VITE_MAPBOX_ACCESS_TOKEN
};
},
methods: {
handleMapLoad(event) {
const map = event.detail.map;
// 与地图交互
}
}
};
</script>
在 Svelte 中使用:
<script>
import './mapbox-map-component';
let mapElement;
function handleMapLoad(event) {
const map = event.detail.map;
// 与地图交互
}
</script>
<mapbox-map
bind:this={mapElement}
access-token={import.meta.env.VITE_MAPBOX_ACCESS_TOKEN}
map-style="mapbox://styles/mapbox/standard"
center="-71.05953,42.3629"
zoom="13"
on:mapload={handleMapLoad}
/>
高级:响应式属性模式:
class MapboxMapReactive extends HTMLElement {
static get observedAttributes() {
return ['center', 'zoom', 'map-style'];
}
constructor() {
super();
this.map = null;
}
connectedCallback() {
mapboxgl.accessToken = this.getAttribute('access-token');
this.map = new mapboxgl.Map({
container: this,
style: this.getAttribute('map-style') || 'mapbox://styles/mapbox/standard',
center: this.getAttribute('center')?.split(',').map(Number) || [0, 0],
zoom: parseFloat(this.getAttribute('zoom')) || 9
});
}
disconnectedCallback() {
if (this.map) {
this.map.remove();
this.map = null;
}
}
// 响应属性变化
attributeChangedCallback(name, oldValue, newValue) {
if (!this.map || oldValue === newValue) return;
switch (name) {
case 'center':
const center = newValue.split(',').map(Number);
this.map.setCenter(center);
break;
case 'zoom':
this.map.setZoom(parseFloat(newValue));
break;
case 'map-style':
this.map.setStyle(newValue);
break;
}
}
}
customElements.define('mapbox-map-reactive', MapboxMapReactive);
关键实现要点:
connectedCallback() 进行初始化(相当于 mount/ngOnInit)disconnectedCallback() 来调用 map.remove()(防止内存泄漏)mapload 等)observedAttributes + attributeChangedCallback 实现响应式更新不同的框架对客户端环境变量使用不同的前缀:
| 框架/打包工具 | 环境变量 | 访问模式 |
|---|---|---|
| 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.YOUR_MAPBOX_TOKEN_HERE
Next.js .env.local 文件:
NEXT_PUBLIC_MAPBOX_TOKEN=pk.YOUR_MAPBOX_TOKEN_HERE
重要事项:
.env 文件提交到版本控制.env 添加到 .gitignore.env.example 模板.gitignore:
.env
.env.local
.env.*.local
.env.example:
VITE_MAPBOX_ACCESS_TOKEN=your_token_here
安装依赖:
npm install @mapbox/search-js-react # React
npm install @mapbox/search-js-web # 原生/Vue/Svelte
注意: 两个包都包含 @mapbox/search-js-core 作为依赖项。只有在构建自定义搜索 UI 时才需要直接安装 -core。
React 搜索模式:
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:[lng, lat] 用于地理偏好搜索结果marker:布尔值,显示/隐藏结果标记placeholder:搜索框占位符文本绝对定位(覆盖层):
<div
style={{
position: 'absolute',
top: 10,
right: 10,
zIndex: 10,
width: 300
}}
>
<SearchBox {...props} />
</div>
常用位置:
top: 10px, right: 10pxtop: 10px, left: 10pxbottom: 10px, left: 10px// 错误 - 内存泄漏!
useEffect(() => {
const map = new mapboxgl.Map({ ... })
// 没有清理函数
}, [])
// 正确 - 正确清理
useEffect(() => {
const map = new mapboxgl.Map({ ... })
return () => map.remove() // ✅ 清理
}, [])
原因: 每个 Map 实例都会创建 WebGL 上下文、事件监听器和 DOM 节点。如果不清理,这些会累积并导致内存泄漏。
// 错误 - React 中的无限循环!
function MapComponent() {
const map = new mapboxgl.Map({ ... }) // 每次渲染都运行
return <div />
}
// 正确 - 在 effect 中初始化
function MapComponent() {
useEffect(() => {
const map = new mapboxgl.Map({ ... })
}, [])
return <div />
}
原因: React 组件会频繁重新渲染。每次渲染都创建新地图会导致无限循环和崩溃。
// 错误 - map 变量在渲染之间丢失
function MapComponent() {
useEffect(() => {
let map = new mapboxgl.Map({ ... })
// map 变量之后无法访问
}, [])
}
// 正确 - 存储在 useRef 中
function MapComponent() {
const mapRef = useRef()
useEffect(() => {
mapRef.current = new mapboxgl.Map({ ... })
// mapRef.current 在整个组件中都可访问
}, [])
}
原因: 你需要访问地图实例来执行添加图层、标记或调用 remove() 等操作。
// 错误 - 每次渲染都重新创建地图
useEffect(() => {
const map = new mapboxgl.Map({ ... })
return () => map.remove()
}) // 没有依赖数组
// 错误 - props 改变时重新创建地图
useEffect(() => {
const map = new mapboxgl.Map({ center: props.center, ... })
return () => map.remove()
}, [props.center])
// 正确 - 初始化一次
useEffect(() => {
const map = new mapboxgl.Map({ ... })
return () => map.remove()
}, []) // 空数组 = 运行一次
// 正确 - 改为更新地图属性
useEffect(() => {
if (mapRef.current) {
mapRef.current.setCenter(props.center)
}
}, [props.center])
原因: 地图初始化开销很大。初始化一次,然后使用地图方法来更新属性。
// 错误 - 令牌在源代码中暴露
mapboxgl.accessToken = 'pk.YOUR_MAPBOX_TOKEN_HERE';
// 正确 - 使用环境变量
mapboxgl.accessToken = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN;
原因: 源代码中的令牌会被提交到版本控制并公开暴露。始终使用环境变量。
// 错误 - 在服务器端渲染期间崩溃
ngOnInit() {
import('mapbox-gl').then(mapboxgl => {
this.map = new mapboxgl.Map({ ... })
})
}
// 正确 - 先检查平台
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)会崩溃。
// 错误 - 地图渲染但样式损坏
import mapboxgl from 'mapbox-gl';
// 缺少 CSS 导入
// 正确 - 导入 CSS 以获得正确样式
import mapboxgl from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
原因: CSS 文件包含地图控件、弹出窗口和标记的关键样式。没有它,地图会显示异常。
'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_* 访问环境变量mapRefimport 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示例默认值(在 create-web-app 演示中使用):
[-71.05953, 42.36290](波士顿,马萨诸塞州)13 用于城市级别视图注意: 如果未指定,GL JS 默认为
center: [0, 0]和zoom: 0。请始终明确设置这些值。
缩放级别指南:
0-2:世界视图3-5:大陆/国家6-9:区域/州10-12:城市视图13-15:街区16-18:街道级别19-22:建筑级别为用户位置自定义:
// 使用浏览器地理位置
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition((position) => {
map.setCenter([position.coords.longitude, position.coords.latitude]);
map.setZoom(13);
});
}
模拟 mapbox-gl:
// vitest.config.js 或 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。模拟该库以测试组件逻辑。
在以下情况下调用此技能:
This skill provides official patterns for integrating Mapbox GL JS into web applications using React, Vue, Svelte, Angular, and vanilla JavaScript. These patterns are based on Mapbox's create-web-app scaffolding tool and represent production-ready best practices.
Recommended: v3.x (latest)
Installing via npm (recommended for production):
npm install mapbox-gl@^3.0.0 # Installs latest v3.x
CDN (for prototyping only):
<!-- 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.
React:
create-web-app scaffolds with React 19.xVue:
Svelte:
create-web-app scaffolds with Svelte 5.xAngular:
create-web-app scaffolds with Angular 19.xNext.js:
Required for search integration:
npm install @mapbox/search-js-react@^1.0.0 # React
npm install @mapbox/search-js-web@^1.0.0 # Other frameworks
Migrating from v2.x to v3.x:
optimizeForTerrain option removedToken patterns (work in v2.x and v3.x):
const token = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN; // Use env vars in production
// Global token (works since v1.x)
mapboxgl.accessToken = token;
const map = new mapboxgl.Map({ container: '...' });
// Per-map token (preferred for multi-map setups)
const map = new mapboxgl.Map({
accessToken: token,
container: '...'
});
Every Mapbox GL JS integration must:
map.remove() on cleanup to prevent memory leaksimport 'mapbox-gl/dist/mapbox-gl.css'Pattern: useRef + useEffect with cleanup
Note: These examples use Vite (the bundler used in
create-web-app). If using Create React App, replaceimport.meta.env.VITE_MAPBOX_ACCESS_TOKENwithprocess.env.REACT_APP_MAPBOX_TOKEN. See the Token Management Patterns section for other bundlers.
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:
useRef for both map instance and containeruseEffect with empty deps []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' }} />
</>
);
}
Pattern: mounted + unmounted lifecycle hooks
<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:
mounted() hookthis.$refs.mapContainerthis.mapunmounted() hook to call map.remove()Pattern: onMount + onDestroy
<script>
import mapboxgl from 'mapbox-gl'
import 'mapbox-gl/dist/mapbox-gl.css'
import { onMount, onDestroy } from 'svelte'
let map
let mapContainer
onMount(() => {
map = new mapboxgl.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:
onMount for initializationbind:this={mapContainer}onDestroy to call map.remove()accessToken directly to Map constructor in SveltePattern: ngOnInit + ngOnDestroy with SSR handling
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):
<div #mapContainer style="height: 100vh; width: 100%"></div>
Key points:
@ViewChild to reference map containerisPlatformBrowser before initializing (SSR support)mapbox-gl to avoid SSR issuesngOnInit() lifecycle hookngOnDestroy() to call map.remove()map.on('error', ...)Pattern: Module imports with initialization function
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>
Key points:
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.
<!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:
3.x.x with specific version (e.g., 3.7.0) from Mapbox docs/latest/ - always pin to specific version for consistencyWhy not CDN for production?
npm install mapbox-gl@^3.0.0Web Components are a W3C standard for creating reusable custom elements that work in any framework or no framework at all.
When to use Web Components:
Real-world example: A company with React (main app), Vue (admin panel), and Svelte (marketing site) can build one <mapbox-map> component that works everywhere.
When to use framework-specific patterns instead:
💡 Tip: If you're using React, Vue, Svelte, or Angular, start with the framework-specific patterns above. They're simpler and better integrated. Use Web Components when you need cross-framework compatibility or are building vanilla JavaScript apps.
Pattern: Standard Custom Element with lifecycle callbacks
Web Components provide a framework-agnostic way to encapsulate Mapbox maps using the W3C Web Components standard.
Basic Web Component:
import mapboxgl from 'mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
class MapboxMap extends HTMLElement {
constructor() {
super();
this.map = null;
}
connectedCallback() {
// Get configuration from attributes
const token = this.getAttribute('access-token') || import.meta.env.VITE_MAPBOX_ACCESS_TOKEN;
const mapStyle = this.getAttribute('map-style') || 'mapbox://styles/mapbox/standard';
const center = this.getAttribute('center')?.split(',').map(Number) || [-71.05953, 42.3629];
const zoom = parseFloat(this.getAttribute('zoom')) || 13;
// Initialize map
mapboxgl.accessToken = token;
this.map = new mapboxgl.Map({
container: this,
style: mapStyle,
center: center,
zoom: zoom
});
// Dispatch custom event when map loads
this.map.on('load', () => {
this.dispatchEvent(
new CustomEvent('mapload', {
detail: { map: this.map }
})
);
});
}
// CRITICAL: Clean up when element is removed
disconnectedCallback() {
if (this.map) {
this.map.remove();
this.map = null;
}
}
// Expose map instance to JavaScript
getMap() {
return this.map;
}
}
// Register the custom element
customElements.define('mapbox-map', MapboxMap);
Usage in HTML:
<!-- Basic usage -->
<mapbox-map
access-token="pk.YOUR_TOKEN"
map-style="mapbox://styles/mapbox/dark-v11"
center="-122.4194,37.7749"
zoom="12"
></mapbox-map>
<style>
mapbox-map {
display: block;
height: 100vh;
width: 100%;
}
</style>
Usage in React:
import './mapbox-map-component'; // Import to register element
function App() {
const mapRef = useRef(null);
useEffect(() => {
const handleMapLoad = (e) => {
const map = e.detail.map;
// Add markers, layers, etc.
new mapboxgl.Marker().setLngLat([-122.4194, 37.7749]).addTo(map);
};
mapRef.current?.addEventListener('mapload', handleMapLoad);
return () => {
mapRef.current?.removeEventListener('mapload', handleMapLoad);
};
}, []);
return (
<mapbox-map
ref={mapRef}
access-token={import.meta.env.VITE_MAPBOX_ACCESS_TOKEN}
map-style="mapbox://styles/mapbox/standard"
center="-122.4194,37.7749"
zoom="12"
/>
);
}
Usage in Vue:
<template>
<mapbox-map
ref="map"
:access-token="token"
map-style="mapbox://styles/mapbox/streets-v12"
center="-71.05953,42.3629"
zoom="13"
@mapload="handleMapLoad"
/>
</template>
<script>
import './mapbox-map-component';
export default {
data() {
return {
token: import.meta.env.VITE_MAPBOX_ACCESS_TOKEN
};
},
methods: {
handleMapLoad(event) {
const map = event.detail.map;
// Interact with map
}
}
};
</script>
Usage in Svelte:
<script>
import './mapbox-map-component';
let mapElement;
function handleMapLoad(event) {
const map = event.detail.map;
// Interact with map
}
</script>
<mapbox-map
bind:this={mapElement}
access-token={import.meta.env.VITE_MAPBOX_ACCESS_TOKEN}
map-style="mapbox://styles/mapbox/standard"
center="-71.05953,42.3629"
zoom="13"
on:mapload={handleMapLoad}
/>
Advanced: Reactive Attributes Pattern:
class MapboxMapReactive extends HTMLElement {
static get observedAttributes() {
return ['center', 'zoom', 'map-style'];
}
constructor() {
super();
this.map = null;
}
connectedCallback() {
mapboxgl.accessToken = this.getAttribute('access-token');
this.map = new mapboxgl.Map({
container: this,
style: this.getAttribute('map-style') || 'mapbox://styles/mapbox/standard',
center: this.getAttribute('center')?.split(',').map(Number) || [0, 0],
zoom: parseFloat(this.getAttribute('zoom')) || 9
});
}
disconnectedCallback() {
if (this.map) {
this.map.remove();
this.map = null;
}
}
// React to attribute changes
attributeChangedCallback(name, oldValue, newValue) {
if (!this.map || oldValue === newValue) return;
switch (name) {
case 'center':
const center = newValue.split(',').map(Number);
this.map.setCenter(center);
break;
case 'zoom':
this.map.setZoom(parseFloat(newValue));
break;
case 'map-style':
this.map.setStyle(newValue);
break;
}
}
}
customElements.define('mapbox-map-reactive', MapboxMapReactive);
Key Implementation Points:
connectedCallback() for initialization (equivalent to mount/ngOnInit)disconnectedCallback() to call map.remove() (prevents memory leaks)mapload, etc.)observedAttributes + attributeChangedCallback for reactive updatesDifferent frameworks use different prefixes for client-side environment variables:
| Framework/Bundler | Environment Variable | Access 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 |
Vite .env file:
VITE_MAPBOX_ACCESS_TOKEN=pk.YOUR_MAPBOX_TOKEN_HERE
Next.js .env.local file:
NEXT_PUBLIC_MAPBOX_TOKEN=pk.YOUR_MAPBOX_TOKEN_HERE
Important:
.env files to version control.env to .gitignore.env.example template for team.gitignore:
.env
.env.local
.env.*.local
.env.example:
VITE_MAPBOX_ACCESS_TOKEN=your_token_here
Install dependency:
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:
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 tokenmap: Map instance (must be initialized first)mapboxgl: The mapboxgl library referenceproximity: [lng, lat] to bias results geographicallymarker: Boolean to show/hide result markerplaceholder: Search box placeholder textAbsolute positioning (overlay):
<div
style={{
position: 'absolute',
top: 10,
right: 10,
zIndex: 10,
width: 300
}}
>
<SearchBox {...props} />
</div>
Common positions:
top: 10px, right: 10pxtop: 10px, left: 10pxbottom: 10px, left: 10px// 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
}, [])
Why: Every Map instance creates WebGL contexts, event listeners, and DOM nodes. Without cleanup, these accumulate and cause memory leaks.
// 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 />
}
Why: React components re-render frequently. Creating a new map on every render causes infinite loops and crashes.
// 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
}, [])
}
Why: You need to access the map instance for operations like adding layers, markers, or calling remove().
// 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])
Why: Map initialization is expensive. Initialize once, then use map methods to update properties.
// BAD - Token exposed in source code
mapboxgl.accessToken = 'pk.YOUR_MAPBOX_TOKEN_HERE';
// 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.
// 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({ ... })
})
}
Why: Mapbox GL JS requires browser APIs (WebGL, Canvas). Angular Universal (SSR) will crash without platform check.
// 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';
Why: The CSS file contains critical styles for map controls, popups, and markers. Without it, the map appears broken.
'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:
'use client' directive (maps require browser APIs)process.env.NEXT_PUBLIC_* for environment variablesmapRef properly with TypeScriptimport 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:
dynamic import with ssr: falseExample defaults (used in create-web-app demos):
[-71.05953, 42.36290] (Boston, MA)13 for city-level viewNote: GL JS defaults to
center: [0, 0]andzoom: 0if not specified. Always set these explicitly.
Zoom level guide:
0-2: World view3-5: Continent/country6-9: Region/state10-12: City view13-15: Neighborhood16-18: Street level19-22: Building levelCustomizing for user location:
// Use browser geolocation
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition((position) => {
map.setCenter([position.coords.longitude, position.coords.latitude]);
map.setZoom(13);
});
}
Mock 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: ''
}
}));
Why: Mapbox GL JS requires WebGL and browser APIs that don't exist in test environments. Mock the library to test component logic.
Invoke this skill when:
Weekly Installs
400
Repository
GitHub Stars
35
First Seen
Jan 31, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykWarn
Installed on
codex364
opencode349
gemini-cli346
github-copilot340
amp324
kimi-cli324
Vue.js测试最佳实践:Vue 3组件、组合式函数、Pinia与异步测试完整指南
3,700 周安装
| Angular | environment.mapboxAccessToken | Environment files (environment.ts) |