客户端检测是 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.StyleMediafunction 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用户代理字符串的历史演变:
"Mozilla/版本号""Mozilla/版本号" 开头"AppleWebKit/版本号"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'
)用户代理检测有以下问题:
// 不好的做法:用户代理检测
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()
}
}客户端检测是开发跨浏览器应用的重要技术:
在实际开发中,应该优先使用能力检测,结合渐进增强的策略,为不同浏览器提供最佳的用户体验。