轩辕李的博客 轩辕李的博客
首页
  • Java
  • Spring
  • 其他语言
  • 工具
  • JavaScript
  • TypeScript
  • Node.js
  • Vue.js
  • 前端工程化
  • 浏览器与Web API
  • 分布式
  • 代码质量管理
  • 基础
  • 操作系统
  • 计算机网络
  • 编程范式
  • 安全
  • 中间件
  • 心得
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

轩辕李

勇猛精进,星辰大海
首页
  • Java
  • Spring
  • 其他语言
  • 工具
  • JavaScript
  • TypeScript
  • Node.js
  • Vue.js
  • 前端工程化
  • 浏览器与Web API
  • 分布式
  • 代码质量管理
  • 基础
  • 操作系统
  • 计算机网络
  • 编程范式
  • 安全
  • 中间件
  • 心得
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • JavaScript

  • TypeScript

  • Node.js

  • Vue.js

  • 工程化

  • 浏览器与Web API

    • HTML基础-语义化标签与文档结构
    • HTML基础-文本与排版标签
    • HTML基础-列表与表格
    • HTML表单-Input类型详解
    • HTML表单-表单元素与验证
    • HTML交互-多媒体元素
    • HTML工程化-模板与组件化
    • HTML工程化-性能优化
    • CSS基础-选择器与优先级
    • CSS基础-盒模型与布局基础
    • CSS基础-单位与颜色系统
    • CSS基础-文本与字体
    • CSS基础-背景、列表与表格样式
    • CSS布局-Flexbox完全指南
    • CSS布局-Grid网格布局
    • CSS布局-响应式设计实践
    • CSS进阶-动画与过渡
    • CSS进阶-渐变与阴影效果
    • CSS进阶-Transform与3D变换
    • CSS进阶-滤镜与混合模式
    • 现代CSS-CSS预处理器对比
    • 现代CSS-CSS-in-JS方案
    • 现代CSS-原子化CSS与Tailwind
    • CSS工程化-架构与规范
    • CSS工程化-性能优化
    • CSS工程化-PostCSS实战指南
    • Web API基础-DOM操作完全指南
    • Web API基础-事件处理与委托
    • Web API基础-BOM与浏览器环境
    • Web API存储-客户端存储方案
    • Web API网络-HTTP请求详解
    • Web API网络-实时通信方案
    • Web API交互-用户体验增强
      • 一、全屏API
        • 1.1 基本使用
        • 1.2 监听全屏事件
        • 1.3 全屏样式
        • 1.4 实际应用
      • 二、通知API
        • 2.1 请求权限
        • 2.2 显示通知
        • 2.3 通知管理
        • 2.4 实际应用场景
      • 三、振动API
        • 3.1 基本使用
        • 3.2 实际应用
      • 四、地理定位API
        • 4.1 基本使用
        • 4.2 监听位置变化
        • 4.3 封装地理定位类
        • 4.4 实际应用
      • 五、综合示例
      • 六、最佳实践
        • 6.1 全屏API
        • 6.2 通知API
        • 6.3 振动API
        • 6.4 地理定位API
      • 七、总结
    • HTML&CSS历代版本新特性
  • 前端
  • 浏览器与Web API
轩辕李
2019-11-08
目录

Web API交互-用户体验增强

# Web API交互-用户体验增强

现代浏览器提供了丰富的API来增强用户体验。本文将介绍全屏API、通知API、振动API、地理定位API等实用技术,帮助开发者打造更好的交互体验。

# 一、全屏API

全屏API允许网页以全屏模式显示内容,常用于视频播放、游戏、演示等场景。

# 1.1 基本使用

const element = document.getElementById('video-container');

// 请求全屏
function requestFullscreen() {
  if (element.requestFullscreen) {
    element.requestFullscreen();
  } else if (element.webkitRequestFullscreen) { // Safari
    element.webkitRequestFullscreen();
  } else if (element.mozRequestFullScreen) { // Firefox
    element.mozRequestFullScreen();
  } else if (element.msRequestFullscreen) { // IE11
    element.msRequestFullscreen();
  }
}

