页面可见性与设备状态 API

深入理解 Page Visibility API、DeviceOrientation、Screen Orientation 等设备状态 API,实现智能的页面行为控制。

页面可见性与设备状态 API(Page Visibility and Device State APIs)

一、Page Visibility API

1.1 为什么需要检测页面可见性

页面可见性 API 允许检测页面是否对用户可见。这对于优化性能和资源使用非常重要:

  • 暂停视频/音频:用户切换标签页时暂停播放
  • 暂停轮询:页面不可见时停止数据轮询
  • 暂停动画:节省 CPU 和电池
  • 记录分析:准确计算用户真实浏览时间
// 检测页面是否可见
console.log('Visibility state:', document.visibilityState);
// 'visible' - 页面完全可见
// 'hidden' - 页面完全不可见
// 'prerender' - 页面正在预渲染(很少使用)

console.log('Hidden:', document.hidden);
// true 表示页面不可见

1.2 visibilitychange 事件

document.addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'visible') {
    console.log('Page is now visible');
    // 恢复视频播放
    // 重新开始数据轮询
    // 恢复动画
  } else {
    console.log('Page is now hidden');
    // 暂停视频播放
    // 停止数据轮询
    // 暂停动画
  }
});

1.3 实际应用场景

class VisibilityAwarePlayer {
  constructor(video) {
    this.video = video;
    this.wasPlaying = false;

    document.addEventListener('visibilitychange', () => {
      this.handleVisibilityChange();
    });
  }

  handleVisibilityChange() {
    if (document.visibilityState === 'visible') {
      // 页面可见时恢复播放
      if (this.wasPlaying) {
        this.video.play();
      }
    } else {
      // 页面不可见时暂停
      this.wasPlaying = !this.video.paused;
      this.video.pause();
    }
  }
}

// 使用
const player = new VisibilityAwarePlayer(document.getElementById('video'));

二、DeviceOrientationEvent

2.1 设备方向检测

// 检测支持
if ('DeviceOrientationEvent' in window) {
  console.log('Device orientation supported');
}

// 添加监听器
window.addEventListener('deviceorientation', (event) => {
  console.log('Alpha (Z轴):', event.alpha);  // 绕 Z 轴旋转 0-360
  console.log('Beta (X轴):', event.beta);   // 绕 X 轴旋转 -180 到 180
  console.log('Gamma (Y轴):', event.gamma); // 绕 Y 轴旋转 -90 到 90
});

2.2 设备方向坐标系

设备坐标系:
    ┌───┐
    │ ▲ │  前面
    └───┘
      │ Z
      │
      │
  ────┼──── X(向右)
      │
      │
      ↓ Y(向下)

Beta  = 前后倾斜(绕 X 轴)
Gamma = 左右倾斜(绕 Y 轴)
Alpha = 指南针方向(绕 Z 轴,相对于磁北)

2.3 实际应用:虚拟现实视角

class OrientationController {
  constructor(element) {
    this.element = element;
    this.rotation = { x: 0, y: 0, z: 0 };

    this.init();
  }

  init() {
    window.addEventListener('deviceorientation', (event) => {
      this.handleOrientation(event);
    });

    // iOS 13+ 需要请求权限
    if (typeof DeviceOrientationEvent.requestPermission === 'function') {
      this.requestPermission();
    }
  }

  async requestPermission() {
    try {
      const permission = await DeviceOrientationEvent.requestPermission();
      if (permission === 'granted') {
        window.addEventListener('deviceorientation', (event) => {
          this.handleOrientation(event);
        });
      }
    } catch (error) {
      console.error('Permission denied:', error);
    }
  }

  handleOrientation(event) {
    // 更新 3D 视角
    this.rotation.x = event.beta || 0;
    this.rotation.y = event.gamma || 0;
    this.rotation.z = event.alpha || 0;

    this.updateView();
  }

  updateView() {
    // 应用 CSS 3D 变换
    this.element.style.transform = `
      rotateX(${this.rotation.x}deg)
      rotateY(${this.rotation.y}deg)
      rotateZ(${this.rotation.z}deg)
    `;
  }
}

三、Screen Orientation API

3.1 获取屏幕方向

// 获取屏幕方向
if ('orientation' in screen) {
  console.log('Orientation type:', screen.orientation.type);
  // 'portrait-primary' - 竖屏主方向
  // 'portrait-secondary' - 竖屏次方向
  // 'landscape-primary' - 横屏主方向
  // 'landscape-secondary' - 横屏次方向
}

// 使用 matchMedia 检测
const isLandscape = window.matchMedia('(orientation: landscape)');
console.log('Is landscape:', isLandscape.matches);

// 监听方向变化
screen.orientation.addEventListener('change', () => {
  console.log('New orientation:', screen.orientation.type);
});

3.2 锁定屏幕方向

// 锁定为竖屏
async function lockPortrait() {
  try {
    await screen.orientation.lock('portrait');
    console.log('Locked to portrait');
  } catch (error) {
    console.error('Lock failed:', error);
  }
}

// 锁定为横屏
async function lockLandscape() {
  try {
    await screen.orientation.lock('landscape');
    console.log('Locked to landscape');
  } catch (error) {
    console.error('Lock failed:', error);
  }
}

