使用用户代理进行浏览器检测

为不同的浏览器提供不同的网页或服务通常不是一个好主意。网络旨在让每个人都能访问,无论他们使用的是什么浏览器或设备。有一些方法可以开发您的网站,使其基于功能的可用性逐步增强自身,而不是针对特定的浏览器。

但是浏览器和标准并不完美,仍然存在一些需要检测浏览器才能解决的极端情况。使用用户代理来检测浏览器看起来很简单,但实际上做好这一点非常困难。本文档将指导您尽可能正确地执行此操作。

注意:值得重申的是:使用用户代理嗅探很少是一个好主意。您几乎总能找到更好的、更广泛兼容的方法来解决您的问题!

使用浏览器检测之前的注意事项

在考虑使用用户代理字符串来检测正在使用的浏览器时,您的第一步是尝试尽可能避免它。首先尝试确定您为什么要这样做。

您是否试图解决某个浏览器版本中的特定错误?

在专门的论坛中查找或提问:您不太可能是第一个遇到此问题的人。此外,专家或拥有其他观点的人可以为您提供解决此错误的方法。如果问题看起来不常见,值得检查此错误是否已通过其错误跟踪系统报告给浏览器供应商(MozillaWebKitBlinkOpera)。浏览器制造商确实会关注错误报告,并且分析可能会暗示解决此错误的其他方法。

您是否试图检查特定功能是否存在?

您的网站需要使用某些浏览器尚不支持的特定 Web 功能,并且您希望将这些用户发送到功能较少但您知道可以正常工作的旧版网站。这是使用用户代理检测的最糟糕的原因,因为很有可能最终所有其他浏览器都会赶上。此外,测试每个不太流行的浏览器并测试这些 Web 功能在实践中是不现实的。您绝不应进行用户代理嗅探。始终可以选择执行功能检测。

您是否希望根据正在使用的浏览器提供不同的 HTML?

这通常是一种不好的做法,但有些情况下这是必要的。在这些情况下,您应该首先分析您的情况以确保确实有必要。您可以通过添加一些非语义的 <div><span> 元素来避免这种情况吗?成功使用用户代理检测的难度值得您对 HTML 纯度造成一些破坏。此外,重新思考您的设计:您可以使用渐进增强或灵活布局来帮助消除执行此操作的需求吗?

避免使用用户代理检测

如果您想避免使用用户代理检测,您可以选择!

功能检测

功能检测是指您不尝试确定哪个浏览器正在呈现您的页面,而是检查您需要的特定功能是否可用。如果不可用,则使用后备方案。在那些浏览器之间行为不同的罕见情况下,您应该实现一个测试来检测浏览器如何实现 API 并确定如何从中使用它,而不是检查用户代理字符串。功能检测的一个示例如下。2017 年,Chrome 取消了正则表达式中实验性后视支持的标记,但其他浏览器都不支持它。因此,您可能认为可以执行以下操作

js
// This code snippet splits a string in a special notation
let splitUpString;
if (navigator.userAgent.includes("Chrome")) {
  // YES! The user is suspected to support look-behind regexps
  // DO NOT USE /(?<=[A-Z])/. It will cause a syntax error in
  // browsers that do not support look-behind expressions
  // because all browsers parse the entire script, including
  // sections of the code that are never executed.
  const camelCaseExpression = new RegExp("(?<=[A-Z])");
  splitUpString = (str) => String(str).split(camelCaseExpression);
} else {
  // This fallback code is much less performant, but works
  splitUpString = (str) =>
    String(str)
      .split(/(.*?[A-Z])/)
      .filter(Boolean);
}

console.log(splitUpString("fooBar")); // ["fooB", "ar"]
console.log(splitUpString("jQWhy")); // ["jQ", "W", "hy"]

上面的代码会做出一些不正确的假设:首先,它假设所有包含子字符串“Chrome”的用户代理字符串都是 Chrome。UA 字符串众所周知具有误导性。然后,它假设如果浏览器是 Chrome,则后视功能将始终可用。代理可能是 Chrome 的旧版本,在添加支持之前,或者(因为该功能当时是实验性的),它可能是删除了该功能的 Chrome 的较新版本。最重要的是,它假设没有其他浏览器会支持此功能。任何时候都可能在其他浏览器中添加支持,但此代码将继续选择较差的路径。