// 退出全屏
function exitFullscreen() {
  if (document.exitFullscreen) {
    document.exitFullscreen();
  } else if (document.webkitExitFullscreen) {
    document.webkitExitFullscreen();
  } else if (document.mozCancelFullScreen) {
    document.mozCancelFullScreen();
  } else if (document.msExitFullscreen) {
    document.msExitFullscreen();
  }
}

// 切换全屏
function toggleFullscreen() {
  if (isFullscreen()) {
    exitFullscreen();
  } else {
    requestFullscreen();
  }
}

// 检查是否全屏
function isFullscreen() {
  return !!(
    document.fullscreenElement ||
    document.webkitFullscreenElement ||
    document.mozFullScreenElement ||
    document.msFullscreenElement
  );
}

// 获取全屏元素
function getFullscreenElement() {
  return (
    document.fullscreenElement ||
    document.webkitFullscreenElement ||
    document.mozFullScreenElement ||
    document.msFullscreenElement
  );
}

# 1.2 监听全屏事件

// 全屏状态改变
document.addEventListener('fullscreenchange', function() {
  if (isFullscreen()) {
    console.log('进入全屏模式');
    updateUI('fullscreen');
  } else {
    console.log('退出全屏模式');
    updateUI('normal');
  }
});

// 兼容性写法
document.addEventListener('fullscreenchange', handleFullscreenChange);
document.addEventListener('webkitfullscreenchange', handleFullscreenChange);
document.addEventListener('mozfullscreenchange', handleFullscreenChange);
document.addEventListener('MSFullscreenChange', handleFullscreenChange);

function handleFullscreenChange() {
  const fullscreenElement = getFullscreenElement();
  
  if (fullscreenElement) {
    console.log('全屏元素:', fullscreenElement);
  }
}

// 全屏错误
document.addEventListener('fullscreenerror', function(event) {
  console.error('全屏请求失败');
});

# 1.3 全屏样式

/* 全屏时的样式 */
:fullscreen {
  background-color: black;
}

:-webkit-full-screen {
  background-color: black;
}

:-moz-full-screen {
  background-color: black;
}

:-ms-fullscreen {
  background-color: black;
}

/* 全屏元素的子元素样式 */
:fullscreen video {
  width: 100%;
  height: 100%;
}

# 1.4 实际应用

// 视频播放器全屏
class VideoPlayer {
  constructor(videoElement) {
    this.video = videoElement;
    this.container = videoElement.parentElement;
    this.fullscreenBtn = this.container.querySelector('.fullscreen-btn');
    
    this.init();
  }
  
  init() {
    this.fullscreenBtn.addEventListener('click', () => {
      this.toggleFullscreen();
    });
    
    document.addEventListener('fullscreenchange', () => {
      this.updateFullscreenButton();
    });
    
    // ESC键退出全屏
    document.addEventListener('keydown', (e) => {
      if (e.key === 'Escape' && this.isFullscreen()) {
        this.exitFullscreen();
      }
    });
  }
  
  toggleFullscreen() {
    if (this.isFullscreen()) {
      this.exitFullscreen();
    } else {
      this.requestFullscreen();
    }
  }
  
  requestFullscreen() {
    const elem = this.container;
    
    if (elem.requestFullscreen) {
      elem.requestFullscreen();
    } else if (elem.webkitRequestFullscreen) {
      elem.webkitRequestFullscreen();
    }
  }
  
  exitFullscreen() {
    if (document.exitFullscreen) {
      document.exitFullscreen();
    } else if (document.webkitExitFullscreen) {
      document.webkitExitFullscreen();
    }
  }
  
  isFullscreen() {
    return !!(document.fullscreenElement || document.webkitFullscreenElement);
  }
  
  updateFullscreenButton() {
    if (this.isFullscreen()) {
      this.fullscreenBtn.textContent = '退出全屏';
    } else {
      this.fullscreenBtn.textContent = '全屏';
    }
  }
}

# 二、通知API

通知API允许网页向用户显示系统通知,即使页面不在前台也能提醒用户。

# 2.1 请求权限

// 检查浏览器支持
if (!('Notification' in window)) {
  console.log('浏览器不支持通知API');
}

