第13章 客户端检测
第13章 客户端检测
能力检测
能力检测(也称为特征检测)使用浏览器的JavaScript运行时中的一组简单检查来测试对各种能力的支持。这种方法假定不需要特定的浏览器知识,并且可以通过确定所讨论的能力是否确实存在来找到解决方案。能力检测的基本模式如下:
if (object.propertyInQuestion) {
// use object.propertyInQuestion
}
例如,DOM方法document.getElmentById()在IE5之前并不存在,但可使用非标准的document.all属性实现。这导致了以下能力检测:
function getElement(id) {
if (document.getElementById) {
return document.getElementById(id);
} else if (document.all) {
return document.all[id];
} else {
throw new Error("No way to retrieve element!");
}
}
安全的能力检测
当不仅要验证该能力是否存在,而且要验证该能力是否可能以适当方式运行时,能力检测最为有效。前一节中的例子依靠被测试对象成员的类型强制转换来确定其存在。虽然这会告诉你对象成员的存在,但没有迹象表明该成员是否是你所期待的对象成员。考虑以下能力,该函数试图确定对象是否可排序:
// AVOID! Incorrect capability detection – only checks for existence
function isSortable(object) {
return !!object.sort;
}
此函数试图通过检查sort()方法的存在来确定对象是否可以排序。问题是,任何具有sort属性的对象也会返回true:
let result = isSortable({ sort: true });
仅仅测试属性的存在并不能明确表明所涉对象是可排序的。更好的方法是检查这种类型实际上是一个函数:
// Better – checks if sort is a function
function isSortable(object) {
return typeof object.sort == "function";
}
使用能力检测进行浏览器分析
虽然有些人可能认为能力检测在某种程度上是一种黑客行为,但正确应用能力检测可以对执行代码的浏览器进行令人惊讶的准确分析。使用能力检测而不是用户代理来识别浏览器的优点是,虽然仿冒用户代理非常容易,但要以能够可靠地愚弄能力检测测试的方式隐藏浏览器的所有能力要困难得多。
检测特征支持
可以将浏览器的各项特征组合到类中。如果知道应用程序需要使用特定的浏览器功能,那么对所有特征进行一次检测而不是重复检测可能很有用:
// determine if the browser has Netscape-style plugins
let hasNSPlugins = !!(navigator.plugins && navigator.plugins.length);
// determine if the browser has basic DOM Level 1 capabilities
let hasDOM1 = !!(document.getElementById && document.createElement &&
document.getElementsByTagName);
检测浏览器身份
还可以测试浏览器支持的各种特征和已知的特征集合比较以确定在使用哪个浏览器。这允许一个安全的用户代理仿冒检测方案(稍后讨论),但未来的浏览器版本可能会打破你的能力检测计划。考虑以下示例,该示例测试各种浏览器已知的独特行为,以推断代码正在运行其内的浏览器。此代码故意不使用navigator.userAgent,这在章节的后期讨论:
class BrowserDetector {
constructor() {
// Test for conditional compilation
// Supported IE6 to IE10
this.isIE_Gte6Lte10 = /*@cc_on!@*/ false;
// Test for presence of documentMode
// Supported IE7 to IE11
this.isIE_Gte7Lte11 = !!document.documentMode;
// Test for presence of StyleMedia constructor
// Supported Edge >= 20
this.isEdge_Gte20 = !!window.StyleMedia;
// Test for proprietary Firefox add-on install API
// Supported for all Firefox versions
this.isFirefox_Gte1 = typeof InstallTrigger !== 'undefined';
// Test for presence of Chrome object and its webstore property. Versions
// of Opera will have window.chrome, but not window.chrome.webstore
// Supported for all Chrome versions
this.isChrome_Gte1 = !!window.chrome && !!window.chrome.webstore;
// Early versions of Safari would append "Constructor" to the constructor
// function identifier.
// window.Element.toString(); // [object ElementConstructor]
// Supported Safari 3 to 9.1
this.isSafari_Gte3Lte9_1 = /constructor/i.test(window.Element);
// Push notification API exposed on window object. Uses default parameters
// to prevent stringification of undefined values.
// Supported Safari 7.1+
this.isSafari_Gte7_1 =
(({
pushNotification = {}
} = {}) =>
pushNotification.toString() == '[object SafariRemoteNotification]'
)(window.safari);
// Tests for the 'addons' property.
// Supported Opera 20+
this.isOpera_Gte20 = !!window.opr && !!window.opr.addons;
}
isIE() {
return this.isIE_Gte6Lte10 || this.isIE_Gte7Lte11;
}
isEdge() {
return this.isEdge_Gte20 && !this.isIE();
}
isFirefox() {
return this.isFirefox_Gte1;
}
isChrome() {
return this.isChrome_Gte1
};
isSafari() {
return this.isSafari_Gte3Lte9_1 || this.isSafari_Gte7_1;
}
isOpera() {
return this.isOpera_Gte20;
}
用户代理检测
用户代理检测使用浏览器的用户代理字符串来确定有关正在使用的浏览器的信息。用户代理字符串作为响应头发送到每个HTTP请求,并在JavaScript中通过navigator.userAgent访问。在服务器方面,查看用户代理字符串以确定正在使用的浏览器并采取相应行动是常见且公认的做法。但是,在客户端方面,用户代理检测应假定为不可靠,并在其他选项不可行的情况下使用。
用户代理字符串的争议性方面包括其长期的欺骗(spoofing)历史,如浏览器试图通过将错误或误导性信息包含到用户代理字符串中来欺骗服务器。要理解此问题,有必要回顾一下自Web首次出现以来用户代理字符串是如何演变的。
用于浏览器分析的用户代理
大多数开发人员希望识别运行其代码的浏览器中从window.navigator.userAgent返回的字符串。所有浏览器都提供这样的字符串,如果信任返回的值,请检查之。
使用用户代理而不是能力检测来识别浏览器的优点是,虽然能力检测提供了更有力的保护,防止脚本可能隐藏浏览器,但现代浏览器的用户代理以可预测的方式组织,用于供应商浏览器的过去、当前和未来版本。
仿冒用户代理
用户代理识别是一个不完美的浏览器识别解决方案,特别是因为它很容易仿冒用户代理字符串。正确实现window.navigator对象(实际上全部)的浏览器将作为只读属性提供。因此,使用setter不会做什么:
console.log(window.navigator.userAgent);
// Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)
Chrome/65.0.3325.181 Safari/537.36
window.navigator.userAgent = 'foobar';
console.log(window.navigator.userAgent);
// Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)
Chrome/65.0.3325.181 Safari/537.36
然而,有许多容易的解决方法。例如,浏览器提供伪私有defineGetter使仿冒用户代理变得非常容易:
console.log(window.navigator.userAgent);
// Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36
window.navigator.__defineGetter__('userAgent', () => 'hahaha');
console.log(window.navigator.userAgent);
// hahaha
像这样的解决方法是一场艰苦的战斗。检测以这种方式被仿冒的用户代理是可能的,但以稳健的方式进行检测本质上是一场打地鼠游戏。
用于浏览器分析的用户代理
通过分析浏览器的用户代理,可以可靠地准确推断出以下许多有关环境的详细信息:
Browser
Browser version
Browser rendering engine
Device class (desktop/mobile)
Device manufacturer
Device model
Operating system
Operating system version
有新的浏览器,操作系统和设备不断推出,许多具有类似但独特的用户代理字符串:因此,用户代理解析器实现需要不断更新以避免过时。任何手卷用户代理解析器实现将迅速过时,因此需不断更新和修订。虽然本书的前一版构建了自己的用户代理解析器版本,但不再建议从零开始构建解析器。相反,以下是一些积极维护的用户代理解析器:
Bowser—https://github.com/lancedikson/bowser
ua-parser-js—https://github.com/faisalman/ua-parser-js
Platform.js—https://github.com/bestiejs/platform.js
current-device—https://github.com/matthewhudson/current-device
Google Closure—https://github.com/google/closure-library/tree/master/closure/goog/useragent
Mootools—https://github.com/mootools/mootools-core/blob/master/Source/Browser/Browser.js
软件和硬件检测
现代浏览器提供一套有关页面执行环境的信息,其中包括浏览器信息、操作系统、硬件和系统外设。这些信息可以通过window.navigator对象上的一系列API访问。 但是,浏览器对这些API的支持远未标准化。
浏览器和操作系统识别
虽然能力检测和用户代理解析是识别当前使用的浏览器的两种方法,但navigator和screen对象还提供有关页面当前执行的软件环境的信息。
f12输入console.log(window.navigator)查看详情 。