可以通过测试功能本身的支持来避免此类问题

js
let isLookBehindSupported = false;

try {
  new RegExp("(?<=)");
  isLookBehindSupported = true;
} catch (err) {
  // If the agent doesn't support look behinds, the attempted
  // creation of a RegExp object using that syntax throws and
  // isLookBehindSupported remains false.
}

const splitUpString = isLookBehindSupported
  ? (str) => String(str).split(new RegExp("(?<=[A-Z])"))
  : (str) =>
      String(str)
        .split(/(.*?[A-Z])/)
        .filter(Boolean);

console.log(splitUpString("fooBar")); // ["fooB", "ar"]
console.log(splitUpString("jQWhy")); // ["jQ", "W", "hy"]

如上述代码所示,始终有一种方法可以测试浏览器支持,而无需用户代理嗅探。绝不有任何理由为此检查用户代理字符串。

最后,上述代码片段引发了跨浏览器编码中的一个关键问题,必须始终考虑在内。不要在不受支持的浏览器中无意中使用您正在测试的 API。这听起来可能显而易见且简单,但有时并非如此。例如,在上面的代码片段中,在不受支持的浏览器中使用简短正则表达式表示法中的后视(例如,/reg/igm)会导致解析器错误。因此,在上面的示例中,您将使用 new RegExp("(?<=look_behind_stuff)"); 而不是 /(?<=look_behind_stuff)/,即使在代码的后视支持部分也是如此。

渐进增强

此设计技术涉及以“分层”的方式开发您的网站,使用自下而上的方法,从一个更简单的层开始,并在后续的层中改进网站的功能,每一层都使用更多功能。

优雅降级

这是一种自上而下的方法,您使用所有想要的功能构建尽可能好的网站,然后对其进行调整以使其在旧版浏览器上也能正常工作。这可能比渐进增强更难执行,效果也可能不佳,但在某些情况下可能有用。

移动设备检测

可以说,用户代理嗅探最常见的使用和误用是检测设备是否为移动设备。但是,人们常常忽略他们真正想要的是什么。人们使用用户代理嗅探来检测用户的设备是否支持触摸并具有小屏幕,以便他们可以相应地优化其网站。虽然用户代理嗅探有时可以检测到这些,但并非所有设备都相同:一些移动设备具有大屏幕尺寸,一些台式机具有小触摸屏,一些人使用智能电视,这完全是另一回事,还有一些人可以通过翻转平板电脑来动态更改屏幕的宽度和高度!因此,用户代理嗅探绝对不是正确的方法。值得庆幸的是,还有更好的替代方案。使用 Navigator.maxTouchPoints 检测用户的设备是否具有触摸屏。然后,仅在 if (!("maxTouchPoints" in navigator)) { /* Code here */ } 时默认回退到检查用户代理屏幕。使用有关设备是否具有触摸屏的信息,不要仅针对触摸设备更改整个网站的布局:您只会为自己制造更多工作和维护。相反,添加触摸便利性,例如更大、更易于点击的按钮(您可以通过增加字体大小使用 CSS 来做到这一点)。这是一个在移动设备上将 #exampleButton 的填充增加到 1em 的代码示例。

js
let hasTouchScreen = false;
if ("maxTouchPoints" in navigator) {
  hasTouchScreen = navigator.maxTouchPoints > 0;
} else if ("msMaxTouchPoints" in navigator) {
  hasTouchScreen = navigator.msMaxTouchPoints > 0;
} else {
  const mQ = matchMedia?.("(pointer:coarse)");
  if (mQ?.media === "(pointer:coarse)") {
    hasTouchScreen = !!mQ.matches;
  } else if ("orientation" in window) {
    hasTouchScreen = true; // deprecated, but good fallback
  } else {
    // Only as a last resort, fall back to user agent sniffing
    const UA = navigator.userAgent;
    hasTouchScreen =
      /\b(BlackBerry|webOS|iPhone|IEMobile)\b/i.test(UA) ||
      /\b(Android|Windows Phone|iPad|iPod)\b/i.test(UA);
  }
}

if (hasTouchScreen) {
  document.getElementById("exampleButton").style.padding = "1em";
}