// 检查权限状态
console.log(Notification.permission);
// "default" - 用户未做选择
// "granted" - 用户已授权
// "denied" - 用户已拒绝

// 请求权限
async function requestNotificationPermission() {
  if (Notification.permission === 'granted') {
    console.log('已有通知权限');
    return true;
  }
  
  if (Notification.permission === 'denied') {
    console.log('用户已拒绝通知权限');
    return false;
  }
  
  // 请求权限
  const permission = await Notification.requestPermission();
  
  if (permission === 'granted') {
    console.log('用户授予通知权限');
    return true;
  } else {
    console.log('用户拒绝通知权限');
    return false;
  }
}

# 2.2 显示通知

// 简单通知
function showSimpleNotification() {
  if (Notification.permission === 'granted') {
    new Notification('Hello!');
  }
}

// 带选项的通知
function showNotification() {
  if (Notification.permission !== 'granted') {
    console.log('没有通知权限');
    return;
  }
  
  const notification = new Notification('新消息', {
    body: '您有一条新消息待查看',
    icon: '/icon.png',
    badge: '/badge.png',
    image: '/image.png',
    tag: 'message-123',           // 相同tag会替换旧通知
    renotify: false,              // 相同tag时是否重新通知
    requireInteraction: false,    // 是否保持显示直到用户交互
    silent: false,                // 是否静音
    vibrate: [200, 100, 200],    // 振动模式
    data: { url: '/messages/123' }, // 自定义数据
    actions: [                    // 操作按钮(部分浏览器不支持)
      { action: 'view', title: '查看' },
      { action: 'close', title: '关闭' }
    ]
  });
  
  // 通知显示
  notification.addEventListener('show', () => {
    console.log('通知已显示');
  });
  
  // 通知点击
  notification.addEventListener('click', (event) => {
    console.log('通知被点击');
    window.focus();
    window.location.href = event.target.data.url;
    notification.close();
  });
  
  // 通知关闭
  notification.addEventListener('close', () => {
    console.log('通知已关闭');
  });
  
  // 通知错误
  notification.addEventListener('error', () => {
    console.error('通知显示失败');
  });
  
  // 3秒后自动关闭
  setTimeout(() => {
    notification.close();
  }, 3000);
}

# 2.3 通知管理

class NotificationManager {
  constructor() {
    this.notifications = new Map();
  }
  
  async init() {
    const granted = await this.requestPermission();
    return granted;
  }
  
  async requestPermission() {
    if (!('Notification' in window)) {
      return false;
    }
    
    if (Notification.permission === 'granted') {
      return true;
    }
    
    const permission = await Notification.requestPermission();
    return permission === 'granted';
  }
  
  show(id, title, options = {}) {
    if (Notification.permission !== 'granted') {
      console.log('没有通知权限');
      return null;
    }
    
    // 关闭相同ID的旧通知
    this.close(id);
    
    const notification = new Notification(title, {
      tag: id,
      ...options
    });
    
    this.notifications.set(id, notification);
    
    notification.addEventListener('click', () => {
      window.focus();
      
      if (options.onClick) {
        options.onClick(notification);
      }
      
      this.close(id);
    });
    
    notification.addEventListener('close', () => {
      this.notifications.delete(id);
      
      if (options.onClose) {
        options.onClose();
      }
    });
    
    return notification;
  }
  
  close(id) {
    const notification = this.notifications.get(id);
    
    if (notification) {
      notification.close();
      this.notifications.delete(id);
    }
  }
  
  closeAll() {
    this.notifications.forEach(notification => {
      notification.close();
    });
    this.notifications.clear();
  }
}

// 使用示例
const notificationManager = new NotificationManager();

await notificationManager.init();

notificationManager.show('msg-1', '新消息', {
  body: '您有一条新消息',
  icon: '/icon.png',
  onClick: () => {
    console.log('通知被点击');
    window.location.href = '/messages';
  }
});

# 2.4 实际应用场景

// 1. 新消息提醒
function notifyNewMessage(message) {
  notificationManager.show(`msg-${message.id}`, '新消息', {
    body: `${message.sender}: ${message.content}`,
    icon: message.avatar,
    onClick: () => {
      openChatWindow(message.sender);
    }
  });
}

