特性检测:用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。
addTest API,这样你还可以自定义检测项,跟 Modernizr 的机制统一管理。
原生检测 vs Modernizr:怎么选?
我不迷信任何工具。下面是我自己的选择标准:
- 只检测一两个API → 原生 typeof / in / try-catch,零依赖
- 需要检测多个特性,且要在CSS里做样式适配 → Modernizr 的 CSS 类很省事
- 项目对包体积极度敏感(如移动端首屏) → 手写检测 + 自己维护 CSS 类
- 需要检测 CSS 媒体查询、@supports 等 → Modernizr 或者
window.CSS.supports()
组合策略:Feature Detection + 渐进增强 + 降级
检测只是第一步。完整的做法是:
- 检测:用原生或 Modernizr 确认 API 存在
- 渐进增强:支持则用最新API提供更好的体验
- 降级:不支持则用回退方案,保证基本功能
举个例子,图片懒加载:
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,不是因为浏览器不支持,而是因为协议限制。我一般加个环境提示。
我把整个决策流程画出来了
下面这张图总结了特性检测的标准流程,你可以当作项目管理时的检查清单:
总结一句话
特性检测不是什么高深技术,它就是 “你敢不敢先试试再决定”。不管是手写 typeof,还是用 Modernizr 全家桶,目的都一样:让我们的代码在五花八门的浏览器里,都能给出最合理的表现。别偷懒去嗅探 UA,你永远不知道下一个用户用的是哪款魔改浏览器。