JS 浏览器 / App WebView 检测工具

检测浏览器信息,具体代码:

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>getBrowserInfo() --- Documentation</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=DM+Mono:wght@300;400;500&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
<style>
  :root {
    --bg: #faf9f6;
    --surface: #ffffff;
    --surface-alt: #f3f1ec;
    --border: #e4e0d8;
    --border-faint: #ece9e2;
    --accent: #1a6b3c;
    --accent-light: #1a6b3c14;
    --accent-mid: #1a6b3c30;
    --warn: #b44d00;
    --info: #1a5fb4;
    --info-light: #1a5fb412;
    --purple: #7c3aed;
    --text-primary: #1a1a18;
    --text-secondary: #6b6860;
    --text-muted: #9e9a90;
    --code-bg: #1b1b1f;
    --code-text: #e0ddd6;
    --code-key: #7eb8ff;
    --code-string: #6dd4a0;
    --code-bool: #ffb86c;
    --code-comment: #5c5a54;
    --code-tag: #ff7eb6;
    --code-attr: #c9a0dc;
    --code-punct: #888478;
    --radius: 8px;
    --radius-lg: 14px;
    --font-serif: 'Instrument Serif', Georgia, serif;
    --font-mono: 'DM Mono', 'Menlo', monospace;
    --font-sans: 'Inter', -apple-system, sans-serif;
    --sidebar-w: 260px;
    --header-h: 64px;
  }

  * { margin: 0; padding: 0; box-sizing: border-box; }

  html { scroll-behavior: smooth; scroll-padding-top: calc(var(--header-h) + 24px); }

  body {
    background: var(--bg);
    color: var(--text-primary);
    font-family: var(--font-sans);
    font-size: 15px;
    line-height: 1.75;
    -webkit-font-smoothing: antialiased;
  }

  ::selection { background: var(--accent-mid); }

  /* ─── Top Bar ─── */
  .topbar {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    height: var(--header-h);
    background: rgba(250,249,246,0.88);
    backdrop-filter: blur(16px) saturate(1.4);
    -webkit-backdrop-filter: blur(16px) saturate(1.4);
    border-bottom: 1px solid var(--border-faint);
    z-index: 100;
    display: flex;
    align-items: center;
    padding: 0 32px;
    gap: 24px;
  }

  .topbar__logo {
    font-family: var(--font-mono);
    font-size: 14px;
    font-weight: 500;
    color: var(--accent);
    white-space: nowrap;
    text-decoration: none;
    display: flex;
    align-items: center;
    gap: 10px;
  }

  .topbar__logo-icon {
    width: 28px;
    height: 28px;
    background: var(--accent);
    border-radius: 6px;
    display: flex;
    align-items: center;
    justify-content: center;
    color: #fff;
    font-size: 13px;
    font-weight: 600;
  }

  .topbar__sep {
    width: 1px;
    height: 20px;
    background: var(--border);
  }

  .topbar__version {
    font-family: var(--font-mono);
    font-size: 12px;
    color: var(--text-muted);
    background: var(--surface-alt);
    padding: 3px 10px;
    border-radius: 100px;
    border: 1px solid var(--border-faint);
  }

  .topbar__nav {
    display: flex;
    gap: 4px;
    margin-left: auto;
  }

  .topbar__nav a {
    font-size: 13px;
    font-weight: 500;
    color: var(--text-secondary);
    text-decoration: none;
    padding: 6px 14px;
    border-radius: 6px;
    transition: all 0.2s ease;
  }

  .topbar__nav a:hover { color: var(--text-primary); background: var(--surface-alt); }
  .topbar__nav a.active { color: var(--accent); background: var(--accent-light); }

  /* ─── Sidebar ─── */
  .sidebar {
    position: fixed;
    top: var(--header-h);
    left: 0;
    width: var(--sidebar-w);
    height: calc(100vh - var(--header-h));
    overflow-y: auto;
    padding: 28px 20px 40px;
    border-right: 1px solid var(--border-faint);
    z-index: 50;
    background: var(--bg);
  }

  .sidebar::-webkit-scrollbar { width: 4px; }
  .sidebar::-webkit-scrollbar-track { background: transparent; }
  .sidebar::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }

  .sidebar__group {
    margin-bottom: 28px;
  }

  .sidebar__group-title {
    font-family: var(--font-mono);
    font-size: 10px;
    font-weight: 500;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--text-muted);
    margin-bottom: 10px;
    padding-left: 12px;
  }

  .sidebar__link {
    display: block;
    font-size: 13px;
    font-weight: 400;
    color: var(--text-secondary);
    text-decoration: none;
    padding: 6px 12px;
    border-radius: 6px;
    transition: all 0.15s ease;
    line-height: 1.5;
  }

  .sidebar__link:hover { color: var(--text-primary); background: var(--surface-alt); }

  .sidebar__link.active {
    color: var(--accent);
    background: var(--accent-light);
    font-weight: 500;
  }

  /* ─── Main Content ─── */
  .main {
    margin-left: var(--sidebar-w);
    padding: calc(var(--header-h) + 40px) 56px 100px;
    max-width: calc(var(--sidebar-w) + 820px);
  }

  @media (max-width: 1000px) {
    .sidebar { display: none; }
    .main { margin-left: 0; padding: calc(var(--header-h) + 24px) 20px 80px; }
  }

  /* ─── Typography ─── */
  h1 {
    font-family: var(--font-serif);
    font-weight: 400;
    font-size: clamp(36px, 4vw, 52px);
    line-height: 1.1;
    letter-spacing: -0.025em;
    margin-bottom: 12px;
  }

  h1 em {
    font-style: italic;
    color: var(--accent);
  }

  .subtitle {
    font-size: 17px;
    font-weight: 400;
    color: var(--text-secondary);
    line-height: 1.7;
    margin-bottom: 40px;
    max-width: 560px;
  }

  h2 {
    font-family: var(--font-serif);
    font-weight: 400;
    font-size: 30px;
    line-height: 1.2;
    letter-spacing: -0.02em;
    margin-top: 56px;
    margin-bottom: 16px;
    padding-top: 24px;
    border-top: 1px solid var(--border-faint);
    scroll-margin-top: calc(var(--header-h) + 24px);
  }

  h2:first-of-type { border-top: none; padding-top: 0; margin-top: 0; }

  h3 {
    font-family: var(--font-sans);
    font-weight: 600;
    font-size: 17px;
    margin-top: 36px;
    margin-bottom: 10px;
    color: var(--text-primary);
  }

  p { margin-bottom: 14px; color: var(--text-secondary); }

  strong { color: var(--text-primary); font-weight: 600; }

  a { color: var(--accent); text-decoration: none; }
  a:hover { text-decoration: underline; }

  /* ─── Code Blocks ─── */
  .code-block {
    position: relative;
    background: var(--code-bg);
    border-radius: var(--radius-lg);
    padding: 24px 28px;
    margin: 18px 0 24px;
    overflow-x: auto;
    font-family: var(--font-mono);
    font-size: 13px;
    line-height: 1.8;
    color: var(--code-text);
    border: 1px solid #2a2a30;
  }

  .code-block__lang {
    position: absolute;
    top: 10px;
    right: 14px;
    font-size: 10px;
    font-weight: 500;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: #555;
    pointer-events: none;
  }

  .code-block__copy {
    position: absolute;
    top: 8px;
    right: 60px;
    font-family: var(--font-mono);
    font-size: 11px;
    padding: 4px 10px;
    border-radius: 5px;
    border: 1px solid #333;
    background: transparent;
    color: #666;
    cursor: pointer;
    transition: all 0.2s;
    opacity: 0;
  }

  .code-block:hover .code-block__copy { opacity: 1; }
  .code-block__copy:hover { color: var(--code-text); border-color: #555; }
  .code-block__copy.copied { color: var(--code-string); border-color: var(--code-string); }

  code {
    font-family: var(--font-mono);
    font-size: 13px;
    background: var(--surface-alt);
    border: 1px solid var(--border-faint);
    padding: 2px 7px;
    border-radius: 5px;
    color: var(--accent);
  }

  .code-block code {
    background: transparent;
    border: none;
    padding: 0;
    color: inherit;
    font-size: inherit;
  }

  /* Syntax colors */
  .ck { color: var(--code-key); }
  .cs { color: var(--code-string); }
  .cb { color: var(--code-bool); }
  .cc { color: var(--code-comment); font-style: italic; }
  .ct { color: var(--code-tag); }
  .ca { color: var(--code-attr); }
  .cp { color: var(--code-punct); }
  .cn { color: #ff9f43; }
  .cf { color: #c9a0dc; }

  /* ─── Tables ─── */
  .table-wrap {
    overflow-x: auto;
    margin: 16px 0 24px;
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
  }

  table {
    width: 100%;
    border-collapse: collapse;
    font-size: 13px;
  }

  thead th {
    text-align: left;
    font-family: var(--font-mono);
    font-size: 11px;
    font-weight: 500;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-muted);
    padding: 12px 18px;
    background: var(--surface-alt);
    border-bottom: 1px solid var(--border);
    white-space: nowrap;
  }

  tbody td {
    padding: 10px 18px;
    border-bottom: 1px solid var(--border-faint);
    color: var(--text-secondary);
    vertical-align: top;
  }

  tbody tr:last-child td { border-bottom: none; }
  tbody tr:hover { background: var(--surface-alt); }

  td code {
    font-size: 12px;
  }

  /* ─── Info / Warning Boxes ─── */
  .callout {
    display: flex;
    gap: 14px;
    padding: 18px 22px;
    border-radius: var(--radius);
    margin: 18px 0;
    font-size: 14px;
    line-height: 1.7;
  }

  .callout__icon {
    font-size: 16px;
    flex-shrink: 0;
    margin-top: 2px;
  }

  .callout--info {
    background: var(--info-light);
    border: 1px solid #1a5fb422;
    color: var(--info);
  }

  .callout--warn {
    background: #b44d0010;
    border: 1px solid #b44d0020;
    color: var(--warn);
  }

  .callout--tip {
    background: var(--accent-light);
    border: 1px solid var(--accent-mid);
    color: var(--accent);
  }

  /* ─── Feature Grid ─── */
  .feature-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: 14px;
    margin: 20px 0 28px;
  }

  .feature-card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 20px;
    transition: all 0.2s ease;
  }

  .feature-card:hover {
    border-color: var(--accent-mid);
    box-shadow: 0 4px 16px rgba(0,0,0,0.04);
  }

  .feature-card__icon {
    width: 36px;
    height: 36px;
    background: var(--accent-light);
    border-radius: 8px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 18px;
    margin-bottom: 12px;
  }

  .feature-card__title {
    font-weight: 600;
    font-size: 14px;
    margin-bottom: 6px;
    color: var(--text-primary);
  }

  .feature-card__desc {
    font-size: 13px;
    color: var(--text-muted);
    line-height: 1.6;
  }

  /* ─── Divider ─── */
  hr {
    border: none;
    border-top: 1px solid var(--border-faint);
    margin: 48px 0;
  }

  /* ─── Badge ─── */
  .badge {
    display: inline-block;
    font-family: var(--font-mono);
    font-size: 10px;
    font-weight: 500;
    letter-spacing: 0.06em;
    padding: 3px 10px;
    border-radius: 100px;
    vertical-align: middle;
    margin-left: 8px;
  }

  .badge--app {
    background: var(--accent-light);
    color: var(--accent);
    border: 1px solid var(--accent-mid);
  }

  .badge--browser {
    background: var(--info-light);
    color: var(--info);
    border: 1px solid #1a5fb422;
  }

  .badge--new {
    background: #7c3aed12;
    color: var(--purple);
    border: 1px solid #7c3aed22;
  }

  /* ─── Param list ─── */
  .param-list {
    margin: 14px 0 24px;
  }

  .param-item {
    display: grid;
    grid-template-columns: 160px 1fr;
    gap: 12px;
    padding: 12px 0;
    border-bottom: 1px solid var(--border-faint);
    font-size: 14px;
    align-items: baseline;
  }

  .param-item:last-child { border-bottom: none; }

  .param-item__name {
    font-family: var(--font-mono);
    font-size: 13px;
    color: var(--accent);
    font-weight: 500;
  }

  .param-item__type {
    font-family: var(--font-mono);
    font-size: 11px;
    color: var(--text-muted);
    margin-left: 6px;
  }

  .param-item__desc {
    color: var(--text-secondary);
  }

  /* ─── Footer ─── */
  .doc-footer {
    margin-top: 64px;
    padding-top: 24px;
    border-top: 1px solid var(--border-faint);
    display: flex;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 12px;
    font-size: 12px;
    color: var(--text-muted);
  }

  /* ─── UL ─── */
  ul, ol {
    padding-left: 24px;
    margin-bottom: 14px;
    color: var(--text-secondary);
  }

  li { margin-bottom: 6px; }

  /* ─── Animations ─── */
  .fade-in {
    opacity: 0;
    transform: translateY(12px);
    animation: fadeIn 0.5s ease forwards;
  }

  @keyframes fadeIn {
    to { opacity: 1; transform: translateY(0); }
  }

  .fade-in:nth-child(2) { animation-delay: 0.05s; }
  .fade-in:nth-child(3) { animation-delay: 0.1s; }
  .fade-in:nth-child(4) { animation-delay: 0.15s; }

  /* ─── Scrollbar ─── */
  ::-webkit-scrollbar { width: 6px; }
  ::-webkit-scrollbar-track { background: transparent; }
  ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