// 2. 定时提醒
function setReminder(text, time) {
  const delay = time - Date.now();
  
  setTimeout(() => {
    notificationManager.show('reminder', '提醒', {
      body: text,
      requireInteraction: true,
      vibrate: [200, 100, 200]
    });
  }, delay);
}

// 3. 后台任务完成通知
async function processLargeFile(file) {
  const result = await uploadFile(file);
  
  notificationManager.show('upload', '上传完成', {
    body: `文件 ${file.name} 上传成功`,
    icon: '/success-icon.png',
    onClick: () => {
      window.location.href = result.url;
    }
  });
}

# 三、振动API

振动API允许网页触发设备振动,常用于移动设备的触觉反馈。

# 3.1 基本使用

// 检查支持
if ('vibrate' in navigator) {
  console.log('支持振动API');
} else {
  console.log('不支持振动API');
}

// 振动200毫秒
navigator.vibrate(200);

// 或使用数组
navigator.vibrate([200]);

// 振动模式:振动-暂停-振动
navigator.vibrate([100, 50, 100]);
// 振动100ms, 暂停50ms, 振动100ms

// 复杂振动模式
navigator.vibrate([200, 100, 200, 100, 200]);

// 停止振动
navigator.vibrate(0);
navigator.vibrate([]);

# 3.2 实际应用

// 1. 按钮点击反馈
button.addEventListener('click', () => {
  navigator.vibrate(10); // 轻微振动
});

// 2. 表单验证失败
function showError(message) {
  navigator.vibrate([50, 50, 50]); // 三次短振动
  alert(message);
}

// 3. 游戏碰撞反馈
function onCollision() {
  navigator.vibrate(100); // 碰撞时振动
}

// 4. 通知提醒
function showNotificationWithVibration(title, body) {
  navigator.vibrate([200, 100, 200]);
  
  new Notification(title, {
    body: body,
    vibrate: [200, 100, 200]
  });
}

// 5. 长按反馈
let longPressTimer;

element.addEventListener('touchstart', () => {
  longPressTimer = setTimeout(() => {
    navigator.vibrate(50); // 长按成功振动
    onLongPress();
  }, 500);
});

element.addEventListener('touchend', () => {
  clearTimeout(longPressTimer);
});

# 四、地理定位API

地理定位API允许网页获取用户的地理位置信息。

# 4.1 基本使用

// 检查支持
if ('geolocation' in navigator) {
  console.log('支持地理定位');
} else {
  console.log('不支持地理定位');
}

// 获取当前位置(一次性)
navigator.geolocation.getCurrentPosition(
  // 成功回调
  (position) => {
    console.log('纬度:', position.coords.latitude);
    console.log('经度:', position.coords.longitude);
    console.log('精度:', position.coords.accuracy, '米');
    console.log('海拔:', position.coords.altitude);
    console.log('海拔精度:', position.coords.altitudeAccuracy);
    console.log('方向:', position.coords.heading);
    console.log('速度:', position.coords.speed);
    console.log('时间戳:', position.timestamp);
  },
  // 错误回调
  (error) => {
    switch(error.code) {
      case error.PERMISSION_DENIED:
        console.error('用户拒绝地理定位请求');
        break;
      case error.POSITION_UNAVAILABLE:
        console.error('位置信息不可用');
        break;
      case error.TIMEOUT:
        console.error('请求超时');
        break;
      default:
        console.error('未知错误');
    }
  },
  // 选项
  {
    enableHighAccuracy: true,  // 高精度(耗电)
    timeout: 5000,             // 超时时间(毫秒)
    maximumAge: 0              // 缓存时间(0表示不使用缓存)
  }
);

# 4.2 监听位置变化

// 持续监听位置
const watchId = navigator.geolocation.watchPosition(
  (position) => {
    console.log('位置更新:', position.coords.latitude, position.coords.longitude);
    updateMap(position.coords);
  },
  (error) => {
    console.error('位置获取失败:', error.message);
  },
  {
    enableHighAccuracy: true,
    timeout: 10000,
    maximumAge: 5000
  }
);

// 停止监听
navigator.geolocation.clearWatch(watchId);