至于屏幕尺寸,请使用 window.innerWidthwindow.addEventListener("resize", () => { /* Refresh screen size dependent things */ })。对于屏幕尺寸,您想要做的事情不是在较小的屏幕上删除信息。这只会惹恼人们,因为它会迫使他们使用桌面版本。相反,尝试在较小的屏幕上以更长的页面显示较少的列信息,而在较大的屏幕尺寸上以较短的页面显示更多列信息。此效果可以使用 CSS 弹性盒 轻松实现,有时可以使用 浮动 作为部分后备方案。

还要尝试将不太相关/重要的信息移到底部,并将页面的内容有意义地组合在一起。虽然这超出了主题范围,但以下详细示例可能会为您提供一些见解和想法,说服您放弃用户代理嗅探。让我们想象一个由信息框组成的页面;每个框都关于不同的猫科动物品种或犬科动物品种。每个框都有一个图像、一个概述和一个历史趣闻。即使在大屏幕上,图片也保持在最大合理尺寸。为了有意义地组合内容,所有猫盒都与所有狗盒分开,这样猫盒和狗盒就不会混合在一起。在大屏幕上,使用多列可以节省空间,以减少图片左侧和右侧浪费的空间。框可以通过两种同样公平的方法分成多列。从现在开始,我们将假设所有狗盒都在源代码的顶部,所有猫盒都在源代码的底部,并且所有这些盒都具有相同的父元素。当然,有一个狗盒实例紧挨着一个猫盒的上面。第一种方法使用水平 弹性盒 来组合内容,以便在向最终用户显示页面时,所有狗盒都在页面的顶部,所有猫盒都在页面的下方。第二种方法使用 布局,并将所有狗放在左侧,所有猫放在右侧。仅在这种特定情况下,为弹性盒/多列不提供后备方案是合适的,这会导致在旧版浏览器上显示一列非常宽的框。还要考虑以下几点。如果更多人访问网页以查看猫,那么最好将所有猫放在源代码中比狗更高的地方,以便更多人在内容折叠成一列的较小屏幕上更快地找到他们想要的内容。

接下来,始终使您的代码动态化。用户可以将他们的移动设备翻转到侧面,从而更改页面宽度和高度。或者,未来可能出现一些奇怪的类似翻盖手机的设备,翻转它可以扩展屏幕。不要成为那个因如何处理类似翻盖手机的设备而头痛的开发者。在您能够打开开发者工具侧边栏并在网页看起来平滑、流畅且动态调整大小的同时调整屏幕大小之前,永远不要对您的网页感到满意。最简单的方法是将所有根据屏幕尺寸移动内容的代码分离到一个单独的函数中,该函数在页面加载时以及此后的每个resize事件时被调用。如果此布局函数在确定页面的新布局之前进行了大量计算,则考虑去抖事件监听器,以便不那么频繁地调用它。还要注意,媒体查询(max-width: 25em)not all and (min-width: 25em)(max-width: 24.99em)之间存在巨大差异:(max-width: 25em)不包括(max-width: 25em),而not all and (min-width: 25em)包括(max-width: 25em)(max-width: 24.99em)not all and (min-width: 25em)的简陋版本:不要使用(max-width: 24.99em),因为将来在非常高分辨率设备上的非常大的字体大小下布局*可能*会中断。始终非常谨慎地选择正确的媒体查询,并在任何相应的 JavaScript 中选择正确的>=<=><,因为这些很容易混淆,导致网站在布局更改的屏幕尺寸处看起来怪异。因此,在布局发生变化的确切宽度/高度处彻底测试网站,以确保布局更改正确发生。

充分利用用户代理嗅探

在回顾了所有上述用户代理嗅探的更好替代方案之后,仍然存在一些可能的情况下,用户代理嗅探是合适且合理的。

其中一个案例是将用户代理嗅探用作检测设备是否具有触摸屏时的后备方案。有关更多信息,请参阅移动设备检测部分。

