特性检测:用Modernizr与原生方法,精准探测API支持

说实话,我刚入行那会儿,特别喜欢写这种代码:

if (navigator.userAgent.indexOf('Chrome') > -1) {
  // 用Chrome专属API
}

结果呢?换了个浏览器就崩。用户反馈一来,我脸都绿了。后来我才明白——浏览器嗅探就是给自己挖坑。你想想看,用户代理字符串能伪造,浏览器版本能升级,硬件环境千差万别。我们真正该问的只有一件事:这个功能,当前浏览器到底支不支持?

答案就是 特性检测(Feature Detection)。它不问你是谁,只问你能不能。

为什么特性检测比浏览器嗅探靠谱?

我给你列个对比,看完就懂了:

维度 浏览器嗅探(User-Agent 解析) 特性检测(Feature Detection)
可靠性 低。字符串可伪造,版本号随时变 高。直接检查能力是否存在
维护成本 高。每个新版本都要更新正则 低。写一次检测逻辑,适配所有浏览器
前瞻性 差。未来浏览器可能直接跳过 好。新浏览器只要实现API就能自动适配
典型误判 iOS中的Chrome也挂WebKit头,易误判为Safari 不依赖特征,不会误判

我有个项目,用UA判断IE11,结果Edge冒充IE被我们当成IE处理,功能全废。经历过那次后,我再也不碰UA嗅探了。

原生检测:手写三招,够用

你不需要一开始就上Modernizr。大多数场景下,原生方法就够了。我最常用的三种:

1. typeof 检查全局对象

if (typeof window.localStorage !== 'undefined') {
  // 使用 localStorage
} else {
  // 降级到 cookie 或内存存储
}

注意:typeof 对未声明的变量不会报错,返回 "undefined",这点很安全。但有个坑——typeof null === "object",所以别拿它去检查可能为空的值。

2. in 操作符检查属性

if ('geolocation' in navigator) {
  navigator.geolocation.getCurrentPosition(success, error);
} else {
  // 显示手动输入位置的地图
}

in 操作符会判真属性继承,所以 'toString' in document 也是 true。我习惯用它检查 navigator、window 这类宿主对象的属性。

3. try-catch 兜底:那些会抛异常的情况

有些API存在但调用时才会报错(比如被浏览器策略拦截)。这时候必须用 try-catch:

function supportsFullscreen() {
  const el = document.documentElement;
  const fn = el.requestFullscreen ||
             el.webkitRequestFullscreen ||
             el.msRequestFullscreen;
  if (!fn) return false;
  try {
    // 有的浏览器在非用户交互时调用会抛异常
    fn.call(el);
    document.exitFullscreen();
    return true;
  } catch (e) {
    return false;
  }
}

我曾经在接百度地图API时,发现部分国产浏览器有 requestFullscreen 属性,但调用就抛安全错误。如果没有 try-catch,后面的全屏逻辑就炸了。

Modernizr:让检测更优雅

手写检测多了,你会发现很多重复模式:检测 canvas、webgl、flexbox……一个个写太累。这时候我推荐 Modernizr。

Modernizr 是一个轻量级特性检测库(可定制,最小版本不到5KB)。它会在 <html> 标签上添加 CSS 类,并暴露 Modernizr 全局对象。

基本用法

<html class="no-js"></html>
<script src="modernizr-custom.js"></script>

运行后 <html> 变成:

<html class="js flexbox flexboxlegacy canvas canvastext webgl ...">

在JS中:

if (Modernizr.canvas) {
  // 绘制 Canvas
} else {
  // 用 SVG 或 Flash 替代
}

if (Modernizr.geolocation) {
  // 原生定位
} else {
  // 用 IP 定位库
}

定制你的 Modernizr

我建议你不要引入全量包。去 Modernizr 下载页,只勾选你需要的检测项。比如你只检测 flexbox、webp 和 serviceworker,生成的文件可能只有2-3KB。

小技巧: 如果项目中用到了 CSS 特性检测,推荐同时开启 addTest API,这样你还可以自定义检测项,跟 Modernizr 的机制统一管理。

原生检测 vs Modernizr:怎么选?

我不迷信任何工具。下面是我自己的选择标准:

  • 只检测一两个API → 原生 typeof / in / try-catch,零依赖
  • 需要检测多个特性,且要在CSS里做样式适配 → Modernizr 的 CSS 类很省事
  • 项目对包体积极度敏感(如移动端首屏) → 手写检测 + 自己维护 CSS 类
  • 需要检测 CSS 媒体查询、@supports 等 → Modernizr 或者 window.CSS.supports()

组合策略:Feature Detection + 渐进增强 + 降级

检测只是第一步。完整的做法是:

  1. 检测:用原生或 Modernizr 确认 API 存在
  2. 渐进增强:支持则用最新API提供更好的体验
  3. 降级:不支持则用回退方案,保证基本功能

举个例子,图片懒加载:

if ('loading' in HTMLImageElement.prototype) {
  // 原生懒加载:只需加 loading="lazy"
  img.loading = 'lazy';
} else {
  // 用 IntersectionObserver 实现懒加载
  const observer = new IntersectionObserver(entries => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        const lazyImg = entry.target;
        lazyImg.src = lazyImg.dataset.src;
        observer.unobserve(lazyImg);
      }
    });
  });
  observer.observe(img);
}

嗯,这里要注意:即使 loading 属性存在,某些旧版 Safari 仍不支持。我建议再加个 typeof HTMLImageElement.prototype.loading !== 'undefined' 双重确认。

避坑指南

我曾经踩过的坑,你绕开走:
  • 不要只用 typeof 检查构造器。 比如 typeof window.fetch === 'function' 在部分浏览器中为 true,但实际 fetch 可能被安全策略禁掉。最好再试调一次。
  • 检测 DOM 特性时注意宿主对象。 typeof document.createElement('video').canPlayType === 'function' 在旧 IE 可能返回 'unknown',但 IE 根本不支持 <video>,所以你还得先检测元素是否有效。
  • 用 Modernizr 时,别忘了加载 Modernizr.load(已经废弃)或自己处理 polyfill。 现在更推荐用 @babel/polyfill 或 dynamic import 来按需加载 polyfill。
  • 检测 serviceWorker 时,注意 https 或 localhost 环境。 有些代码在 http 环境下检测返回 false,不是因为浏览器不支持,而是因为协议限制。我一般加个环境提示。

我把整个决策流程画出来了

下面这张图总结了特性检测的标准流程,你可以当作项目管理时的检查清单:

特性检测与降级决策流程图 开始:需要某个API API 是否存在? 是 直接使用原生 API 渐进增强完成 否 选择降级方案 降级方案执行 测试验证 + 回归 核心原则:只问能力,不问出身。检测失败不必慌张,备选方案随时候补。

总结一句话

特性检测不是什么高深技术,它就是 “你敢不敢先试试再决定”。不管是手写 typeof,还是用 Modernizr 全家桶,目的都一样:让我们的代码在五花八门的浏览器里,都能给出最合理的表现。别偷懒去嗅探 UA,你永远不知道下一个用户用的是哪款魔改浏览器。