JavaScript Vibration API 完全指南

掌握设备振动控制:触觉反馈、游戏体验、通知提醒与交互增强开发

JavaScript Vibration API 完全指南

Vibration API 允许网页控制设备的振动功能。本文详解其使用方法和实战应用。

基础概念

检测支持

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

// 安全的振动函数
function vibrate(pattern) {
  if ('vibrate' in navigator) {
    return navigator.vibrate(pattern);
  }
  return false;
}

简单振动

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

// 振动 1 秒
navigator.vibrate(1000);

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

振动模式

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

// SOS 摩尔斯电码
// ... --- ... (3短 3长 3短)
const sos = [
  100, 50, 100, 50, 100, // S: ...
  200,                    // 间隔
  300, 50, 300, 50, 300, // O: ---
  200,                    // 间隔
  100, 50, 100, 50, 100  // S: ...
];
navigator.vibrate(sos);

// 心跳模式
const heartbeat = [100, 100, 100, 400];
navigator.vibrate(heartbeat);

振动管理器

封装类

class VibrationManager {
  constructor() {
    this.isSupported = 'vibrate' in navigator;
    this.isEnabled = true;
    this.currentPattern = null;
  }
  
  // 检查是否可以振动
  canVibrate() {
    return this.isSupported && this.isEnabled;
  }
  
  // 启用/禁用振动
  setEnabled(enabled) {
    this.isEnabled = enabled;
    if (!enabled) {
      this.stop();
    }
  }
  
  // 简单振动
  vibrate(duration = 100) {
    if (!this.canVibrate()) return false;
    return navigator.vibrate(duration);
  }
  
  // 模式振动
  pattern(pattern) {
    if (!this.canVibrate()) return false;
    this.currentPattern = pattern;
    return navigator.vibrate(pattern);
  }
  
  // 重复模式
  repeat(pattern, times) {
    if (!this.canVibrate()) return false;
    
    const repeatedPattern = [];
    for (let i = 0; i < times; i++) {
      repeatedPattern.push(...pattern);
      if (i < times - 1) {
        // 添加间隔
        repeatedPattern.push(pattern[pattern.length - 1] || 100);
      }
    }
    
    return navigator.vibrate(repeatedPattern);
  }
  
  // 停止振动
  stop() {
    if (this.isSupported) {
      navigator.vibrate(0);
      this.currentPattern = null;
    }
  }
}

// 使用
const vibration = new VibrationManager();

// 简单振动
vibration.vibrate(200);

// 模式振动
vibration.pattern([100, 50, 100]);

// 重复 3 次
vibration.repeat([100, 50, 100], 3);

// 禁用振动
vibration.setEnabled(false);

预设模式库

class VibrationPatterns {
  static patterns = {
    // 通知类型
    notification: [100, 50, 100],
    success: [50, 50, 100],
    error: [100, 50, 100, 50, 100],
    warning: [200, 100, 200],
    
    // 交互反馈
    tap: [10],
    doubleTap: [10, 50, 10],
    longPress: [50],
    
    // 游戏效果
    explosion: [100, 30, 50, 30, 200],
    hit: [30],
    powerUp: [50, 30, 50, 30, 100],
    gameOver: [200, 100, 200, 100, 500],
    
    // 特殊模式
    heartbeat: [100, 100, 100, 400],
    alarm: [500, 200, 500, 200, 500],
    sos: [100, 50, 100, 50, 100, 200, 300, 50, 300, 50, 300, 200, 100, 50, 100, 50, 100],
    
    // 音乐节奏
    rhythm1: [100, 100, 100, 100, 200, 200],
    rhythm2: [50, 50, 50, 50, 100, 100, 200]
  };
  
  static get(name) {
    return this.patterns[name] || [100];
  }
  
  static play(name) {
    const pattern = this.get(name);
    if ('vibrate' in navigator) {
      navigator.vibrate(pattern);
    }
  }
  
  static register(name, pattern) {
    this.patterns[name] = pattern;
  }
  
  static list() {
    return Object.keys(this.patterns);
  }
}

// 使用
VibrationPatterns.play('success');
VibrationPatterns.play('heartbeat');

// 注册自定义模式
VibrationPatterns.register('custom', [75, 25, 75, 25, 150]);
VibrationPatterns.play('custom');