另一个此类案例是修复无法自动更新的浏览器中的错误。WebKit(在 iOS 上)就是一个完美的例子。Apple 强制 iOS 上的所有浏览器在内部使用 WebKit,因此用户无法在旧设备上获得更好、更新的浏览器。大多数错误都可以检测到,但有些错误比其他错误更难检测。在这种情况下,使用用户代理嗅探来节省性能可能是有益的。例如,WebKit 6 存在一个错误,即当设备方向发生变化时,浏览器可能不会在应该时触发MediaQueryList监听器。要克服此错误,请查看下面的代码。

js
const UA = navigator.userAgent;
const isWebkit =
  /\b(iPad|iPhone|iPod)\b/.test(UA) &&
  /WebKit/.test(UA) &&
  !/Edge/.test(UA) &&
  !window.MSStream;

let mediaQueryUpdated = true;
const mqL = [];

function whenMediaChanges() {
  mediaQueryUpdated = true;
}

const listenToMediaQuery = isWebkit
  ? (mQ, f) => {
      if (/height|width/.test(mQ.media)) {
        mqL.push([mQ, f]);
      }
      mQ.addListener(f);
      mQ.addListener(whenMediaChanges);
    }
  : () => {};

const destroyMediaQuery = isWebkit
  ? (mQ) => {
      for (let i = 0; i < mqL.length; i++) {
        if (mqL[i][0] === mQ) {
          mqL.splice(i, 1);
        }
      }
      mQ.removeListener(whenMediaChanges);
    }
  : listenToMediaQuery;

let orientationChanged = false;
addEventListener(
  "orientationchange",
  () => {
    orientationChanged = true;
  },
  PASSIVE_LISTENER_OPTION,
);

addEventListener("resize", () =>
  setTimeout(() => {
    if (orientationChanged && !mediaQueryUpdated) {
      for (let i = 0; i < mqL.length; i++) {
        mqL[i][1](mqL[i][0]);
      }
    }
    mediaQueryUpdated = orientationChanged = false;
  }, 0),
);

用户代理的哪个部分包含您要查找的信息?

由于用户代理字符串的不同部分没有统一性,所以这是棘手的部分。

浏览器名称和版本

当人们说他们想要“浏览器检测”时,通常他们实际上想要“渲染引擎检测”。您是否真的想检测 Firefox(而不是 SeaMonkey),或者检测 Chrome(而不是 Chromium)?或者您是否真的想查看浏览器是否正在使用 Gecko 或 WebKit 渲染引擎?如果这是您需要的,请参阅页面下方的内容。

大多数浏览器都以BrowserName/VersionNumber格式设置名称和版本。但是,由于名称不是用户代理字符串中唯一以该格式存在的信息,因此您无法发现浏览器的名称,只能检查您要查找的名称是否存在。但请注意,有些浏览器是撒谎的:例如,Chrome 同时报告为 Chrome 和 Safari。因此,要检测 Safari,您必须检查 Safari 字符串和 Chrome 字符串的不存在,Chromium 通常也将其自身报告为 Chrome,或者 Seamonkey 有时将其自身报告为 Firefox。

此外,请注意不要在 BrowserName 上使用简单的正则表达式,用户代理还包含 Keyword/Value 语法之外的字符串。例如,Safari 和 Chrome 包含字符串“like Gecko”。

浏览器名称 必须包含 必须不包含
Firefox Firefox/xyz Seamonkey/xyz
Seamonkey Seamonkey/xyz
Chrome Chrome/xyz Chromium/xyzEdg.*/xyz
Chromium Chromium/xyz
Safari Safari/xyz Chrome/xyzChromium/xyz
Opera 15+(基于 Blink 的引擎) OPR/xyz
Opera 12-(基于 Presto 的引擎) Opera/xyz

[1] Safari 提供两个版本号:一个技术版本号在Safari/xyz标记中,一个用户友好版本号在Version/xyz标记中。

当然,绝对不能保证其他浏览器不会劫持其中的一些内容(就像 Chrome 过去劫持了 Safari 字符串一样)。这就是为什么使用用户代理字符串进行浏览器检测不可靠,并且应该只与版本号检查一起使用(不太可能劫持旧版本)。

渲染引擎

如前所述,在大多数情况下,查找渲染引擎是更好的方法。这将有助于不排除鲜为人知的浏览器。共享通用渲染引擎的浏览器将以相同的方式显示页面:通常可以合理地假设在一个浏览器中起作用的内容在另一个浏览器中也会起作用。