# 4.3 封装地理定位类

class GeoLocation {
  constructor(options = {}) {
    this.options = {
      enableHighAccuracy: false,
      timeout: 10000,
      maximumAge: 0,
      ...options
    };
    
    this.watchId = null;
  }
  
  // 检查支持
  isSupported() {
    return 'geolocation' in navigator;
  }
  
  // 获取当前位置
  getCurrentPosition() {
    return new Promise((resolve, reject) => {
      if (!this.isSupported()) {
        reject(new Error('浏览器不支持地理定位'));
        return;
      }
      
      navigator.geolocation.getCurrentPosition(
        (position) => {
          resolve({
            latitude: position.coords.latitude,
            longitude: position.coords.longitude,
            accuracy: position.coords.accuracy,
            altitude: position.coords.altitude,
            heading: position.coords.heading,
            speed: position.coords.speed,
            timestamp: position.timestamp
          });
        },
        (error) => {
          reject(this.handleError(error));
        },
        this.options
      );
    });
  }
  
  // 监听位置变化
  watchPosition(callback, errorCallback) {
    if (!this.isSupported()) {
      if (errorCallback) {
        errorCallback(new Error('浏览器不支持地理定位'));
      }
      return null;
    }
    
    this.watchId = navigator.geolocation.watchPosition(
      (position) => {
        callback({
          latitude: position.coords.latitude,
          longitude: position.coords.longitude,
          accuracy: position.coords.accuracy,
          timestamp: position.timestamp
        });
      },
      (error) => {
        if (errorCallback) {
          errorCallback(this.handleError(error));
        }
      },
      this.options
    );
    
    return this.watchId;
  }
  
  // 停止监听
  clearWatch() {
    if (this.watchId !== null) {
      navigator.geolocation.clearWatch(this.watchId);
      this.watchId = null;
    }
  }
  
  // 计算两点距离(米)
  calculateDistance(lat1, lon1, lat2, lon2) {
    const R = 6371e3; // 地球半径(米)
    const φ1 = lat1 * Math.PI / 180;
    const φ2 = lat2 * Math.PI / 180;
    const Δφ = (lat2 - lat1) * Math.PI / 180;
    const Δλ = (lon2 - lon1) * Math.PI / 180;
    
    const a = Math.sin(Δφ / 2) * Math.sin(Δφ / 2) +
              Math.cos(φ1) * Math.cos(φ2) *
              Math.sin(Δλ / 2) * Math.sin(Δλ / 2);
    
    const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
    
    return R * c;
  }
  
  // 错误处理
  handleError(error) {
    const errorMessages = {
      1: '用户拒绝地理定位请求',
      2: '位置信息不可用',
      3: '请求超时'
    };
    
    return new Error(errorMessages[error.code] || '未知错误');
  }
}

// 使用示例
const geoLocation = new GeoLocation({
  enableHighAccuracy: true,
  timeout: 10000
});

// 获取当前位置
try {
  const position = await geoLocation.getCurrentPosition();
  console.log('当前位置:', position.latitude, position.longitude);
} catch (error) {
  console.error('获取位置失败:', error.message);
}

// 监听位置变化
geoLocation.watchPosition(
  (position) => {
    console.log('位置更新:', position);
  },
  (error) => {
    console.error('位置获取失败:', error.message);
  }
);

# 4.4 实际应用

// 1. 附近搜索
async function searchNearby(keyword) {
  try {
    const position = await geoLocation.getCurrentPosition();
    
    const response = await fetch(`/api/search?keyword=${keyword}&lat=${position.latitude}&lng=${position.longitude}`);
    const results = await response.json();
    
    displayResults(results);
  } catch (error) {
    alert('无法获取位置信息');
  }
}

// 2. 导航
async function startNavigation(destination) {
  const position = await geoLocation.getCurrentPosition();
  
  // 监听位置变化
  geoLocation.watchPosition((newPosition) => {
    const distance = geoLocation.calculateDistance(
      newPosition.latitude,
      newPosition.longitude,
      destination.lat,
      destination.lng
    );
    
    if (distance < 10) {
      alert('已到达目的地');
      geoLocation.clearWatch();
    }
  });
}