实际应用

触觉反馈按钮

class HapticButton {
  constructor(element, options = {}) {
    this.element = element;
    this.vibration = new VibrationManager();
    this.pattern = options.pattern || [10];
    this.enabled = options.enabled !== false;
    
    this.bindEvents();
  }
  
  bindEvents() {
    this.element.addEventListener('click', () => {
      this.onClick();
    });
    
    this.element.addEventListener('touchstart', () => {
      this.onTouchStart();
    });
  }
  
  onClick() {
    if (this.enabled) {
      this.vibration.pattern(this.pattern);
    }
  }
  
  onTouchStart() {
    if (this.enabled) {
      this.vibration.vibrate(5);
    }
  }
  
  setEnabled(enabled) {
    this.enabled = enabled;
  }
  
  setPattern(pattern) {
    this.pattern = pattern;
  }
}

// 使用
document.querySelectorAll('.haptic-btn').forEach(btn => {
  new HapticButton(btn, {
    pattern: [15],
    enabled: true
  });
});

// 不同按钮不同反馈
new HapticButton(document.getElementById('submit-btn'), {
  pattern: VibrationPatterns.get('success')
});

new HapticButton(document.getElementById('delete-btn'), {
  pattern: VibrationPatterns.get('warning')
});

游戏振动反馈

class GameHaptics {
  constructor() {
    this.vibration = new VibrationManager();
    this.intensityMultiplier = 1;
  }
  
  // 设置强度系数
  setIntensity(multiplier) {
    this.intensityMultiplier = Math.max(0, Math.min(2, multiplier));
  }
  
  // 应用强度
  applyIntensity(pattern) {
    return pattern.map(duration => 
      Math.round(duration * this.intensityMultiplier)
    );
  }
  
  // 碰撞反馈
  collision(force = 1) {
    const baseDuration = 30;
    const duration = Math.round(baseDuration * force * this.intensityMultiplier);
    this.vibration.vibrate(Math.min(duration, 200));
  }
  
  // 爆炸效果
  explosion(intensity = 1) {
    const pattern = this.applyIntensity([
      100 * intensity,
      30,
      50 * intensity,
      30,
      200 * intensity
    ]);
    this.vibration.pattern(pattern);
  }
  
  // 受击效果
  hit(damage = 1) {
    const duration = Math.round(20 + damage * 30);
    this.vibration.vibrate(Math.min(duration, 150));
  }
  
  // 拾取道具
  pickup() {
    this.vibration.pattern(this.applyIntensity([30, 30, 50]));
  }
  
  // 升级效果
  levelUp() {
    this.vibration.pattern(this.applyIntensity([50, 50, 50, 50, 100, 100, 200]));
  }
  
  // 游戏结束
  gameOver() {
    this.vibration.pattern(this.applyIntensity([200, 100, 200, 100, 500]));
  }
  
  // 胜利效果
  victory() {
    this.vibration.pattern(this.applyIntensity([
      100, 50, 100, 50, 100, 100,
      200, 100, 200, 100,
      300
    ]));
  }
  
  // 倒计时
  countdown(remaining) {
    if (remaining <= 3) {
      this.vibration.vibrate(100 + (4 - remaining) * 50);
    }
  }
}

// 使用
const haptics = new GameHaptics();
haptics.setIntensity(1.2);

// 游戏事件
game.on('collision', (force) => haptics.collision(force));
game.on('explosion', () => haptics.explosion());
game.on('hit', (damage) => haptics.hit(damage));
game.on('pickup', () => haptics.pickup());
game.on('levelUp', () => haptics.levelUp());
game.on('gameOver', () => haptics.gameOver());

表单验证反馈

class FormHapticFeedback {
  constructor(form) {
    this.form = form;
    this.vibration = new VibrationManager();
    
    this.bindEvents();
  }
  
  bindEvents() {
    // 表单提交
    this.form.addEventListener('submit', (e) => {
      if (!this.form.checkValidity()) {
        e.preventDefault();
        this.onValidationError();
      } else {
        this.onSubmitSuccess();
      }
    });
    
    // 输入字段验证
    this.form.querySelectorAll('input, select, textarea').forEach(field => {
      field.addEventListener('blur', () => {
        if (!field.checkValidity()) {
          this.onFieldError(field);
        }
      });
      
      field.addEventListener('input', () => {
        if (field.checkValidity() && field.value) {
          this.onFieldValid(field);
        }
      });
    });
  }
  
