使用使用者代理進行瀏覽器檢測

為不同的瀏覽器提供不同的網頁或服務通常不是一個好主意。網路旨在讓每個人都能訪問,無論他們使用的是什麼瀏覽器或裝置。有一些方法可以開發您的網站,使其基於功能的可用性逐步增強自身,而不是針對特定的瀏覽器。

但是瀏覽器和標準並不完美,仍然存在一些需要檢測瀏覽器才能解決的極端情況。使用使用者代理來檢測瀏覽器看起來很簡單,但實際上做好這一點非常困難。本文件將指導您儘可能正確地執行此操作。

注意:值得重申的是:使用使用者代理嗅探很少是一個好主意。您幾乎總能找到更好的、更廣泛相容的方法來解決您的問題!

使用瀏覽器檢測之前的注意事項

在考慮使用使用者代理字串來檢測正在使用的瀏覽器時,您的第一步是嘗試儘可能避免它。首先嚐試確定您為什麼要這樣做。

您是否試圖解決某個瀏覽器版本中的特定錯誤?

在專門的論壇中查詢或提問:您不太可能是第一個遇到此問題的人。此外,專家或擁有其他觀點的人可以為您提供解決此錯誤的方法。如果問題看起來不常見,值得檢查此錯誤是否已透過其錯誤跟蹤系統報告給瀏覽器供應商(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標記,則應提供您的桌面網站(作為最佳實踐,無論如何都應支援觸控輸入,因為越來越多的桌上型電腦配備了觸控式螢幕)。