持续追踪
watchPosition 方法是 Geolocation API 提供的持续定位功能,它会自动追踪设备位置的变化,并在位置更新时调用回调函数。与 getCurrentPosition 只获取一次位置不同,watchPosition 适合需要实时跟踪用户移动轨迹的场景,如导航、运动记录、打车应用等。
前置知识
阅读本节前,建议先了解:获取位置信息
基础概念
watchPosition vs getCurrentPosition
| 特性 | getCurrentPosition | watchPosition |
|---|---|---|
| 获取次数 | 一次 | 持续 |
| 返回值 | 无 | watch ID |
| 适用场景 | 获取当前位置 | 实时追踪移动 |
| 位置更新 | 不更新 | 自动更新 |
| 停止方式 | 无需停止 | clearWatch() |
watchPosition 工作流程
调用 watchPosition()
│
├── 立即返回 watchId
│
├── 获取第一个位置 → 调用成功回调
│
├── 位置发生变化 → 再次调用成功回调
│
├── 位置继续变化 → 持续调用成功回调
│
└── 调用 clearWatch(watchId) → 停止追踪语法与 API
基本用法
javascript
// 开始持续追踪
const watchId = navigator.geolocation.watchPosition(
onSuccess, // 位置更新回调
onError, // 错误回调
options // 配置选项(与 getCurrentPosition 相同)
);
// 停止追踪
navigator.geolocation.clearWatch(watchId);
// 成功回调(每次位置更新时调用)
function onSuccess(position) {
console.log('位置更新:', position.coords.latitude, position.coords.longitude);
}
// 错误回调
function onError(error) {
console.error('追踪错误:', error.message);
}
// 配置选项
const options = {
enableHighAccuracy: true, // 高精度模式
timeout: 15000, // 超时时间
maximumAge: 0 // 不使用缓存
};watchPosition 返回值
javascript
// watchPosition 返回一个数字 ID
const watchId = navigator.geolocation.watchPosition(onSuccess, onError);
console.log('watch ID:', watchId); // 例如: 1
// 使用 ID 停止追踪
navigator.geolocation.clearWatch(watchId);详细说明
位置更新频率
watchPosition 的更新频率由浏览器和系统决定,开发者无法直接控制:
| 设备/环境 | 典型更新间隔 | 说明 |
|---|---|---|
| GPS(户外) | 1 秒 | 最频繁 |
| WiFi 定位 | 5-10 秒 | 中等频率 |
| 基站定位 | 10-30 秒 | 较低频率 |
| 设备静止 | 不更新 | 节省电量 |
| 浏览器后台 | 可能停止 | 取决于系统策略 |
同时使用多个 watch
javascript
// 可以同时创建多个 watch
const highAccuracyWatch = navigator.geolocation.watchPosition(
onHighAccuracy,
onError,
{ enableHighAccuracy: true, timeout: 15000 }
);
const lowAccuracyWatch = navigator.geolocation.watchPosition(
onLowAccuracy,
onError,
{ enableHighAccuracy: false, timeout: 5000 }
);
// 分别停止
function stopAllWatches() {
navigator.geolocation.clearWatch(highAccuracyWatch);
navigator.geolocation.clearWatch(lowAccuracyWatch);
}追踪轨迹记录
javascript
// 轨迹追踪器
const Tracker = {
watchId: null,
trackPoints: [], // 轨迹点
startTime: null, // 开始时间
totalDistance: 0, // 总距离
// 开始追踪
start(options = {}) {
this.trackPoints = [];
this.totalDistance = 0;
this.startTime = Date.now();
this.watchId = navigator.geolocation.watchPosition(
(position) => this.onPositionUpdate(position),
(error) => this.onError(error),
{
enableHighAccuracy: true,
timeout: 15000,
maximumAge: 0,
...options
}
);
console.log('开始追踪, watchId:', this.watchId);
},
// 位置更新处理
onPositionUpdate(position) {
const point = {
lat: position.coords.latitude,
lng: position.coords.longitude,
accuracy: position.coords.accuracy,
speed: position.coords.speed,
altitude: position.coords.altitude,
timestamp: position.timestamp
};
// 计算与上一个点的距离
if (this.trackPoints.length > 0) {
const last = this.trackPoints[this.trackPoints.length - 1];
const distance = this.calculateDistance(last.lat, last.lng, point.lat, point.lng);
// 只记录有效移动(过滤 GPS 抖动)
if (distance > 2 && distance < 100) {
this.totalDistance += distance;
this.trackPoints.push(point);
this.onUpdate(point, distance);
}
} else {
this.trackPoints.push(point);
this.onUpdate(point, 0);
}
},
// 更新回调(可覆盖)
onUpdate(point, distance) {
console.log(`新位置: ${point.lat.toFixed(6)}, ${point.lng.toFixed(6)} | 移动: ${distance.toFixed(1)}m`);
},
// 错误处理
onError(error) {
console.error('追踪错误:', error.message);
},
// 停止追踪
stop() {
if (this.watchId !== null) {
navigator.geolocation.clearWatch(this.watchId);
this.watchId = null;
console.log('追踪已停止');
console.log(`总距离: ${this.totalDistance.toFixed(0)} 米`);
console.log(`轨迹点数: ${this.trackPoints.length}`);
console.log(`总时间: ${((Date.now() - this.startTime) / 1000).toFixed(0)} 秒`);
}
},
// Haversine 距离计算
calculateDistance(lat1, lon1, lat2, lon2) {
const R = 6371000;
const dLat = (lat2 - lat1) * Math.PI / 180;
const dLon = (lon2 - lon1) * Math.PI / 180;
const a = Math.sin(dLat / 2) ** 2 +
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
Math.sin(dLon / 2) ** 2;
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
},
// 获取统计信息
getStats() {
const elapsed = (Date.now() - this.startTime) / 1000;
const avgSpeed = elapsed > 0 ? this.totalDistance / elapsed : 0;
return {
totalDistance: this.totalDistance,
totalPoints: this.trackPoints.length,
elapsedTime: elapsed,
averageSpeed: avgSpeed
};
}
};实战示例
移动轨迹追踪器
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>移动轨迹追踪</title>
<style>
body { font-family: -apple-system, sans-serif; padding: 20px; background: #f5f7fa; }
.tracker-card {
max-width: 600px;
margin: 0 auto;
background: white;
border-radius: 12px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
h2 { text-align: center; color: #1a1a2e; }
.controls {
display: flex;
gap: 10px;
margin: 20px 0;
}
.btn {
flex: 1;
padding: 12px;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
}
.btn-start { background: #4CAF50; color: white; }
.btn-stop { background: #F44336; color: white; }
.btn-stop:disabled { opacity: 0.5; cursor: not-allowed; }
.stats-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-top: 16px;
}
.stat-item {
text-align: center;
padding: 16px 8px;
background: #f8f9fa;
border-radius: 8px;
}
.stat-value {
font-size: 24px;
font-weight: 700;
color: #1a73e8;
font-family: monospace;
}
.stat-label {
font-size: 12px;
color: #888;
margin-top: 4px;
}
.trail-list {
margin-top: 20px;
max-height: 300px;
overflow-y: auto;
padding: 12px;
background: #1a1a2e;
border-radius: 8px;
font-family: monospace;
font-size: 12px;
color: #0f0;
}
.trail-list .entry {
padding: 2px 0;
border-bottom: 1px solid #333;
}
.current-pos {
margin-top: 16px;
padding: 16px;
background: #e3f2fd;
border-radius: 8px;
text-align: center;
}
.current-pos .coords {
font-size: 18px;
font-weight: 600;
color: #1565c0;
font-family: monospace;
}
</style>
</head>
<body>
<div class="tracker-card">
<h2>移动轨迹追踪</h2>
<div class="controls">
<button class="btn btn-start" id="startBtn" onclick="startTracking()">开始追踪</button>
<button class="btn btn-stop" id="stopBtn" onclick="stopTracking()" disabled>停止追踪</button>
</div>
<div class="current-pos" id="currentPos">
<div style="color:#888;">等待开始追踪...</div>
</div>
<div class="stats-grid">
<div class="stat-item">
<div class="stat-value" id="totalDist">0</div>
<div class="stat-label">总距离 (米)</div>
</div>
<div class="stat-item">
<div class="stat-value" id="pointCount">0</div>
<div class="stat-label">轨迹点数</div>
</div>
<div class="stat-item">
<div class="stat-value" id="elapsed">0</div>
<div class="stat-label">用时 (秒)</div>
</div>
</div>
<div class="trail-list" id="trailLog">
轨迹记录将在此显示...
</div>
</div>
<script>
let watchId = null;
let trackPoints = [];
let totalDistance = 0;
let startTime = null;
let timerInterval = null;
function startTracking() {
trackPoints = [];
totalDistance = 0;
startTime = Date.now();
document.getElementById('trailLog').innerHTML = '';
document.getElementById('startBtn').disabled = true;
document.getElementById('stopBtn').disabled = false;
// 更新计时器
timerInterval = setInterval(updateTimer, 1000);
watchId = navigator.geolocation.watchPosition(
onPositionUpdate,
(error) => {
addLog(`错误: ${error.message}`, '#f44');
stopTracking();
},
{ enableHighAccuracy: true, timeout: 15000, maximumAge: 0 }
);
addLog('开始追踪位置');
}
function onPositionUpdate(position) {
const c = position.coords;
const point = { lat: c.latitude, lng: c.longitude, time: position.timestamp };
// 更新当前位置显示
document.getElementById('currentPos').innerHTML = `
<div style="color:#1565c0;font-size:12px;">当前位置</div>
<div class="coords">${c.latitude.toFixed(6)}, ${c.longitude.toFixed(6)}</div>
<div style="color:#888;font-size:12px;margin-top:4px;">精度: ${c.accuracy.toFixed(0)}m</div>
`;
// 计算距离
if (trackPoints.length > 0) {
const last = trackPoints[trackPoints.length - 1];
const dist = haversine(last.lat, last.lng, point.lat, point.lng);
if (dist > 2 && dist < 100) { // 过滤抖动和异常
totalDistance += dist;
trackPoints.push(point);
addLog(`#${trackPoints.length} | ${point.lat.toFixed(6)}, ${point.lng.toFixed(6)} | +${dist.toFixed(1)}m | 总计: ${totalDistance.toFixed(0)}m`);
}
} else {
trackPoints.push(point);
addLog(`#${1} | ${point.lat.toFixed(6)}, ${point.lng.toFixed(6)} | 起点`);
}
// 更新统计
document.getElementById('totalDist').textContent = totalDistance.toFixed(0);
document.getElementById('pointCount').textContent = trackPoints.length;
}
function stopTracking() {
if (watchId !== null) {
navigator.geolocation.clearWatch(watchId);
watchId = null;
}
if (timerInterval) {
clearInterval(timerInterval);
timerInterval = null;
}
document.getElementById('startBtn').disabled = false;
document.getElementById('stopBtn').disabled = true;
addLog(`追踪结束 | 总距离: ${totalDistance.toFixed(0)}m | 点数: ${trackPoints.length}`);
}
function updateTimer() {
const elapsed = ((Date.now() - startTime) / 1000).toFixed(0);
document.getElementById('elapsed').textContent = elapsed;
}
function addLog(text, color = '#0f0') {
const log = document.getElementById('trailLog');
const time = new Date().toLocaleTimeString();
log.innerHTML += `<div class="entry" style="color:${color}">[${time}] ${text}</div>`;
log.scrollTop = log.scrollHeight;
}
function haversine(lat1, lon1, lat2, lon2) {
const R = 6371000;
const dLat = (lat2 - lat1) * Math.PI / 180;
const dLon = (lon2 - lon1) * Math.PI / 180;
const a = Math.sin(dLat / 2) ** 2 +
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
Math.sin(dLon / 2) ** 2;
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
</script>
</body>
</html>注意事项
- 电量消耗:持续使用 GPS 会显著增加耗电量,非必要时应及时
clearWatch - 后台限制:当页面不可见时,浏览器可能降低更新频率或暂停追踪
- GPS 抖动:静止时 GPS 数据会有轻微波动,需要过滤处理
- 内存管理:长时间追踪会积累大量轨迹点,注意控制存储
最佳实践
- 使用合理的最小距离阈值(如 2 米)过滤无效的位置更新
- 在页面不可见时暂停追踪(
visibilitychange事件) - 为轨迹点设置最大数量限制,超出后移除旧点
- 及时调用
clearWatch释放资源 - 持续追踪时提供清晰的 UI 反馈
下一节
继续学习:Worker 基础