  onValidationError() {
    this.vibration.pattern([50, 30, 50, 30, 100]);
  }
  
  onSubmitSuccess() {
    this.vibration.pattern([50, 50, 100]);
  }
  
  onFieldError(field) {
    this.vibration.vibrate(30);
  }
  
  onFieldValid(field) {
    this.vibration.vibrate(10);
  }
}

// 使用
const form = document.getElementById('signup-form');
new FormHapticFeedback(form);

通知振动

class NotificationVibration {
  constructor() {
    this.vibration = new VibrationManager();
    this.priorities = {
      low: [50],
      normal: [100, 50, 100],
      high: [200, 100, 200],
      urgent: [100, 50, 100, 50, 100, 100, 200, 100, 200]
    };
  }
  
  notify(priority = 'normal') {
    const pattern = this.priorities[priority] || this.priorities.normal;
    this.vibration.pattern(pattern);
  }
  
  message() {
    this.notify('normal');
  }
  
  call() {
    // 来电振动(持续振动)
    this.startRinging();
  }
  
  startRinging() {
    this.ringInterval = setInterval(() => {
      this.vibration.pattern([400, 200, 400, 1000]);
    }, 2000);
  }
  
  stopRinging() {
    if (this.ringInterval) {
      clearInterval(this.ringInterval);
      this.ringInterval = null;
      this.vibration.stop();
    }
  }
  
  alarm() {
    this.notify('urgent');
  }
  
  reminder() {
    this.notify('low');
  }
}

// 使用
const notifVibration = new NotificationVibration();

// 收到消息
notifVibration.message();

// 来电
notifVibration.call();

// 接听或拒绝时停止
document.getElementById('answer-btn').addEventListener('click', () => {
  notifVibration.stopRinging();
});

摩尔斯电码生成器

class MorseVibrator {
  constructor() {
    this.vibration = new VibrationManager();
    
    // 时间单位(毫秒)
    this.unit = 100;
    
    // 摩尔斯电码表
    this.morseCode = {
      'A': '.-', 'B': '-...', 'C': '-.-.', 'D': '-..', 'E': '.', 
      'F': '..-.', 'G': '--.', 'H': '....', 'I': '..', 'J': '.---',
      'K': '-.-', 'L': '.-..', 'M': '--', 'N': '-.', 'O': '---',
      'P': '.--.', 'Q': '--.-', 'R': '.-.', 'S': '...', 'T': '-',
      'U': '..-', 'V': '...-', 'W': '.--', 'X': '-..-', 'Y': '-.--',
      'Z': '--..', '0': '-----', '1': '.----', '2': '..---',
      '3': '...--', '4': '....-', '5': '.....', '6': '-....',
      '7': '--...', '8': '---..', '9': '----.'
    };
  }
  
  // 文本转振动模式
  textToPattern(text) {
    const pattern = [];
    const upperText = text.toUpperCase();
    
    for (let i = 0; i < upperText.length; i++) {
      const char = upperText[i];
      
      if (char === ' ') {
        // 单词间隔(7 单位)
        pattern.push(0, this.unit * 7);
      } else if (this.morseCode[char]) {
        const morse = this.morseCode[char];
        
        for (let j = 0; j < morse.length; j++) {
          if (morse[j] === '.') {
            pattern.push(this.unit); // 点:1 单位
          } else {
            pattern.push(this.unit * 3); // 划:3 单位
          }
          
          // 符号间隔(1 单位)
          if (j < morse.length - 1) {
            pattern.push(this.unit);
          }
        }
        
        // 字符间隔(3 单位)
        if (i < upperText.length - 1 && upperText[i + 1] !== ' ') {
          pattern.push(this.unit * 3);
        }
      }
    }
    
    return pattern.filter(d => d > 0);
  }
  
  // 振动发送文本
  send(text) {
    const pattern = this.textToPattern(text);
    this.vibration.pattern(pattern);
    return pattern;
  }
  