</style>
</head>
<body>

<!-- ═══════════════ Top Bar ═══════════════ -->
<div class="topbar">
  <a href="#" class="topbar__logo">
    <span class="topbar__logo-icon">B</span>
    getBrowserInfo
  </a>
  <span class="topbar__sep"></span>
  <span class="topbar__version">v1.0.0</span>
  <nav class="topbar__nav">
    <a href="#overview" class="active">Docs</a>
    <a href="#api">API</a>
    <a href="#registry">Registry</a>
  </nav>
</div>

<!-- ═══════════════ Sidebar ═══════════════ -->
<aside class="sidebar">
  <div class="sidebar__group">
    <div class="sidebar__group-title">Getting Started</div>
    <a href="#overview" class="sidebar__link active">Overview</a>
    <a href="#installation" class="sidebar__link">Installation</a>
    <a href="#quick-start" class="sidebar__link">Quick Start</a>
  </div>
  <div class="sidebar__group">
    <div class="sidebar__group-title">Core API</div>
    <a href="#api" class="sidebar__link">getBrowserInfo()</a>
    <a href="#return-value" class="sidebar__link">Return Value</a>
    <a href="#properties" class="sidebar__link">Property Reference</a>
  </div>
  <div class="sidebar__group">
    <div class="sidebar__group-title">Shortcut Methods</div>
    <a href="#app-detection" class="sidebar__link">App Detection</a>
    <a href="#platform-detection" class="sidebar__link">Platform Detection</a>
  </div>
  <div class="sidebar__group">
    <div class="sidebar__group-title">Detection Engine</div>
    <a href="#registry" class="sidebar__link">Registry (30+ engines)</a>
    <a href="#platforms" class="sidebar__link">Platforms</a>
    <a href="#devices" class="sidebar__link">Device Types</a>
    <a href="#network" class="sidebar__link">Network Detection</a>
  </div>
  <div class="sidebar__group">
    <div class="sidebar__group-title">Advanced</div>
    <a href="#server-side" class="sidebar__link">Server-Side Usage</a>
    <a href="#custom-registry" class="sidebar__link">Extending the Registry</a>
    <a href="#best-practices" class="sidebar__link">Best Practices</a>
    <a href="#limitations" class="sidebar__link">Limitations</a>
  </div>
</aside>

