客户端检测

客户端检测是 JavaScript 中最有争议的一个主题,因为不同浏览器之间存在差异。检测 Web 客户端的手段有多种,每一种都有优缺点。

能力检测

能力检测(feature detection)不关心用户代理字符串,而是通过检测浏览器是否支持特定的能力来确定如何编写代码。

安全能力检测

能力检测的基本模式:

if (object.propertyInQuestion) {
  // 使用 object.propertyInQuestion
}

这种检测方式的问题在于:假设属性存在就一定能正常工作。

更好的方式是检测某个能力是否能够正常执行:

// 检测排序能力
function isSortable(object) {
  return typeof object.sort === 'function'
}

// 检测 DOM 能力
function hasCreateElement() {
  return typeof document.createElement === 'function'
}

// 检测事件处理能力
function hasAddEventListener() {
  return typeof document.addEventListener === 'function'
}

基于能力检测进行浏览器分析

通过检测多个能力来确定浏览器:

class BrowserDetector {
  constructor() {
    this.isFirefox = typeof InstallTrigger !== 'undefined'
    this.isChrome = !!window.chrome && !!window.chrome.webstore
    this.isSafari = /constructor/i.test(window.HTMLElement) ||
                   (function (p) { return p.toString() === '[object SafariRemoteNotification]' })(!window['safari'] || (typeof safari !== 'undefined' && window['safari'].pushNotification))
    this.isIE = /*@cc_on!@*/false || !!document.documentMode
    this.isEdge = !this.isIE && !!window.StyleMedia
  }

  getBrowser() {
    if (this.isFirefox) return 'Firefox'
    if (this.isChrome) return 'Chrome'
    if (this.isSafari) return 'Safari'
    if (this.isIE) return 'IE'
    if (this.isEdge) return 'Edge'
    return 'Unknown'
  }
}

const detector = new BrowserDetector()
console.log(detector.getBrowser())

怪癖检测

怪癖检测(quirks detection)通过检测浏览器的特殊行为来确定浏览器类型。

浏览器怪癖

// 检测 IE 浏览器
const isIE = navigator.userAgent.indexOf('MSIE') !== -1

// 检测 Safari 的怪癖
const isSafari = /AppleWebKit/.test(navigator.userAgent) &&
                /Mobile\/\w+/.test(navigator.userAgent) &&
                !/Chrome/.test(navigator.userAgent)

// 检测 Firefox
const isFirefox = typeof InstallTrigger !== 'undefined'

// 检测 Chrome
const isChrome = !!window.chrome

// 检测 Edge
const isEdge = !isIE && !!window.StyleMedia

检测浏览器版本

function getBrowserVersion() {
  const ua = navigator.userAgent
  let version = null

  // Firefox
  if (ua.indexOf('Firefox') !== -1) {
    version = ua.match(/Firefox\/(\d+)/)[1]
    return { browser: 'Firefox', version: parseInt(version) }
  }

  // Chrome
  if (ua.indexOf('Chrome') !== -1) {
    version = ua.match(/Chrome\/(\d+)/)[1]
    return { browser: 'Chrome', version: parseInt(version) }
  }

  // Safari
  if (ua.indexOf('Safari') !== -1 && ua.indexOf('Chrome') === -1) {
    version = ua.match(/Version\/(\d+)/)[1]
    return { browser: 'Safari', version: parseInt(version) }
  }

  // IE
  if (ua.indexOf('MSIE') !== -1 || ua.indexOf('Trident') !== -1) {
    if (ua.indexOf('MSIE') !== -1) {
      version = ua.match(/MSIE (\d+)/)[1]
    } else {
      version = ua.match(/rv:(\d+)/)[1]
    }
    return { browser: 'IE', version: parseInt(version) }
  }

  return { browser: 'Unknown', version: null }
}

用户代理检测

用户代理检测(user-agent detection)通过检查用户代理字符串来确定浏览器。

用户代理字符串

console.log(navigator.userAgent)

// 示例输出:
// Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36

用户代理检测的历史

用户代理字符串的历史演变:

  1. 早期浏览器:Netscape Navigator 的字符串是 "Mozilla/版本号"
  2. IE 冒充 Mozilla:IE 也使用 "Mozilla/版本号" 开头
  3. WebKit 内核:Safari、Chrome 等都包含 "AppleWebKit/版本号"
  4. 移动设备:添加了移动设备相关信息

用户代理检测的实现

class UserAgentDetector {
  constructor() {
    this.ua = navigator.userAgent.toLowerCase()
    this.browser = this.detectBrowser()
    this.version = this.detectVersion()
    this.os = this.detectOS()
    this.device = this.detectDevice()
  }

  detectBrowser() {
    if (this.ua.includes('firefox')) return 'Firefox'
    if (this.ua.includes('chrome') && !this.ua.includes('edg')) return 'Chrome'
    if (this.ua.includes('safari') && !this.ua.includes('chrome')) return 'Safari'
    if (this.ua.includes('edg')) return 'Edge'
    if (this.ua.includes('opera') || this.ua.includes('opr')) return 'Opera'
    if (this.ua.includes('msie') || this.ua.includes('trident')) return 'IE'
    return 'Unknown'
  }

  detectVersion() {
    const browsers = {
      'firefox': /firefox\/([0-9.]+)/,
      'chrome': /chrome\/([0-9.]+)/,
      'safari': /version\/([0-9.]+)/,
      'edg': /edg\/([0-9.]+)/,
      'opera': /opr\/([0-9.]+)/,
      'ie': /msie ([0-9.]+)|rv:([0-9.]+)/
    }

    const regex = browsers[this.browser.toLowerCase()]
    if (regex) {
      const match = this.ua.match(regex)
      return match ? match[1] || match[2] : null
    }
    return null
  }