有三个活跃的主要渲染引擎:Blink、Gecko 和 WebKit。由于嗅探渲染引擎名称很常见,因此许多用户代理添加了其他渲染名称以触发检测。因此,在检测渲染引擎时要注意不要触发误报非常重要。

引擎 必须包含 评论
Blink Chrome/xyz
Gecko Gecko/xyz
WebKit AppleWebKit/xyz 请注意,WebKit 浏览器添加了“like Gecko”字符串,如果检测不仔细,可能会触发 Gecko 的误报。
Presto Opera/xyz 已过时;Presto 不再用于 Opera 浏览器版本 >= 15 的构建(请参阅“Blink”)
EdgeHTML Edge/xyz 非 Chromium 版 Edge 将其引擎版本放在Edge/标记之后,而不是应用程序版本之后。已过时;EdgeHTML 不再用于 Edge 浏览器版本 >= 79 的构建(请参阅“Blink”)。

渲染引擎版本

大多数渲染引擎将版本号放在RenderingEngine/VersionNumber标记中,Gecko 是一个显著的例外。Gecko 将 Gecko 版本号放在 User Agent 的注释部分,位于rv:字符串之后。从移动版本的 Gecko 14 和桌面版本的 Gecko 17 开始,它也将在Gecko/version标记中放置此值(先前版本在那里放置构建日期,然后是称为 GeckoTrail 的固定日期)。

操作系统

操作系统在大多数用户代理字符串中给出(尽管不是像 Firefox OS 这样的以 Web 为中心的平台),但格式差异很大。它是 User Agent 注释部分中两个分号之间的固定字符串。这些字符串对每个浏览器都是特定的。它们指示操作系统,而且通常还指示其版本以及依赖硬件的信息(32 位或 64 位、Mac 的 Intel/PPC 或 Windows PC 的 x86/ARM CPU 架构)。

与所有情况一样,这些字符串将来可能会更改,应该只与已发布浏览器的检测结合使用。当新的浏览器版本发布时,必须制定技术调查以调整脚本。

移动设备、平板电脑或台式机

执行用户代理嗅探最常见的原因是确定浏览器运行在何种类型的设备上。目标是为不同的设备类型提供不同的 HTML。

  • 永远不要假设浏览器或渲染引擎只运行在一类设备上。尤其不要为不同的浏览器或渲染引擎设置不同的默认值。
  • 永远不要使用 OS 标记来定义浏览器是否在移动设备、平板电脑或台式机上运行。OS 可能运行在多种类型的设备上(例如,Android 既可以在平板电脑上运行,也可以在手机上运行)。

下表总结了常见浏览器供应商指示其浏览器在移动设备上运行的方式

浏览器 规则 示例
Mozilla(Gecko,Firefox) 注释中包含MobileTablet Mozilla/5.0 (Android; Mobile; rv:13.0) Gecko/13.0 Firefox/13.0
基于 WebKit 的(Android,Safari) 注释外部的Mobile Safari标记。 Mozilla/5.0 (Linux; U; Android 4.0.3; de-ch; HTC Sensation Build/IML74K) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30
基于 Blink 的(Chromium,Google Chrome,Opera 15+,Android 上的 Edge) 注释外部的Mobile Safari标记。 Mozilla/5.0 (Linux; Android 4.4.2; Nexus 5 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.117 Mobile Safari/537.36 OPR/20.0.1396.72047
基于 Presto 的(Opera 12-) 注释内部的Opera Mobi/xyz标记。 Opera/9.80 (Android 2.3.3; Linux; Opera Mobi/ADR-1111101157; U; es-ES) Presto/2.9.201 Version/11.50
Windows 10 Mobile 上的 Edge 注释外部的Mobile/xyzEdge/标记。 Mozilla/5.0 (Windows Phone 10.0; Android 6.0.1; Xbox; Xbox One) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Mobile Safari/537.36 Edge/16.16299

总而言之,我们建议在 User Agent 中的任何位置查找字符串Mobi以检测移动设备。

注意:如果设备足够大,以至于没有用Mobi标记,则应提供您的桌面网站(作为最佳实践,无论如何都应支持触摸输入,因为越来越多的台式机配备了触摸屏)。