<!-- ═══════════════ Main Content ═══════════════ -->
<main class="main">

  <!-- ── Overview ── -->
  <section id="overview" class="fade-in">
    <h1>getBrowserInfo<em>()</em></h1>
    <p class="subtitle">
      A zero-dependency browser and App WebView detection library for the Chinese and global ecosystem.
      Identifies 30+ App WebViews and standalone browsers, detects platform, device type, and network conditions --- in both client and server environments.
    </p>

    <div class="feature-grid">
      <div class="feature-card">
        <div class="feature-card__icon">🔍</div>
        <div class="feature-card__title">30+ Engines</div>
        <div class="feature-card__desc">WeChat, Alipay, DingTalk, Douyin, Taobao, QQ, Weibo, Facebook, Instagram and more</div>
      </div>
      <div class="feature-card">
        <div class="feature-card__icon">🌐</div>
        <div class="feature-card__title">Cross-Platform</div>
        <div class="feature-card__desc">Android, iOS, Windows, macOS, Linux --- with Mobile / Tablet / PC device detection</div>
      </div>
      <div class="feature-card">
        <div class="feature-card__icon">⚡</div>
        <div class="feature-card__title">Zero Dependencies</div>
        <div class="feature-card__desc">Pure JavaScript, UMD module --- works with ES modules, CommonJS, and script tags</div>
      </div>
      <div class="feature-card">
        <div class="feature-card__icon">🖥️</div>
        <div class="feature-card__title">Server-Side Ready</div>
        <div class="feature-card__desc">Pass a custom UA string for server-side detection in Node.js, Deno, or any runtime</div>
      </div>
    </div>
  </section>

  <!-- ── Installation ── -->
  <section id="installation">
    <h2>Installation</h2>

    <h3>Via npm</h3>
    <div class="code-block">
      <span class="code-block__lang">sh</span>
      <pre><code>npm install get-browser-info</code></pre>
    </div>

    <h3>Via CDN (script tag)</h3>
    <div class="code-block">
      <span class="code-block__lang">html</span>
      <pre><code><span class="ct">&lt;script</span> <span class="ca">src</span>=<span class="cs">"https://unpkg.com/get-browser-info/dist/index.min.js"</span><span class="ct">&gt;&lt;/script&gt;</span>
<span class="cc">&lt;!-- Exposes window.getBrowserInfo globally --&gt;</span></code></pre>
    </div>

    <h3>Direct Copy</h3>
    <p>Since the library has <strong>zero dependencies</strong>, you can also copy the source file directly into your project. It self-registers via UMD and works in any module system.</p>
  </section>

  <!-- ── Quick Start ── -->
  <section id="quick-start">
    <h2>Quick Start</h2>

    <h3>Client-Side (auto-detect current environment)</h3>
    <div class="code-block">
      <span class="code-block__lang">js</span>
      <pre><code><span class="ck">const</span> info = <span class="cf">getBrowserInfo</span>();

console.<span class="cf">log</span>(info.name);      <span class="cc">// "WeChat"</span>
console.<span class="cf">log</span>(info.version);   <span class="cc">// "8.0.31"</span>
console.<span class="cf">log</span>(info.type);      <span class="cc">// "app"</span>
console.<span class="cf">log</span>(info.inApp);     <span class="cc">// "WeChat"</span>
console.<span class="cf">log</span>(info.platform);  <span class="cc">// "Android"</span>
console.<span class="cf">log</span>(info.device);    <span class="cc">// "Mobile"</span>
console.<span class="cf">log</span>(info.isWebView); <span class="cc">// true</span>
console.<span class="cf">log</span>(info.network);   <span class="cc">// "4g"</span></code></pre>
    </div>

    <h3>Server-Side (pass a custom UA)</h3>
    <div class="code-block">
      <span class="code-block__lang">js</span>
      <pre><code><span class="ck">const</span> getBrowserInfo = <span class="cf">require</span>(<span class="cs">'get-browser-info'</span>);

<span class="cc">// In Express / Koa / Fastify middleware:</span>
app.<span class="cf">use</span>((<span class="ca">req</span>, <span class="ca">res</span>, <span class="ca">next</span>) <span class="ck">=&gt;</span> {
  req.browser = <span class="cf">getBrowserInfo</span>(req.headers[<span class="cs">'user-agent'</span>]);
  <span class="cf">next</span>();
});</code></pre>
    </div>

    <h3>Quick Boolean Checks</h3>
    <div class="code-block">
      <span class="code-block__lang">js</span>
      <pre><code><span class="ck">if</span> (getBrowserInfo.<span class="cf">isWeChat</span>()) {
  <span class="cc">// Redirect to WeChat-specific payment flow</span>
  <span class="cf">initWeChatPay</span>();
}

<span class="ck">if</span> (getBrowserInfo.<span class="cf">isMobile</span>()) {
  <span class="cc">// Serve mobile-optimized assets</span>
}</code></pre>
    </div>
  </section>

  <hr>

  <!-- ── API ── -->
  <section id="api">
    <h2>getBrowserInfo([customUA])</h2>
    <p>The main detection function. When called with no arguments, it reads <code>navigator.userAgent</code> from the current browser. Pass a UA string for server-side or testing use.</p>

    <h3>Parameters</h3>
    <div class="param-list">
      <div class="param-item">
        <div>
          <span class="param-item__name">customUA</span>
          <span class="param-item__type">string?</span>
        </div>
        <div class="param-item__desc">
          Optional. A User-Agent string to analyze. If omitted or falsy, uses <code>navigator.userAgent</code>.
          When a custom UA is provided, the <code>network</code> field is returned as an empty string (network info requires a live browser environment).
        </div>
      </div>
    </div>

    <h3>Returns</h3>
    <p>An <code>Object</code> containing the detection result (see <a href="#return-value">Return Value</a> below).</p>

    <h3 id="return-value">Return Value</h3>
    <div class="code-block">
      <span class="code-block__lang">ts</span>
      <pre><code>{
  <span class="ck">name</span>:      <span class="cs">string</span>,       <span class="cc">// Browser / WebView name</span>
  <span class="ck">version</span>:   <span class="cs">string</span>,       <span class="cc">// Version number</span>
  <span class="ck">type</span>:      <span class="cs">"app" | "browser" | "unknown"</span>,
  <span class="ck">inApp</span>:     <span class="cs">string | null</span>, <span class="cc">// Host app name (only for type="app")</span>
  <span class="ck">platform</span>:  <span class="cs">string</span>,       <span class="cc">// "Android" | "iOS" | "Windows" | "macOS" | "Linux" | "Unknown"</span>
  <span class="ck">device</span>:    <span class="cs">string</span>,       <span class="cc">// "Mobile" | "Tablet" | "PC"</span>
  <span class="ck">network</span>:   <span class="cs">string</span>,       <span class="cc">// "4g" | "3g" | "2g" | "slow-2g" | "wifi" | ""</span>
  <span class="ck">isWebView</span>: <span class="cs">boolean</span>,      <span class="cc">// true if type === "app"</span>
  <span class="ck">ua</span>:        <span class="cs">string</span>,       <span class="cc">// The original UA string used for detection</span>
}</code></pre>
    </div>
  </section>

  <!-- ── Properties ── -->
  <section id="properties">
    <h2>Property Reference</h2>

    <div class="table-wrap">
      <table>
        <thead>
          <tr>
            <th>Property</th>
            <th>Type</th>
            <th>Description</th>
          </tr>
        </thead>
        <tbody>
          <tr>
            <td><code>name</code></td>
            <td><code>string</code></td>
            <td>The detected browser or WebView name, e.g. <code>"WeChat"</code>, <code>"Chrome"</code>, <code>"Safari"</code>. Returns <code>"Unknown"</code> if no registry entry matches.</td>
          </tr>
          <tr>
            <td><code>version</code></td>
            <td><code>string</code></td>
            <td>The version extracted from the UA. Returns an empty string <code>""</code> if extraction fails or the engine has no version regex.</td>
          </tr>
          <tr>
            <td><code>type</code></td>
            <td><code>"app" | "browser" | "unknown"</code></td>
            <td><code>"app"</code> for in-app WebViews, <code>"browser"</code> for standalone browsers, <code>"unknown"</code> if nothing matched.</td>
          </tr>
          <tr>
            <td><code>inApp</code></td>
            <td><code>string | null</code></td>
            <td>The host application name when inside a WebView (e.g. <code>"WeChat"</code>, <code>"Douyin"</code>). <code>null</code> for standalone browsers.</td>
          </tr>
          <tr>
            <td><code>platform</code></td>
            <td><code>string</code></td>
            <td>Operating system: <code>"Android"</code>, <code>"iOS"</code>, <code>"Windows"</code>, <code>"macOS"</code>, <code>"Linux"</code>, or <code>"Unknown"</code>.</td>
          </tr>
          <tr>
            <td><code>device</code></td>
            <td><code>string</code></td>
            <td>Device form factor: <code>"Mobile"</code>, <code>"Tablet"</code>, or <code>"PC"</code>. Defaults to <code>"PC"</code> when no mobile/tablet indicators are found.</td>
          </tr>
          <tr>
            <td><code>network</code></td>
            <td><code>string</code></td>
            <td>Network type from the <a href="https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation" target="_blank" rel="noopener">Network Information API</a>. Values: <code>"4g"</code>, <code>"3g"</code>, <code>"2g"</code>, <code>"slow-2g"</code>, <code>"wifi"</code>. Returns <code>""</code> when using a custom UA or if the API is unavailable.</td>
          </tr>
          <tr>
            <td><code>isWebView</code></td>
            <td><code>boolean</code></td>
            <td>Convenience flag. <code>true</code> when <code>type === "app"</code>.</td>
          </tr>
          <tr>
            <td><code>ua</code></td>
            <td><code>string</code></td>
            <td>The raw User-Agent string used for detection. Useful for logging or further analysis.</td>
          </tr>
        </tbody>
      </table>
    </div>
  </section>

  <hr>

  <!-- ── Shortcut Methods ── -->
  <section id="app-detection">
    <h2>Shortcut Methods --- App Detection</h2>
    <p>All shortcuts accept an optional <code>ua</code> parameter. If omitted, the current browser's UA is used.</p>

    <div class="table-wrap">
      <table>
        <thead>
          <tr>
            <th>Method</th>
            <th>Signature</th>
            <th>Detects</th>
          </tr>
        </thead>
        <tbody>
          <tr>
            <td><code>isWeChat(ua?)</code></td>
            <td><code>boolean</code></td>
            <td>WeChat / 微信内置浏览器</td>
          </tr>
          <tr>
            <td><code>isAlipay(ua?)</code></td>
            <td><code>boolean</code></td>
            <td>Alipay / 支付宝内置浏览器</td>
          </tr>
          <tr>
            <td><code>isQQ(ua?)</code></td>
            <td><code>boolean</code></td>
            <td>QQ App 内置浏览器</td>
          </tr>
          <tr>
            <td><code>isWeibo(ua?)</code></td>
            <td><code>boolean</code></td>
            <td>Weibo / 微博内置浏览器</td>
          </tr>
          <tr>
            <td><code>isDingTalk(ua?)</code></td>
            <td><code>boolean</code></td>
            <td>DingTalk / 钉钉内置浏览器</td>
          </tr>
          <tr>
            <td><code>isDouyin(ua?)</code></td>
            <td><code>boolean</code></td>
            <td>Douyin / 抖音内置浏览器</td>
          </tr>
        </tbody>
      </table>
    </div>

    <div class="code-block">
      <span class="code-block__lang">js</span>
      <pre><code><span class="cc">// Client-side --- no arguments needed</span>