  detectOS() {
    if (this.ua.includes('windows nt')) return 'Windows'
    if (this.ua.includes('mac os x')) return 'macOS'
    if (this.ua.includes('linux')) return 'Linux'
    if (this.ua.includes('android')) return 'Android'
    if (this.ua.includes('iphone') || this.ua.includes('ipad')) return 'iOS'
    return 'Unknown'
  }

  detectDevice() {
    if (this.ua.includes('mobile')) return 'Mobile'
    if (this.ua.includes('tablet')) return 'Tablet'
    return 'Desktop'
  }

  getInfo() {
    return {
      browser: this.browser,
      version: this.version,
      os: this.os,
      device: this.device,
      userAgent: this.ua
    }
  }
}

// 使用示例
const detector = new UserAgentDetector()
console.log(detector.getInfo())

navigator 对象包含了大量关于浏览器的信息。

基本属性

// 基本信息
console.log(navigator.appName)        // 浏览器全名
console.log(navigator.appVersion)     // 浏览器版本
console.log(navigator.appCodeName)    // 浏览器代码名
console.log(navigator.userAgent)      // 用户代理字符串
console.log(navigator.platform)       // 浏览器所在平台
console.log(navigator.language)       // 浏览器主语言
console.log(navigator.languages)      // 浏览器支持的语言数组
console.log(navigator.onLine)          // 是否联网
console.log(navigator.cookieEnabled)  // 是否启用 cookie

检测插件

// 检测插件(非 IE)
function hasPlugin(name) {
  name = name.toLowerCase()
  for (let plugin of navigator.plugins) {
    if (plugin.name.toLowerCase().includes(name)) {
      return true
    }
  }
  return false
}

// IE 插件检测
function hasIEPlugin(name) {
  try {
    new ActiveXObject(name)
    return true
  } catch (ex) {
    return false
  }
}

// 检测 Flash
function hasFlash() {
  let result = hasPlugin('Flash')
  if (!result) {
    result = hasIEPlugin('ShockwaveFlash.ShockwaveFlash')
  }
  return result
}

// 检测 PDF 阅读器
function hasPDFReader() {
  return hasPlugin('PDF') || hasPlugin('Chrome PDF Viewer')
}

注册处理程序

HTML5 允许网页注册为特定类型信息的默认处理程序。

// 注册邮件处理程序
navigator.registerProtocolHandler(
  'mailto',
  'http://www.example.com?cmd=mailto&uri=%s',
  'Example Mail'
)

// 注册电话处理程序
navigator.registerProtocolHandler(
  'tel',
  'http://www.example.com?cmd=tel&uri=%s',
  'Example Phone'
)

客户端检测的最佳实践

避免用户代理检测

用户代理检测有以下问题:

  1. 字符串欺骗:浏览器可以随意修改用户代理字符串
  2. 字符串解析困难:不同浏览器的格式不一致
  3. 版本更新频繁:需要不断更新检测逻辑
  4. 无法检测所有场景:有些能力无法通过字符串判断

推荐使用能力检测

// 不好的做法:用户代理检测
if (navigator.userAgent.includes('MSIE')) {
  // IE 特定代码
}

// 好的做法:能力检测
if (typeof document.addEventListener === 'function') {
  // 支持标准事件处理
} else if (typeof document.attachEvent === 'function') {
  // IE 事件处理
}

组合检测策略

在某些情况下,需要组合多种检测方法:

function getBrowserInfo() {
  // 首先尝试能力检测
  const capabilities = {
    geolocation: !!navigator.geolocation,
    webGL: !!window.WebGLRenderingContext,
    localStorage: !!window.localStorage,
    serviceWorker: !!navigator.serviceWorker,
    webRTC: !!window.RTCPeerConnection
  }

  // 补充用户代理信息
  const ua = navigator.userAgent
  const isMobile = /Mobile|Android|iP(hone|od|ad)/.test(ua)

  // 检测操作系统
  let os = 'Unknown'
  if (ua.includes('Windows')) os = 'Windows'
  else if (ua.includes('Mac')) os = 'macOS'
  else if (ua.includes('Linux')) os = 'Linux'
  else if (ua.includes('Android')) os = 'Android'
  else if (ua.includes('iOS')) os = 'iOS'

  return {
    capabilities,
    isMobile,
    os
  }
}

渐进增强与优雅降级

// 渐进增强:从基本功能开始,逐步添加高级功能
function setupFeatures() {
  // 基本功能
  setupBasicFeatures()

  // 检测并添加高级功能
  if (navigator.geolocation) {
    setupGeolocation()
  }

  if (window.localStorage) {
    setupLocalStorage()
  }

  if ('serviceWorker' in navigator) {
    setupServiceWorker()
  }
}

// 优雅降级:从完整功能开始,逐步移除不支持的功能
function setupFeatures() {
  // 完整功能
  setupAllFeatures()

  // 检测并移除不支持的功能
  if (!navigator.geolocation) {
    removeGeolocation()
  }

  if (!window.localStorage) {
    removeLocalStorage()
  }
}

总结

客户端检测是开发跨浏览器应用的重要技术:

  1. 能力检测:检测浏览器是否支持特定功能,是最可靠的方法
  2. 怪癖检测:检测浏览器的特殊行为来识别浏览器
  3. 用户代理检测:检查用户代理字符串,虽然有局限性但有时仍有用
  4. navigator 对象:提供了丰富的浏览器和系统信息

在实际开发中,应该优先使用能力检测,结合渐进增强的策略,为不同浏览器提供最佳的用户体验。