// 解锁
function unlock() {
  screen.orientation.unlock();
}

四、Battery Status API

4.1 获取电池信息

// 获取电池信息
async function getBatteryInfo() {
  const battery = await navigator.getBattery();

  console.log('Charging:', battery.charging);
  console.log('Level:', battery.level * 100 + '%');
  console.log('Charging time:', battery.chargingTime, 'seconds');
  console.log('Discharging time:', battery.dischargingTime, 'seconds');
}

// 添加事件监听
battery.addEventListener('chargingchange', () => {
  console.log('Charging changed:', battery.charging);
});

battery.addEventListener('levelchange', () => {
  console.log('Level changed:', battery.level);
});

battery.addEventListener('chargingtimechange', () => {
  console.log('Charging time changed');
});

battery.addEventListener('dischargingtimechange', () => {
  console.log('Discharging time changed');
});

4.2 实际应用

class BatteryAwarePlayer {
  constructor(video) {
    this.video = video;
    this.lowBatteryThreshold = 0.2; // 20%

    this.init();
  }

  async init() {
    const battery = await navigator.getBattery();

    // 监听电量变化
    battery.addEventListener('levelchange', () => {
      this.adjustQuality(battery.level);
    });

    // 监听充电状态
    battery.addEventListener('chargingchange', () => {
      if (battery.charging) {
        this.setHighQuality();
      } else {
        this.adjustQuality(battery.level);
      }
    });
  }

  adjustQuality(level) {
    if (level < this.lowBatteryThreshold) {
      // 低电量,降低画质
      this.video.setPlaybackQuality('low');
    } else {
      this.video.setPlaybackQuality('auto');
    }
  }

  setHighQuality() {
    this.video.setPlaybackQuality('high');
  }
}

五、网络状态 API

5.1 Network Information API

// 获取连接信息
if ('connection' in navigator) {
  const connection = navigator.connection;

  console.log('Effective type:', connection.effectiveType);
  // 'slow-2g' - 2G 网络
  // '2g' - 2G 网络
  // '3g' - 3G 网络
  // '4g' - 4G 网络

  console.log('Downlink:', connection.downlink, 'Mbps');
  console.log('RTT:', connection.rtt, 'ms');
  console.log('Save data:', connection.saveData);
}

// 监听连接变化
connection.addEventListener('change', () => {
  console.log('Connection changed');
  console.log('New type:', connection.effectiveType);
});

5.2 实际应用

class NetworkAwareMedia {
  constructor(video) {
    this.video = video;

    this.init();
  }

  init() {
    const connection = navigator.connection;

    if (!connection) return;

    // 根据网络质量调整媒体质量
    connection.addEventListener('change', () => {
      this.adaptQuality();
    });

    // 初始调整
    this.adaptQuality();
  }

  adaptQuality() {
    const connection = navigator.connection;
    const effectiveType = connection.effectiveType;

    switch (effectiveType) {
      case 'slow-2g':
      case '2g':
        this.video.setPlaybackRate(0.5);
        this.video.setResolution('low');
        break;
      case '3g':
        this.video.setPlaybackRate(1);
        this.video.setResolution('medium');
        break;
      case '4g':
        this.video.setPlaybackRate(1);
        this.video.setResolution('high');
        break;
    }
  }
}

六、综合示例:智能资源管理

class SmartResourceManager {
  constructor() {
    this.isVisible = true;
    this.isOnBattery = false;
    this.isSlowNetwork = false;

    this.init();
  }

  async init() {
    // 页面可见性
    document.addEventListener('visibilitychange', () => {
      this.isVisible = document.visibilityState === 'visible';
      this.updateBehavior();
    });

    // 电池状态
    if ('getBattery' in navigator) {
      const battery = await navigator.getBattery();
      this.isOnBattery = !battery.charging;

      battery.addEventListener('chargingchange', () => {
        this.isOnBattery = !battery.charging;
        this.updateBehavior();
      });
    }

    // 网络状态
    if ('connection' in navigator) {
      const connection = navigator.connection;
      this.isSlowNetwork = ['slow-2g', '2g'].includes(connection.effectiveType);

      connection.addEventListener('change', () => {
        this.isSlowNetwork = ['slow-2g', '2g'].includes(connection.effectiveType);
        this.updateBehavior();
      });
    }
  }

  updateBehavior() {
    // 决定是否加载高清图片
    const loadHighResImages = this.isVisible &&
                              !this.isOnBattery &&
                              !this.isSlowNetwork;

    // 决定是否自动播放视频
    const autoplayVideo = this.isVisible &&
                          !this.isOnBattery &&
                          !this.isSlowNetwork;

    // 决定轮询频率
    const pollingInterval = this.isVisible ? 5000 : 60000;

    console.log({
      loadHighResImages,
      autoplayVideo,
      pollingInterval
    });

    // 触发自定义事件
    window.dispatchEvent(new CustomEvent('resource-policy-change', {
      detail: { loadHighResImages, autoplayVideo, pollingInterval }
    }));
  }
}

// 使用
const manager = new SmartResourceManager();

参考资料

延展阅读