<span class="ck">if</span> (getBrowserInfo.<span class="cf">isWeChat</span>()) {
  <span class="cf">showWeChatGuide</span>();
}

<span class="cc">// Server-side --- pass the UA string</span>
<span class="ck">const</span> ua = req.headers[<span class="cs">'user-agent'</span>];
<span class="ck">if</span> (getBrowserInfo.<span class="cf">isAlipay</span>(ua)) {
  <span class="cf">redirect</span>(<span class="cs">'/alipay-callback'</span>);
}</code></pre>
    </div>
  </section>

  <!-- ── Platform Detection ── -->
  <section id="platform-detection">
    <h2>Shortcut Methods --- Platform Detection</h2>

    <div class="table-wrap">
      <table>
        <thead>
          <tr>
            <th>Method</th>
            <th>Signature</th>
            <th>Detects</th>
          </tr>
        </thead>
        <tbody>
          <tr>
            <td><code>isMobile(ua?)</code></td>
            <td><code>boolean</code></td>
            <td>Any mobile device (Android phone, iPhone, iPad, iPod)</td>
          </tr>
          <tr>
            <td><code>isAndroid(ua?)</code></td>
            <td><code>boolean</code></td>
            <td>Android platform (phones + tablets)</td>
          </tr>
          <tr>
            <td><code>isIOS(ua?)</code></td>
            <td><code>boolean</code></td>
            <td>iOS platform (iPhone, iPad, iPod)</td>
          </tr>
        </tbody>
      </table>
    </div>
  </section>

  <hr>

  <!-- ── Registry ── -->
  <section id="registry">
    <h2>Detection Registry</h2>
    <p>The engine uses a priority-ordered registry --- the <strong>first match wins</strong>. App WebViews are checked before standalone browsers, because many App WebViews inherit the underlying browser engine's UA tokens.</p>

    <h3>App WebViews <span class="badge badge--app">type: app</span></h3>
    <div class="table-wrap">
      <table>
        <thead>
          <tr>
            <th>Name</th>
            <th>Host App</th>
            <th>UA Test Pattern</th>
            <th>Version Pattern</th>
          </tr>
        </thead>
        <tbody>
          <tr><td><code>WeChat</code></td><td>WeChat</td><td><code>MicroMessenger</code></td><td><code>MicroMessenger/x.x.x</code></td></tr>
          <tr><td><code>Alipay</code></td><td>Alipay</td><td><code>AlipayClient</code></td><td><code>AlipayClient/x.x.x</code></td></tr>
          <tr><td><code>DingTalk</code></td><td>DingTalk</td><td><code>DingTalk</code></td><td><code>DingTalk/x.x.x</code></td></tr>
          <tr><td><code>Taobao</code></td><td>Taobao</td><td><code>AliApp(TB/</code></td><td><code>AliApp(TB/x.x.x</code></td></tr>
          <tr><td><code>Tmall</code></td><td>Tmall</td><td><code>AliApp(TM/</code></td><td><code>AliApp(TM/x.x.x</code></td></tr>
          <tr><td><code>Douyin</code></td><td>Douyin</td><td><code>aweme</code></td><td><code>aweme/x.x.x</code></td></tr>
          <tr><td><code>BytedanceWV</code></td><td>Bytedance</td><td><code>BytedanceWebview</code></td><td><code>BytedanceWebview/x.x.x</code></td></tr>
          <tr><td><code>Toutiao</code></td><td>Toutiao</td><td><code>NewsArticle</code></td><td><code>NewsArticle/x.x.x</code></td></tr>
          <tr><td><code>Weibo</code></td><td>Weibo</td><td><code>Weibo</code></td><td><code>Weibo_x.x.x</code></td></tr>
          <tr><td><code>BaiduApp</code></td><td>Baidu</td><td><code>baiduboxapp</code></td><td><code>baiduboxapp/x.x.x</code></td></tr>
          <tr><td><code>QQ</code></td><td>QQ</td><td><code>QQ/</code></td><td><code>QQ/x.x.x</code></td></tr>
          <tr><td><code>MQQBrowser</code></td><td>QQ</td><td><code>MQQBrowser</code></td><td><code>MQQBrowser/x.x.x</code></td></tr>
          <tr><td><code>KakaoTalk</code></td><td>KakaoTalk</td><td><code>KAKAOTALK</code></td><td><code>KAKAOTALK_x.x.x</code></td></tr>
          <tr><td><code>Facebook</code></td><td>Facebook</td><td><code>FBAN|FBAV</code></td><td><code>FBAV/x.x.x</code></td></tr>
          <tr><td><code>Instagram</code></td><td>Instagram</td><td><code>Instagram</code></td><td><code>Instagram x.x.x</code></td></tr>
          <tr><td><code>Line</code></td><td>Line</td><td><code>Line/</code></td><td><code>Line/x.x.x</code></td></tr>
        </tbody>
      </table>
    </div>

    <h3>Standalone Browsers <span class="badge badge--browser">type: browser</span></h3>
    <div class="table-wrap">
      <table>
        <thead>
          <tr>
            <th>Name</th>
            <th>Category</th>
            <th>UA Test Pattern</th>
            <th>Version Pattern</th>
          </tr>
        </thead>
        <tbody>
          <tr><td><code>Edge</code></td><td>Mainstream</td><td><code>Edg/</code></td><td><code>Edg/x.x.x</code></td></tr>
          <tr><td><code>Opera</code></td><td>Mainstream</td><td><code>OPR/</code></td><td><code>OPR/x.x.x</code></td></tr>
          <tr><td><code>Vivaldi</code></td><td>Mainstream</td><td><code>Vivaldi</code></td><td><code>Vivaldi/x.x.x</code></td></tr>
          <tr><td><code>Brave</code></td><td>Mainstream</td><td><code>Brave</code></td><td><code>Brave/x.x.x</code></td></tr>
          <tr><td><code>Yandex</code></td><td>Mainstream</td><td><code>Yandex</code></td><td><code>YaBrowser/x.x.x</code></td></tr>
          <tr><td><code>HuaweiBrowser</code></td><td>Manufacturer</td><td><code>HuaweiBrowser</code></td><td><code>HuaweiBrowser/x.x.x</code></td></tr>
          <tr><td><code>MiuiBrowser</code></td><td>Manufacturer</td><td><code>MiuiBrowser</code></td><td><code>MiuiBrowser/x.x.x</code></td></tr>
          <tr><td><code>SamsungBrowser</code></td><td>Manufacturer</td><td><code>SamsungBrowser</code></td><td><code>SamsungBrowser/x.x.x</code></td></tr>
          <tr><td><code>UCBrowser</code></td><td>Regional</td><td><code>UCBrowser</code></td><td><code>UCBrowser/x.x.x</code></td></tr>
          <tr><td><code>QQBrowser</code></td><td>Regional</td><td><code>QQBrowser</code></td><td><code>QQBrowser/x.x.x</code></td></tr>
          <tr><td><code>BaiduBrowser</code></td><td>Regional</td><td><code>BIDUBrowser</code></td><td><code>BIDUBrowser x.x.x</code></td></tr>
          <tr><td><code>SogouBrowser</code></td><td>Regional</td><td><code>MetaSr|Sogou</code></td><td><code>SogouMobileBrowser/x.x.x</code></td></tr>
          <tr><td><code>Quark</code></td><td>Regional</td><td><code>Quark</code></td><td><code>Quark/x.x.x</code></td></tr>
          <tr><td><code>360Browser</code></td><td>Regional</td><td><code>360Browser|QihooBrowser</code></td><td><code>360Browser/x.x.x</code></td></tr>
          <tr><td><code>Firefox</code></td><td>Mainstream</td><td><code>Firefox</code></td><td><code>Firefox/x.x.x</code></td></tr>
          <tr><td><code>Chrome</code></td><td>Mainstream</td><td><code>Chrome</code></td><td><code>Chrome/x.x.x</code></td></tr>
          <tr><td><code>Safari</code></td><td>Mainstream</td><td><code>Safari</code></td><td><code>Version/x.x.x</code></td></tr>
          <tr><td><code>IE</code></td><td>Legacy</td><td><code>Trident|MSIE</code></td><td><code>rv:x.x.x</code> or <code>MSIE x.x</code></td></tr>
        </tbody>
      </table>
    </div>

    <div class="callout callout--warn">
      <span class="callout__icon">⚠</span>
      <div><strong>Priority matters.</strong> Safari and Chrome are placed near the bottom of the registry because many App WebViews include <code>Safari</code> and <code>Chrome</code> tokens in their UA. The specific App patterns are matched first to avoid false positives.</div>
    </div>
  </section>

  <!-- ── Platforms ── -->
  <section id="platforms">
    <h2>Platform Detection</h2>
    <p>Platform detection checks the UA against known OS identifiers. Priority order:</p>
    <div class="table-wrap">
      <table>
        <thead>
          <tr><th>#</th><th>Platform</th><th>UA Pattern</th></tr>
        </thead>
        <tbody>
          <tr><td>1</td><td><code>Android</code></td><td><code>/Android/i</code></td></tr>
          <tr><td>2</td><td><code>iOS</code></td><td><code>/iPhone|iPad|iPod/i</code></td></tr>
          <tr><td>3</td><td><code>Windows</code></td><td><code>/Windows/i</code></td></tr>
          <tr><td>4</td><td><code>macOS</code></td><td><code>/Mac OS X/i</code></td></tr>
          <tr><td>5</td><td><code>Linux</code></td><td><code>/Linux/i</code></td></tr>
        </tbody>
      </table>
    </div>
  </section>

  <!-- ── Devices ── -->
  <section id="devices">
    <h2>Device Type Detection</h2>
    <p>Device detection uses a three-tier heuristic:</p>
    <div class="table-wrap">
      <table>
        <thead>
          <tr><th>#</th><th>Device</th><th>UA Pattern</th><th>Notes</th></tr>
        </thead>
        <tbody>
          <tr><td>1</td><td><code>Tablet</code></td><td><code>/iPad/i</code></td><td>iPad is always tablet</td></tr>
          <tr><td>2</td><td><code>Tablet</code></td><td><code>/Android(?!.*Mobile)/i</code></td><td>Android without "Mobile" token</td></tr>
          <tr><td>3</td><td><code>Mobile</code></td><td><code>/iPhone|iPod|Android.*Mobile/i</code></td><td>iPhone, iPod, or Android with "Mobile"</td></tr>
        </tbody>
      </table>
    </div>

    <div class="callout callout--info">
      <span class="callout__icon">ℹ</span>
      <div>If no device pattern matches (e.g., on desktop Windows/macOS), the fallback is <code>"PC"</code>.</div>
    </div>
  </section>

  <!-- ── Network ── -->
  <section id="network">
    <h2>Network Detection</h2>
    <p>When running in a browser environment with no custom UA, the library queries the <a href="https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation" target="_blank" rel="noopener">Network Information API</a> to detect the effective connection type.</p>

    <div class="table-wrap">
      <table>
        <thead>
          <tr><th>Value</th><th>Meaning</th></tr>
        </thead>
        <tbody>
          <tr><td><code>"slow-2g"</code></td><td>RTT &ge; 2000ms, downlink &lt; 50 Kbps</td></tr>
          <tr><td><code>"2g"</code></td><td>RTT &ge; 1400ms, downlink &lt; 70 Kbps</td></tr>
          <tr><td><code>"3g"</code></td><td>RTT &ge; 270ms, downlink &lt; 700 Kbps</td></tr>
          <tr><td><code>"4g"</code></td><td>RTT &lt; 100ms, downlink varies</td></tr>
          <tr><td><code>"wifi"</code></td><td>Wi-Fi connection (via <code>conn.type</code>)</td></tr>
          <tr><td><code>""</code></td><td>API unavailable or using custom UA</td></tr>
        </tbody>
      </table>
    </div>
  </section>

  <hr>

  <!-- ── Server-Side ── -->
  <section id="server-side">
    <h2>Server-Side Usage</h2>

    <h3>Node.js / Express</h3>
    <div class="code-block">
      <span class="code-block__lang">js</span>
      <pre><code><span class="ck">const</span> express = <span class="cf">require</span>(<span class="cs">'express'</span>);
<span class="ck">const</span> getBrowserInfo = <span class="cf">require</span>(<span class="cs">'get-browser-info'</span>);

<span class="ck">const</span> app = <span class="cf">express</span>();

<span class="cc">// Attach browser info to every request</span>
app.<span class="cf">use</span>((<span class="ca">req</span>, <span class="ca">res</span>, <span class="ca">next</span>) <span class="ck">=&gt;</span> {
  req.browserInfo = <span class="cf">getBrowserInfo</span>(req.headers[<span class="cs">'user-agent'</span>]);
  <span class="cf">next</span>();
});

app.<span class="cf">get</span>(<span class="cs">'/download'</span>, (<span class="ca">req</span>, <span class="ca">res</span>) <span class="ck">=&gt;</span> {
  <span class="ck">const</span> { platform, device, isWebView } = req.browserInfo;

  <span class="ck">if</span> (isWebView) {
    <span class="ck">return</span> res.<span class="cf">json</span>({
      message: <span class="cs">'Please open in your default browser'</span>,
      deepLink: platform === <span class="cs">'iOS'</span>
        ? <span class="cs">'https://apps.apple.com/app/xxx'</span>
        : <span class="cs">'https://play.google.com/store/apps/xxx'</span>
    });
  }

  <span class="cc">// Serve platform-specific download</span>
  <span class="ck">const</span> file = platform === <span class="cs">'iOS'</span> ? <span class="cs">'app.ipa'</span> : <span class="cs">'app.apk'</span>;
  res.<span class="cf">download</span>(file);
});</code></pre>
    </div>

    <h3>Koa</h3>
    <div class="code-block">
      <span class="code-block__lang">js</span>
      <pre><code><span class="ck">const</span> Koa = <span class="cf">require</span>(<span class="cs">'koa'</span>);
<span class="ck">const</span> getBrowserInfo = <span class="cf">require</span>(<span class="cs">'get-browser-info'</span>);

<span class="ck">const</span> app = <span class="ck">new</span> <span class="cf">Koa</span>();

app.<span class="cf">use</span>((<span class="ca">ctx</span>) <span class="ck">=&gt;</span> {
  <span class="ck">const</span> info = <span class="cf">getBrowserInfo</span>(ctx.headers[<span class="cs">'user-agent'</span>]);

  <span class="cc">// Redirect WeChat users to a custom page</span>
  <span class="ck">if</span> (info.inApp === <span class="cs">'WeChat'</span>) {
    ctx.<span class="cf">redirect</span>(<span class="cs">'/wechat-guide'</span>);
  }
});</code></pre>
    </div>

    <h3>Logging &amp; Analytics</h3>
    <div class="code-block">
      <span class="code-block__lang">js</span>
      <pre><code><span class="cc">// Collect browser stats for analytics</span>
<span class="ck">const</span> info = <span class="cf">getBrowserInfo</span>(ua);

analytics.<span class="cf">track</span>(<span class="cs">'page_view'</span>, {
  browser:   info.name,
  version:   info.version,
  platform:  info.platform,
  device:    info.device,
  isWebView: info.isWebView,
  hostApp:   info.inApp,
});</code></pre>
    </div>
  </section>

  <!-- ── Custom Registry ── -->
  <section id="custom-registry">
    <h2>Extending the Registry</h2>
    <p>The current architecture uses an internal <code>REGISTRY</code> array that isn't directly exported. If you need to add custom detections, the recommended approach is to fork the source and append entries to the registry:</p>

    <div class="code-block">
      <span class="code-block__lang">js</span>
      <pre><code><span class="cc">// Inside the REGISTRY array, add your custom entry:</span>
{
  <span class="ck">test</span>:    <span class="cs">/MyCustomApp/i</span>,
  <span class="ck">name</span>:    <span class="cs">'MyCustomApp'</span>,
  <span class="ck">version</span>: <span class="cs">/MyCustomApp\/([\d.]+)/</span>,
  <span class="ck">type</span>:    <span class="cs">'app'</span>,
  <span class="ck">inApp</span>:   <span class="cs">'MyCustomApp'</span>,
}</code></pre>
    </div>

    <h3>Registry Entry Schema</h3>
    <div class="table-wrap">
      <table>
        <thead>
          <tr><th>Field</th><th>Type</th><th>Required</th><th>Description</th></tr>
        </thead>
        <tbody>
          <tr>
            <td><code>test</code></td>
            <td><code>RegExp</code></td>
            <td>Yes</td>
            <td>Matches against the UA string. No capture groups needed.</td>
          </tr>
          <tr>
            <td><code>name</code></td>
            <td><code>string</code></td>
            <td>Yes</td>
            <td>The display name returned in <code>info.name</code>.</td>
          </tr>
          <tr>
            <td><code>version</code></td>
            <td><code>RegExp</code></td>
            <td>No</td>
            <td>Must contain exactly one capture group for the version number.</td>
          </tr>
          <tr>
            <td><code>type</code></td>
            <td><code>"app" | "browser"</code></td>
            <td>No</td>
            <td>Defaults to <code>"browser"</code>. Set to <code>"app"</code> for WebViews.</td>
          </tr>
          <tr>
            <td><code>inApp</code></td>
            <td><code>string</code></td>
            <td>No</td>
            <td>The host application name. Only meaningful when <code>type === "app"</code>.</td>
          </tr>
        </tbody>
      </table>
    </div>
  </section>

  <!-- ── Best Practices ── -->
  <section id="best-practices">
    <h2>Best Practices</h2>

    <h3>1. Check <code>isWebView</code> before JS SDKs</h3>
    <p>Many third-party JS SDKs (payment, social sharing, analytics) behave differently inside WebViews. Gate them with the detection result:</p>
    <div class="code-block">
      <span class="code-block__lang">js</span>
      <pre><code><span class="ck">const</span> info = <span class="cf">getBrowserInfo</span>();

<span class="ck">if</span> (info.isWebView) {
  <span class="cc">// Use native bridge or App-specific SDK</span>
  <span class="cf">initNativeBridge</span>(info.inApp);
} <span class="ck">else</span> {
  <span class="cc">// Use standard web SDKs</span>
  <span class="cf">initWebSDK</span>();
}</code></pre>
    </div>

    <h3>2. Use shortcuts for common checks</h3>
    <p>For single-app checks, shortcut methods are cleaner and more readable than comparing <code>info.name</code>:</p>
    <div class="code-block">
      <span class="code-block__lang">js</span>
      <pre><code><span class="cc">// Preferred</span>
<span class="ck">if</span> (getBrowserInfo.<span class="cf">isWeChat</span>()) { ... }

<span class="cc">// Also works, but less idiomatic</span>
<span class="ck">if</span> (<span class="cf">getBrowserInfo</span>().name === <span class="cs">'WeChat'</span>) { ... }</code></pre>
    </div>

    <h3>3. Cache the result</h3>
    <p>The detection is deterministic within a page session. Avoid calling <code>getBrowserInfo()</code> repeatedly in hot paths:</p>
    <div class="code-block">
      <span class="code-block__lang">js</span>
      <pre><code><span class="cc">// Good --- compute once, use everywhere</span>
<span class="ck">const</span> browserInfo = <span class="cf">getBrowserInfo</span>();

<span class="cc">// Avoid --- re-parses UA on every call</span>
<span class="ck">for</span> (<span class="ck">const</span> item <span class="ck">of</span> list) {
  <span class="ck">if</span> (<span class="cf">getBrowserInfo</span>().isWebView) { ... }
}</code></pre>
    </div>

    <h3>4. Combine with feature detection</h3>
    <p>UA detection tells you <em>what the browser claims to be</em>, not <em>what it supports</em>. Use it for routing and UX decisions, but pair it with feature detection for capability gating:</p>
    <div class="code-block">
      <span class="code-block__lang">js</span>
      <pre><code><span class="ck">const</span> info = <span class="cf">getBrowserInfo</span>();

<span class="cc">// UA detection for routing</span>
<span class="ck">if</span> (info.inApp === <span class="cs">'WeChat'</span>) {
  <span class="cf">loadWeChatJSSDK</span>();
}

<span class="cc">// Feature detection for capabilities</span>
<span class="ck">if</span> (<span class="cs">'IntersectionObserver'</span> <span class="ck">in</span> window) {
  <span class="cf">enableLazyLoading</span>();
}</code></pre>
    </div>
  </section>

  <!-- ── Limitations ── -->
  <section id="limitations">
    <h2>Limitations</h2>

    <div class="callout callout--warn">
      <span class="callout__icon">⚠</span>
      <div>
        <strong>User-Agent strings are self-reported and can be spoofed.</strong>
        Browsers and apps can modify their UA at will. This library performs <em>best-effort</em> detection based on common UA patterns --- it is not a security mechanism.
      </div>
    </div>

    <ul>
      <li><strong>UA Client Hints:</strong> Chrome is gradually reducing UA string granularity in favor of <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent" target="_blank" rel="noopener">User-Agent Client Hints</a>. This library relies on the full UA string and does not yet support Client Hints APIs.</li>
      <li><strong>Embedded browsers in new apps:</strong> As new apps launch with their own WebViews, the registry needs to be updated. Contributions are welcome.</li>
      <li><strong>Version extraction edge cases:</strong> Some UAs embed version numbers in non-standard formats. If <code>version</code> returns <code>""</code>, the UA likely doesn't match the expected format.</li>
      <li><strong>iPadOS 13+:</strong> iPadOS 13+ reports as macOS in its UA by default. The library may classify it as <code>macOS</code> / <code>PC</code> unless the UA includes the <code>iPad</code> token.</li>
      <li><strong>Network detection:</strong> The Network Information API is not available in all browsers (notably limited in Safari and Firefox). When unavailable, <code>network</code> returns <code>""</code>.</li>
    </ul>
  </section>

  <hr>

  <!-- ── License ── -->
  <div class="doc-footer">
    <span>MIT License &middot; getBrowserInfo v1.0.0</span>
    <span>Last updated: July 2025</span>
  </div>

</main>

<script>
// Sidebar active state on scroll
const sections = document.querySelectorAll('section[id]');
const sidebarLinks = document.querySelectorAll('.sidebar__link');

function updateActiveLink() {
  let current = '';
  sections.forEach(section => {
    const top = section.offsetTop - 100;
    if (window.scrollY >= top) {
      current = section.getAttribute('id');
    }
  });
  sidebarLinks.forEach(link => {
    link.classList.remove('active');
    if (link.getAttribute('href') === '#' + current) {
      link.classList.add('active');
    }
  });
}

window.addEventListener('scroll', updateActiveLink, { passive: true });
updateActiveLink();

// Copy button for code blocks
document.querySelectorAll('.code-block').forEach(block => {
  const btn = document.createElement('button');
  btn.className = 'code-block__copy';
  btn.textContent = 'Copy';
  btn.addEventListener('click', () => {
    const code = block.querySelector('pre').textContent;
    navigator.clipboard.writeText(code).then(() => {
      btn.textContent = 'Copied!';
      btn.classList.add('copied');
      setTimeout(() => {
        btn.textContent = 'Copy';
        btn.classList.remove('copied');
      }, 2000);
    });
  });
  block.appendChild(btn);
});

// Fade in on scroll
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      entry.target.style.opacity = '1';
      entry.target.style.transform = 'translateY(0)';
    }
  });
}, { threshold: 0.1 });

