「这是我参与11月更文挑战的第11天,活动详情查看:2021最后一次更文挑战」。
前言
虽然浏览器厂商齐心协力想要实现一致的接口,但事实上仍然是每家浏览器都有自己的长处与不 足。跨平台的浏览器尽管版本相同,但总会存在不同的问题。这些差异迫使 Web 开发者要么面向最大 公约数而设计,要么(更常见地)使用各种方法来检测客户端,以克服或避免这些缺陷。
客户端检测一直是 Web 开发中饱受争议的话题,这些话题普遍围绕所有浏览器应支持一系列公共 特性,理想情况下是这样的。而现实当中,浏览器之间的差异和莫名其妙的行为,让客户端检测变成一 种补救措施,而且也成为了开发策略的重要一环。如今,浏览器之间的差异相对 IE 大溃败以前已经好 很多了,但浏览器间的不一致性依旧是 Web 开发中的常见主题。
要检测当前的浏览器有很多方法,每一种都有各自的长处和不足。问题的关键在于知道客户端检测 应该是解决问题的最后一个举措。任何时候,只要有更普适的方案可选,都应该毫不犹豫地选择。首先 要设计最常用的方案,然后再考虑为特定的浏览器进行补救
能力检测
能力检测(又称特性检测)即在 JavaScript 运行时中使用一套简单的检测逻辑,测试浏览器是否支 持某种特性。这种方式不要求事先知道特定浏览器的信息,只需检测自己关心的能力是否存在即可。能 力检测的基本模式如下:
if (object.propertyInQuestion) {
// 使用 object.propertyInQuestion
}
比如,IE5 之前的版本中没有 document.getElementById()这个 DOM 方法,但可以通过 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!");
}
}
这个 getElement()函数的目的是根据给定的 ID 获取元素。因为标准的方式是使用 document. getElementById(),所以首先测试它。如果这个函数存在(不是 undefined),那就使用这个方法; 否则检测 document.all 是否存在,如果存在则使用。如果这两个能力都不存在(基本上不可能),则 抛出错误说明功能无法实现。
能力检测的关键是理解两个重要概念。首先,如前所述,应该先检测最常用的方式。在前面的例子 中就是先检测 document.getElementById()再检测 document.all。测试最常用的方案可以优化代 码执行,这是因为在多数情况下都可以避免无谓检测。
其次是必须检测切实需要的特性。某个能力存在并不代表别的能力也存在。比如下面的例子:
function getWindowWidth() {
if (document.all) { // 假设 IE
return document.documentElement.clientWidth; // 不正确的用法!
} else {
return window.innerWidth;
}
}
这个例子展示了不正确的能力检测方式。getWindowWidth()函数首先检测 document.all 是否 存在,如果存在则返回 document.documentElement.clientWidth,理由是 IE8 及更低版本不支持 window.innerWidth。这个例子的问题在于检测到 document.all 存在并不意味着浏览器是 IE。事实, 也可能是某个早期版本的 Opera,既支持 document.all 也支持 windown.innerWidth。
安全能力检测
能力检测最有效的场景是检测能力是否存在的同时,验证其是否能够展现出预期的行为。前一节中 的例子依赖将测试对象的成员转换类型,然后再确定它是否存在。虽然这样能够确定检测的对象成员存 在,但不能确定它就是你想要的。来看下面的例子,这个函数尝试检测某个对象是否可以排序: // 不要这样做!错误的能力检测,只能检测到能力是否存在
function isSortable(object) {
return !!object.sort;
}
这个函数尝试通过检测对象上是否有 sort()方法来确定它是否支持排序。问题在于,即使这个对 象有一个 sort 属性,这个函数也会返回 true: let result = isSortable({ sort: true }); 简单地测试到一个属性存在并不代表这个对象就可以排序。更好的方式是检测 sort 是不是函数: // 好一些,检测 sort 是不是函数
function isSortable(object) {
return typeof object.sort == "function";
}
上面的代码中使用的 typeof 操作符可以确定 sort 是不是函数,从而确认是否可以调用它对数据 进行排序。
进行能力检测时应该尽量使用 typeof 操作符,但光有它还不够。尤其是某些宿主对象并不保证对 typeof 测试返回合理的值。最有名的例子就是 Internet Explorer(IE)。在多数浏览器中,下面的代码都 会在 document.createElement()存在时返回 true:
// 不适用于 IE8 及更低版本
function hasCreateElement() {
return typeof document.createElement == "function";
}
但在 IE8 及更低版本中,这个函数会返回 false。这是因为 typeof document.createElement 返回"object"而非"function"。前面提到过,DOM 对象是宿主对象,而宿主对象在 IE8 及更低版本 中是通过 COM 而非 JScript 实现的。因此,document.createElement()函数被实现为 COM 对象, typeof 返回"object"。IE9 对 DOM 方法会返回"function"。
基于能力检测进行浏览器分析
虽然可能有人觉得能力检测类似于黑科技,但恰当地使用能力检测可以精准地分析运行代码的浏览 器。使用能力检测而非用户代理检测的优点在于,伪造用户代理字符串很简单,而伪造能够欺骗能力检 测的浏览器特性却很难。
检测特性
可以按照能力将浏览器归类。如果你的应用程序需要使用特定的浏览器能力,那么最好集中检测所 有能力,而不是等到用的时候再重复检测。比如:
// 检测浏览器是否支持 Netscape 式的插件
let hasNSPlugins = !!(navigator.plugins && navigator.plugins.length);
// 检测浏览器是否具有 DOM Level 1 能力
let hasDOM1 = !!(document.getElementById && document.createElement &&
document.getElementsByTagName);
这个例子完成了两项检测:一项是确定浏览器是否支持 Netscape 式的插件,另一项是检测浏览器 是否具有 DOM Level 1 能力。保存在变量中的布尔值可以用在后面的条件语句中,这样比重复检测省 事多了。
检测浏览器
可以根据对浏览器特性的检测并与已知特性对比,确认用户使用的是什么浏览器。这样可以获得比 用户代码嗅探(稍后讨论)更准确的结果。但未来的浏览器版本可能不适用于这套方案。 下面来看一个例子,根据不同浏览器独有的行为推断出浏览器的身份。这里故意没有使用 navigator. userAgent 属性
class BrowserDetector {
constructor() {
// 测试条件编译
// IE6~10 支持
this.isIE_Gte6Lte10 = /*@cc_on!@*/false;
// 测试 documentMode
// IE7~11 支持
this.isIE_Gte7Lte11 = !!document.documentMode;
// 测试 StyleMedia 构造函数
// Edge 20 及以上版本支持
this.isEdge_Gte20 = !!window.StyleMedia;
// 测试 Firefox 专有扩展安装 API
// 所有版本的 Firefox 都支持
this.isFirefox_Gte1 = typeof InstallTrigger !== 'undefined';
// 测试 chrome 对象及其 webstore 属性
// Opera 的某些版本有 window.chrome,但没有 window.chrome.webstore
// 所有版本的 Chrome 都支持
this.isChrome_Gte1 = !!window.chrome && !!window.chrome.webstore;
// Safari 早期版本会给构造函数的标签符追加"Constructor"字样,如:
// window.Element.toString(); // [object ElementConstructor]
// Safari 3~9.1 支持
this.isSafari_Gte3Lte9_1 = /constructor/i.test(window.Element);
// 推送通知 API 暴露在 window 对象上
// 使用默认参数值以避免对 undefined 调用 toString()
// Safari 7.1 及以上版本支持
this.isSafari_Gte7_1 =
(({pushNotification = {}} = {}) =>
pushNotification.toString() == '[object SafariRemoteNotification]'
)(window.safari);
// 测试 addons 属性
// 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; }
}
这个类暴露的通用浏览器检测方法使用了检测浏览器范围的能力测试。随着浏览器的变迁及发展, 可以不断调整底层检测逻辑,但主要的 API 可以保持不变。
能力检测的局限
通过检测一种或一组能力,并不总能确定使用的是哪种浏览器。以下“浏览器检测”代码(或其他 类似代码)经常出现在很多网站中,但都没有正确使用能力检测:
// 不要这样做!不够特殊
let isFirefox = !!(navigator.vendor && navigator.vendorSub);
// 不要这样做!假设太多
let isIE = !!(document.all && document.uniqueID);
这是错误使用能力检测的典型示例。过去,Firefox 可以通过 navigator.vendor 和 navigator. vendorSub 来检测,但后来 Safari 也实现了同样的属性,于是这段代码就会产生误报。为确定 IE, 这段代码检测了 document.all 和 document.uniqueID。这是假设 IE 将来的版本中还会继续存在这 两个属性,而且其他浏览器也不会实现它们。不过这两个检测都使用双重否定操作符来产生布尔值(这样可以生成便于存储和访问的结果)。