// 3. 签到
async function checkIn() {
  try {
    const position = await geoLocation.getCurrentPosition();
    
    const response = await fetch('/api/checkin', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        latitude: position.latitude,
        longitude: position.longitude,
        accuracy: position.accuracy
      })
    });
    
    const result = await response.json();
    alert('签到成功');
  } catch (error) {
    alert('签到失败');
  }
}

# 五、综合示例

<html>
  <div id="ux-demo-l5m6n">
    <h3>用户体验增强综合示例</h3>
    
    <div class="section-l5m6n">
      <h4>全屏API</h4>
      <div class="fullscreen-container-l5m6n" id="fs-container-l5m6n">
        <div class="content-l5m6n">
          <p>这是全屏内容区域</p>
          <button id="fs-toggle-l5m6n">切换全屏</button>
        </div>
      </div>
      <div class="status-l5m6n" id="fs-status-l5m6n">状态: 正常模式</div>
    </div>
    
    <div class="section-l5m6n">
      <h4>通知API</h4>
      <div class="controls-l5m6n">
        <button id="notif-request-l5m6n">请求通知权限</button>
        <button id="notif-show-l5m6n" disabled>显示通知</button>
      </div>
      <div class="status-l5m6n" id="notif-status-l5m6n">权限状态: 未知</div>
    </div>
    
    <div class="section-l5m6n">
      <h4>振动API</h4>
      <div class="controls-l5m6n">
        <button id="vibrate-short-l5m6n">短振动</button>
        <button id="vibrate-pattern-l5m6n">振动模式</button>
        <button id="vibrate-stop-l5m6n">停止振动</button>
      </div>
      <div class="status-l5m6n" id="vibrate-status-l5m6n">
        支持: <span id="vibrate-support-l5m6n">检测中...</span>
      </div>
    </div>
    
    <div class="section-l5m6n">
      <h4>地理定位API</h4>
      <div class="controls-l5m6n">
        <button id="geo-get-l5m6n">获取位置</button>
        <button id="geo-watch-l5m6n">监听位置</button>
        <button id="geo-stop-l5m6n" disabled>停止监听</button>
      </div>
      <div class="geo-info-l5m6n" id="geo-info-l5m6n">
        <div>纬度: <span id="geo-lat-l5m6n">-</span></div>
        <div>经度: <span id="geo-lng-l5m6n">-</span></div>
        <div>精度: <span id="geo-acc-l5m6n">-</span></div>
      </div>
    </div>
  </div>
</html>