document.querySelectorAll('h2, .table-wrap, .code-block, .callout, .feature-grid, .param-list, ul, p').forEach(el => {
  el.style.opacity = '0';
  el.style.transform = 'translateY(14px)';
  el.style.transition = 'opacity 0.5s ease, transform 0.5s ease';
  observer.observe(el);
});
</script>
</body>
</html>

JS工具如下:

javascript 复制代码
/**
 * 浏览器 / App WebView 检测工具
 *
 * 功能:
 *   - 识别主流国内 App 内置 WebView(微信、支付宝、QQ、钉钉、淘宝、抖音等)
 *   - 识别主流独立浏览器(Chrome、Safari、Firefox、Edge 等)
 *   - 识别设备厂商浏览器(华为、小米、三星、UC 等)
 *   - 检测平台(Android / iOS / Windows / macOS / Linux)
 *   - 检测设备类型(Mobile / Tablet / PC)
 *   - 检测网络类型(4g / 3g / 2g / slow-2g)
 *   - 支持传入自定义 UA,可运行于服务端
 *
 * 用法:
 *   const info = getBrowserInfo();        // 客户端自动取 navigator.userAgent
 *   const info = getBrowserInfo(uaStr);   // 传入自定义 UA(服务端场景)
 *   getBrowserInfo.isWeChat();            // 快捷判断
 */