  // 发送 SOS
  sendSOS() {
    return this.send('SOS');
  }
  
  // 设置速度
  setSpeed(wpm) {
    // 标准:PARIS = 50 单位,每分钟 wpm 个 PARIS
    this.unit = Math.round(1200 / wpm);
  }
}

// 使用
const morse = new MorseVibrator();
morse.setSpeed(15); // 15 WPM

// 发送消息
morse.send('HELLO');

// 发送 SOS
document.getElementById('sos-btn').addEventListener('click', () => {
  morse.sendSOS();
});

音乐节拍振动

class RhythmVibrator {
  constructor() {
    this.vibration = new VibrationManager();
    this.bpm = 120;
    this.isPlaying = false;
    this.beatInterval = null;
  }
  
  // 设置 BPM
  setBPM(bpm) {
    this.bpm = Math.max(30, Math.min(300, bpm));
    
    // 如果正在播放,重新开始
    if (this.isPlaying) {
      this.stop();
      this.start();
    }
  }
  
  // 计算拍子间隔(毫秒)
  getBeatInterval() {
    return 60000 / this.bpm;
  }
  
  // 开始节拍
  start() {
    if (this.isPlaying) return;
    
    this.isPlaying = true;
    const interval = this.getBeatInterval();
    
    // 立即振动一次
    this.beat();
    
    this.beatInterval = setInterval(() => {
      this.beat();
    }, interval);
  }
  
  // 单次拍子
  beat() {
    this.vibration.vibrate(30);
  }
  
  // 停止
  stop() {
    this.isPlaying = false;
    if (this.beatInterval) {
      clearInterval(this.beatInterval);
      this.beatInterval = null;
    }
    this.vibration.stop();
  }
  
  // 播放节奏模式
  playPattern(pattern, repeat = 1) {
    // pattern 示例: [1, 0, 1, 0, 1, 1, 0, 1]
    // 1 = 振动, 0 = 静音
    const interval = this.getBeatInterval();
    let index = 0;
    let repeatCount = 0;
    
    this.stop();
    this.isPlaying = true;
    
    const playBeat = () => {
      if (!this.isPlaying) return;
      
      if (pattern[index]) {
        this.beat();
      }
      
      index++;
      
      if (index >= pattern.length) {
        index = 0;
        repeatCount++;
        
        if (repeat > 0 && repeatCount >= repeat) {
          this.stop();
          return;
        }
      }
      
      this.beatInterval = setTimeout(playBeat, interval);
    };
    
    playBeat();
  }
}

// 使用
const rhythm = new RhythmVibrator();
rhythm.setBPM(100);

// 开始节拍
document.getElementById('start-rhythm').addEventListener('click', () => {
  rhythm.start();
});

// 停止
document.getElementById('stop-rhythm').addEventListener('click', () => {
  rhythm.stop();
});

// 播放 4/4 拍节奏
document.getElementById('pattern-44').addEventListener('click', () => {
  rhythm.playPattern([1, 0, 1, 0, 1, 0, 1, 0], 4);
});

最佳实践总结

Vibration API 最佳实践:
┌─────────────────────────────────────────────────────┐
│                                                     │
│   用户体验                                          │
│   ├── 提供禁用振动的选项                           │
│   ├── 振动时长保持简短                             │
│   ├── 避免过度使用振动                             │
│   └── 考虑用户场景(会议等)                       │
│                                                     │
│   性能考虑                                          │
│   ├── 避免连续高频振动                             │
│   ├── 长模式可能被系统中断                         │
│   ├── 后台页面振动可能不工作                       │
│   └── 电量消耗需考虑                               │
│                                                     │
│   兼容性处理                                        │
│   ├── 始终检查 API 支持                            │
│   ├── 桌面浏览器通常不支持                         │
│   ├── 提供非振动替代方案                           │
│   └── iOS Safari 不支持                            │
│                                                     │
└─────────────────────────────────────────────────────┘
场景推荐时长模式示例
轻触反馈5-15ms[10]
点击确认15-30ms[20]
成功提示50-100ms[50, 30, 80]
错误警告100-200ms[100, 50, 100]
通知提醒200-500ms[200, 100, 200]

善用 Vibration API,提升移动端交互体验。