<style>
  #ux-demo-l5m6n {
    font-family: sans-serif;
    max-width: 800px;
  }
  
  .section-l5m6n {
    margin-bottom: 24px;
    padding: 20px;
    background: #f9f9f9;
    border-radius: 8px;
  }
  
  .section-l5m6n h4 {
    margin: 0 0 16px 0;
    color: #333;
    border-bottom: 2px solid #2196F3;
    padding-bottom: 8px;
  }
  
  .fullscreen-container-l5m6n {
    background: white;
    border: 2px solid #2196F3;
    border-radius: 8px;
    padding: 40px;
    text-align: center;
    margin-bottom: 12px;
  }
  
  .fullscreen-container-l5m6n:fullscreen {
    background: #1a1a1a;
    color: white;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  
  .fullscreen-container-l5m6n:-webkit-full-screen {
    background: #1a1a1a;
    color: white;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  
  .content-l5m6n p {
    font-size: 18px;
    margin-bottom: 20px;
  }
  
  .controls-l5m6n {
    display: flex;
    gap: 8px;
    margin-bottom: 12px;
    flex-wrap: wrap;
  }
  
  button {
    padding: 10px 20px;
    background: #2196F3;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-size: 14px;
    transition: background 0.2s;
  }
  
  button:hover:not(:disabled) {
    background: #1976D2;
  }
  
  button:disabled {
    background: #ccc;
    cursor: not-allowed;
  }
  
  .status-l5m6n {
    padding: 12px;
    background: white;
    border-radius: 4px;
    font-size: 14px;
  }
  
  .geo-info-l5m6n {
    background: white;
    padding: 16px;
    border-radius: 4px;
    display: grid;
    gap: 8px;
    font-size: 14px;
  }
  
  .geo-info-l5m6n span {
    color: #2196F3;
    font-weight: bold;
  }
</style>

<script>
  const fsContainer = document.getElementById('fs-container-l5m6n');
  const fsToggle = document.getElementById('fs-toggle-l5m6n');
  const fsStatus = document.getElementById('fs-status-l5m6n');
  
  fsToggle.addEventListener('click', () => {
    if (document.fullscreenElement) {
      document.exitFullscreen();
    } else {
      fsContainer.requestFullscreen().catch(err => {
        alert('无法进入全屏模式');
      });
    }
  });
  
  document.addEventListener('fullscreenchange', () => {
    if (document.fullscreenElement) {
      fsStatus.textContent = '状态: 全屏模式';
      fsToggle.textContent = '退出全屏';
    } else {
      fsStatus.textContent = '状态: 正常模式';
      fsToggle.textContent = '切换全屏';
    }
  });

  const notifRequest = document.getElementById('notif-request-l5m6n');
  const notifShow = document.getElementById('notif-show-l5m6n');
  const notifStatus = document.getElementById('notif-status-l5m6n');
  
  function updateNotifStatus() {
    if (!('Notification' in window)) {
      notifStatus.textContent = '权限状态: 浏览器不支持';
      return;
    }
    
    const permission = Notification.permission;
    notifStatus.textContent = `权限状态: ${
      permission === 'granted' ? '已授权' :
      permission === 'denied' ? '已拒绝' : '未决定'
    }`;
    
    notifShow.disabled = permission !== 'granted';
  }
  
  updateNotifStatus();
  
  notifRequest.addEventListener('click', async () => {
    if (!('Notification' in window)) {
      alert('浏览器不支持通知API');
      return;
    }
    
    const permission = await Notification.requestPermission();
    updateNotifStatus();
    
    if (permission === 'granted') {
      alert('已获得通知权限');
    } else {
      alert('未获得通知权限');
    }
  });
  
  notifShow.addEventListener('click', () => {
    const notification = new Notification('测试通知', {
      body: '这是一条测试通知消息',
      icon: 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="50" cy="50" r="40" fill="%232196F3"/></svg>'
    });
    
    notification.addEventListener('click', () => {
      window.focus();
      notification.close();
    });
    
    setTimeout(() => {
      notification.close();
    }, 3000);
  });

  const vibrateSupport = document.getElementById('vibrate-support-l5m6n');
  const vibrateStatus = document.getElementById('vibrate-status-l5m6n');
  
  if ('vibrate' in navigator) {
    vibrateSupport.textContent = '是';
    vibrateSupport.style.color = '#4caf50';
  } else {
    vibrateSupport.textContent = '否';
    vibrateSupport.style.color = '#f44336';
  }
  
  document.getElementById('vibrate-short-l5m6n').addEventListener('click', () => {
    if ('vibrate' in navigator) {
      navigator.vibrate(200);
      vibrateStatus.innerHTML = '支持: <span style="color: #4caf50;">已振动 200ms</span>';
    } else {
      alert('设备不支持振动API');
    }
  });
  
  document.getElementById('vibrate-pattern-l5m6n').addEventListener('click', () => {
    if ('vibrate' in navigator) {
      navigator.vibrate([100, 50, 100, 50, 100]);
      vibrateStatus.innerHTML = '支持: <span style="color: #4caf50;">振动模式执行中...</span>';
    } else {
      alert('设备不支持振动API');
    }
  });
  
  document.getElementById('vibrate-stop-l5m6n').addEventListener('click', () => {
    if ('vibrate' in navigator) {
      navigator.vibrate(0);
      vibrateStatus.innerHTML = '支持: <span style="color: #ff9800;">已停止振动</span>';
    }
  });

  let watchId = null;
  
  const geoGet = document.getElementById('geo-get-l5m6n');
  const geoWatch = document.getElementById('geo-watch-l5m6n');
  const geoStop = document.getElementById('geo-stop-l5m6n');
  const geoLat = document.getElementById('geo-lat-l5m6n');
  const geoLng = document.getElementById('geo-lng-l5m6n');
  const geoAcc = document.getElementById('geo-acc-l5m6n');
  
  function updatePosition(position) {
    geoLat.textContent = position.coords.latitude.toFixed(6);
    geoLng.textContent = position.coords.longitude.toFixed(6);
    geoAcc.textContent = position.coords.accuracy.toFixed(2) + ' 米';
  }
  
  function handleError(error) {
    const messages = {
      1: '用户拒绝定位请求',
      2: '位置信息不可用',
      3: '请求超时'
    };
    
    alert(messages[error.code] || '未知错误');
  }
  
  geoGet.addEventListener('click', () => {
    if (!('geolocation' in navigator)) {
      alert('浏览器不支持地理定位');
      return;
    }
    
    geoLat.textContent = '获取中...';
    geoLng.textContent = '获取中...';
    geoAcc.textContent = '获取中...';
    
    navigator.geolocation.getCurrentPosition(
      (position) => {
        updatePosition(position);
        
        setTimeout(() => {
          geoLat.textContent = '39.915 (模拟)';
          geoLng.textContent = '116.404 (模拟)';
          geoAcc.textContent = '20 米 (模拟)';
        }, 500);
      },
      handleError,
      { enableHighAccuracy: true, timeout: 5000 }
    );
  });
  
  geoWatch.addEventListener('click', () => {
    if (!('geolocation' in navigator)) {
      alert('浏览器不支持地理定位');
      return;
    }
    
    if (watchId !== null) {
      alert('已在监听位置');
      return;
    }
    
    geoWatch.disabled = true;
    geoStop.disabled = false;
    
    geoLat.textContent = '39.915 (模拟监听)';
    geoLng.textContent = '116.404 (模拟监听)';
    geoAcc.textContent = '20 米';
    
    alert('已开始监听位置(模拟)\n实际环境中会持续更新位置信息');
  });
  
  geoStop.addEventListener('click', () => {
    if (watchId !== null) {
      navigator.geolocation.clearWatch(watchId);
      watchId = null;
    }
    
    geoWatch.disabled = false;
    geoStop.disabled = true;
    
    geoLat.textContent = '-';
    geoLng.textContent = '-';
    geoAcc.textContent = '-';
    
    alert('已停止监听位置');
  });
</script>

# 六、最佳实践

# 6.1 全屏API

  1. 提供明显的全屏按钮
  2. 支持ESC键退出全屏
  3. 全屏状态UI适配
  4. 处理全屏失败情况
  5. 记住用户全屏偏好

# 6.2 通知API

  1. 在合适时机请求权限(不要页面加载时就请求)
  2. 提供清晰的权限说明
  3. 使用有意义的通知内容
  4. 设置合理的通知时长
  5. 相同类型通知使用相同tag避免堆积

# 6.3 振动API

  1. 振动时长不宜过长(影响用户体验)
  2. 提供关闭振动的选项
  3. 注意设备兼容性(主要是移动设备)
  4. 不要过度使用振动

# 6.4 地理定位API

  1. 说明为何需要位置信息
  2. 提供位置权限拒绝的降级方案
  3. 合理设置精度要求(高精度耗电)
  4. 使用缓存减少定位请求
  5. 处理定位失败情况

# 七、总结

用户体验增强API提供了丰富的交互能力:

  1. 全屏API:沉浸式体验,适合视频、游戏
  2. 通知API:主动提醒,增强用户参与
  3. 振动API:触觉反馈,提升交互体验
  4. 地理定位API:位置服务,支持本地化功能

合理使用这些API,能够显著提升用户体验和应用价值。

祝你变得更强!

编辑 (opens new window)
#Web API#用户体验#全屏#通知#地理定位
上次更新: 2025/11/28
Web API网络-实时通信方案
HTML&CSS历代版本新特性

← Web API网络-实时通信方案 HTML&CSS历代版本新特性→

最近更新
01
AI编程时代的一些心得
09-11
02
Claude Code与Codex的协同工作
09-01
03
Claude Code 最佳实践(个人版)
08-01
更多文章>
Theme by Vdoing | Copyright © 2018-2025 京ICP备2021021832号-2 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式