;
(function(root, factory) {
  if (typeof module !== 'undefined' && module.exports) {
    module.exports = factory();
  } else {
    root.getBrowserInfo = factory();
  }
})(typeof globalThis !== 'undefined' ? globalThis : this, function() {

  // ============================================
  // 浏览器 / App 注册表
  // ============================================
  //
  // 每一项结构:
  //   test    --- test(ua) 的正则,无需捕获组
  //   name    --- 浏览器 / App 名称
  //   version --- 提取版本号的正则,需带一个捕获组
  //   type    --- 'app' | 'browser',默认 'browser'
  //   inApp   --- 宿主 App 名称,仅 type='app' 时有效
  //
  // ⚠️ 顺序决定优先级:先匹配的先返回

  var REGISTRY = [

    // ────────────────────────────────────────
    // App 内置 WebView(优先级最高)
    // ────────────────────────────────────────

    { test: /MicroMessenger/i, name: 'WeChat', version: /MicroMessenger\/([\d.]+)/, type: 'app', inApp: 'WeChat' },
    { test: /AlipayClient/i, name: 'Alipay', version: /AlipayClient\/([\d.]+)/, type: 'app', inApp: 'Alipay' },
    { test: /DingTalk/i, name: 'DingTalk', version: /DingTalk\/([\d.]+)/, type: 'app', inApp: 'DingTalk' },
    { test: /AliApp\(TB\//i, name: 'Taobao', version: /AliApp\(TB\/([\d.]+)/, type: 'app', inApp: 'Taobao' },
    { test: /AliApp\(TM\//i, name: 'Tmall', version: /AliApp\(TM\/([\d.]+)/, type: 'app', inApp: 'Tmall' },
    { test: /aweme/i, name: 'Douyin', version: /aweme\/([\d.]+)/, type: 'app', inApp: 'Douyin' },
    { test: /BytedanceWebview/i, name: 'BytedanceWV', version: /BytedanceWebview\/([\d.]+)/, type: 'app',
      inApp: 'Bytedance' },
    { test: /NewsArticle/i, name: 'Toutiao', version: /NewsArticle\/([\d.]+)/, type: 'app', inApp: 'Toutiao' },
    { test: /Weibo/i, name: 'Weibo', version: /Weibo[_/]([\d.]+)/, type: 'app', inApp: 'Weibo' },
    { test: /baiduboxapp/i, name: 'BaiduApp', version: /baiduboxapp\/([\d.]+)/, type: 'app', inApp: 'Baidu' },
    { test: /QQ\//i, name: 'QQ', version: /QQ\/([\d.]+)/, type: 'app', inApp: 'QQ' },
    { test: /MQQBrowser/i, name: 'MQQBrowser', version: /MQQBrowser\/([\d.]+)/, type: 'app', inApp: 'QQ' },
    { test: /KAKAOTALK/i, name: 'KakaoTalk', version: /KAKAOTALK[_/]([\d.]+)/, type: 'app', inApp: 'KakaoTalk' },
    { test: /FBAN|FBAV/i, name: 'Facebook', version: /FBAV\/([\d.]+)/, type: 'app', inApp: 'Facebook' },
    { test: /Instagram/i, name: 'Instagram', version: /Instagram\s([\d.]+)/, type: 'app', inApp: 'Instagram' },
    { test: /Line\//i, name: 'Line', version: /Line\/([\d.]+)/, type: 'app', inApp: 'Line' },

    // ────────────────────────────────────────
    // 独立浏览器(从特殊到通用)
    // ────────────────────────────────────────

    { test: /Edg\//i, name: 'Edge', version: /Edg\/([\d.]+)/ },
    { test: /OPR\//i, name: 'Opera', version: /OPR\/([\d.]+)/ },
    { test: /Vivaldi/i, name: 'Vivaldi', version: /Vivaldi\/([\d.]+)/ },
    { test: /Brave/i, name: 'Brave', version: /Brave\/([\d.]+)/ },
    { test: /Yandex/i, name: 'Yandex', version: /YaBrowser\/([\d.]+)/ },
    { test: /HuaweiBrowser/i, name: 'HuaweiBrowser', version: /HuaweiBrowser\/([\d.]+)/ },
    { test: /MiuiBrowser/i, name: 'MiuiBrowser', version: /MiuiBrowser\/([\d.]+)/ },
    { test: /SamsungBrowser/i, name: 'SamsungBrowser', version: /SamsungBrowser\/([\d.]+)/ },
    { test: /UCBrowser/i, name: 'UCBrowser', version: /UCBrowser\/([\d.]+)/ },
    { test: /QQBrowser/i, name: 'QQBrowser', version: /QQBrowser\/([\d.]+)/ },
    { test: /BIDUBrowser/i, name: 'BaiduBrowser', version: /BIDUBrowser[\s/]([\d.]+)/ },
    { test: /MetaSr|Sogou/i, name: 'SogouBrowser', version: /SogouMobileBrowser\/([\d.]+)/ },
    { test: /Quark/i, name: 'Quark', version: /Quark\/([\d.]+)/ },
    { test: /360Browser|QihooBrowser/i, name: '360Browser', version: /(?:360Browser|QihooBrowser)\/([\d.]+)/ },
    { test: /Firefox/i, name: 'Firefox', version: /Firefox\/([\d.]+)/ },
    { test: /Chrome/i, name: 'Chrome', version: /Chrome\/([\d.]+)/ },
    { test: /Safari/i, name: 'Safari', version: /Version\/([\d.]+)/ },
    { test: /Trident|MSIE/i, name: 'IE', version: /(?:rv:|MSIE\s?)([\d.]+)/ },
  ];

  // 平台映射表
  var PLATFORM_MAP = [
    { test: /Android/i, name: 'Android' },
    { test: /iPhone|iPad|iPod/i, name: 'iOS' },
    { test: /Windows/i, name: 'Windows' },
    { test: /Mac OS X/i, name: 'macOS' },
    { test: /Linux/i, name: 'Linux' },
  ];

  // 设备类型映射表
  var DEVICE_MAP = [
    { test: /iPad/i, name: 'Tablet' },
    { test: /Android(?!.*Mobile)/i, name: 'Tablet' },
    { test: /iPhone|iPod|Android.*Mobile/i, name: 'Mobile' },
  ];

  // ============================================
  // 内部工具函数
  // ============================================

  /**
   * 在映射表中查找第一个匹配项
   * @param {Array}   map - 映射表
   * @param {string}  ua  - UA 字符串
   * @returns {Object|null}
   */
  function findMatch(map, ua) {
    for (var i = 0; i < map.length; i++) {
      if (map[i].test.test(ua)) {
        return map[i];
      }
    }
    return null;
  }

  /**
   * 用正则从 UA 中提取版本号
   * @param {string}  ua    - UA 字符串
   * @param {RegExp}  regex - 带一个捕获组的正则
   * @returns {string}
   */
  function extractVersion(ua, regex) {
    if (!regex) return '';
    var m = ua.match(regex);
    return m ? m[1] : '';
  }

  /**
   * 获取当前网络类型(仅客户端可用)
   * @returns {string} '4g' | '3g' | '2g' | 'slow-2g' | 'wifi' | ''
   */
  function getNetworkType() {
    if (typeof navigator === 'undefined') return '';
    var conn = navigator.connection ||
      navigator.mozConnection ||
      navigator.webkitConnection;
    if (!conn) return '';
    return conn.effectiveType || conn.type || '';
  }

  // ============================================
  // 主函数
  // ============================================

  /**
   * 获取当前浏览器 / App WebView 信息
   *
   * @param  {string} [customUA]  自定义 UA 字符串(服务端场景传入)
   * @return {Object} 浏览器信息对象
   *
   * @property {string}  name      - 浏览器 / WebView 名称
   * @property {string}  version   - 版本号
   * @property {string}  type      - 'app' | 'browser' | 'unknown'
   * @property {string|null} inApp - 宿主 App 名称(仅 type='app' 时有值)
   * @property {string}  platform  - 'Android' | 'iOS' | 'Windows' | 'macOS' | 'Linux' | 'Unknown'
   * @property {string}  device    - 'Mobile' | 'Tablet' | 'PC'
   * @property {string}  network   - '4g' | '3g' | '2g' | 'slow-2g' | ''
   * @property {boolean} isWebView - 是否为 App 内嵌 WebView
   * @property {string}  ua        - 原始 UA 字符串
   */
  function getBrowserInfo(customUA) {
    var ua = customUA || (typeof navigator !== 'undefined' ? navigator.userAgent : '');
    var matched = findMatch(REGISTRY, ua);
    var platform = findMatch(PLATFORM_MAP, ua);
    var device = findMatch(DEVICE_MAP, ua);

    return {
      name: matched ? matched.name : 'Unknown',
      version: matched ? extractVersion(ua, matched.version) : '',
      type: matched ? (matched.type || 'browser') : 'unknown',
      inApp: matched && matched.inApp ? matched.inApp : null,
      platform: platform ? platform.name : 'Unknown',
      device: device ? device.name : 'PC',
      network: customUA ? '' : getNetworkType(),
      isWebView: !!(matched && matched.type === 'app'),
      ua: ua,
    };
  }

  // ============================================
  // 便捷快捷方法
  // ============================================

  /** 是否为微信内置浏览器 */
  getBrowserInfo.isWeChat = function(ua) {
    return /MicroMessenger/i.test(ua || (typeof navigator !== 'undefined' ? navigator.userAgent : ''));
  };

  /** 是否为支付宝内置浏览器 */
  getBrowserInfo.isAlipay = function(ua) {
    return /AlipayClient/i.test(ua || (typeof navigator !== 'undefined' ? navigator.userAgent : ''));
  };

  /** 是否为 QQ 内置浏览器 */
  getBrowserInfo.isQQ = function(ua) {
    return /QQ\//i.test(ua || (typeof navigator !== 'undefined' ? navigator.userAgent : ''));
  };

  /** 是否为微博内置浏览器 */
  getBrowserInfo.isWeibo = function(ua) {
    return /Weibo/i.test(ua || (typeof navigator !== 'undefined' ? navigator.userAgent : ''));
  };

  /** 是否为钉钉内置浏览器 */
  getBrowserInfo.isDingTalk = function(ua) {
    return /DingTalk/i.test(ua || (typeof navigator !== 'undefined' ? navigator.userAgent : ''));
  };

  /** 是否为抖音内置浏览器 */
  getBrowserInfo.isDouyin = function(ua) {
    return /aweme/i.test(ua || (typeof navigator !== 'undefined' ? navigator.userAgent : ''));
  };

  /** 是否为移动端设备 */
  getBrowserInfo.isMobile = function(ua) {
    ua = ua || (typeof navigator !== 'undefined' ? navigator.userAgent : '');
    return /Android|iPhone|iPad|iPod/i.test(ua);
  };

  /** 是否为 Android 设备 */
  getBrowserInfo.isAndroid = function(ua) {
    return /Android/i.test(ua || (typeof navigator !== 'undefined' ? navigator.userAgent : ''));
  };

  /** 是否为 iOS 设备 */
  getBrowserInfo.isIOS = function(ua) {
    return /iPhone|iPad|iPod/i.test(ua || (typeof navigator !== 'undefined' ? navigator.userAgent : ''));
  };

  return getBrowserInfo;
});
相关推荐
程序喵大人1 小时前
【C++进阶】STL容器与迭代器 - 04 list 和 forward_list 用节点换稳定位置
开发语言·c++·list
taocarts_bidfans1 小时前
Taoify 站点访问地区限制与 IP 管控配置
前端·javascript·tcp/ip·taoify
qq_366086221 小时前
关于接口路径中中文值解码问题
java·开发语言·数据库
麻瓜老宋2 小时前
AI开发C语言应用按步走,表达式计算器calc的第二十步,魔法数修复、测试分隔符、位运算截断检查、进制溢出检查
c语言·开发语言·atomcode
今日无bug2 小时前
深入理解 JavaScript 变量提升(Hoisting)
javascript·面试
冻柠檬飞冰走茶2 小时前
PTA基础编程题目集 7-11 分段计算居民水费(C语言实现)
c语言·开发语言·算法
An_s2 小时前
Java Spring Boot+vue3文件管理系统
java·开发语言
大猫会长2 小时前
获取favicon.ico的方法
前端·javascript·html
默_笙2 小时前
🎀 大模型说"呀"还是"吗"?temperature 和 Top K 在偷偷控制它的"性格"
前端·javascript