要判断浏览器是否支持某项HTML5能力,单靠读取navigator.userAgent并匹配版本号的做法早已不可靠。同一浏览器内核的不同版本、移动端WebView的差异、第三方浏览器对UA字符串的伪装,都会让这种判断出现偏差。更稳妥的思路是直接探测运行环境中是否存在某个对象、方法或属性,也就是所谓的特性检测。特性检测的核心原则是:只关心能力是否存在,不关心浏览器品牌和版本。例如要确认Canvas 2D绘图是否可用,可以创建canvas元素并检查它是否提供getContext方法;要确认本地存储是否可用,可以检测window对象上是否存在localStorage属性。

接下来的内容会围绕原生JavaScript检测技巧、现有检测库以及容易踩到的坑展开。实际项目里既可以用手写的轻量判断函数,也可以引入Modernizr这类成熟方案,重点是根据业务场景选择合适的粒度。
一、为什么UA嗅探不再适合HTML5兼容性判断
过去的一些老项目里会看到类似这样的代码:通过navigator.userAgent判断浏览器是否为Chrome、Firefox或IE,然后分别加载不同的脚本。这种做法的最大问题在于UA字符串可以被随意修改,而且同一渲染引擎在不同设备上的表现也可能不同。不少第三方浏览器为了获取更好的网页兼容性,会主动伪装成主流浏览器的UA;某些WebView环境干脆不提供完整的UA信息,导致分支判断直接失效。
另一个问题是维护成本很高。每当新版本浏览器发布,UA匹配规则就需要重新调整。与其维护一个不断膨胀的UA数据库,不如直接检测代码真正需要使用的特性。例如只需要判断浏览器是否支持WebSocket,就检测window.WebSocket是否存在;需要判断是否支持Promise,就检测window.Promise。这样的检测逻辑短小、稳定,而且不依赖任何厂商字符串。
当然,UA嗅探并非毫无价值,在某些需要区分移动端和桌面端的场景中仍然可以使用。但针对HTML5各项API的兼容性,特性检测是更准确、更可维护的方式。下面进入具体的检测模式。
二、原生JavaScript特性检测的几种实现模式
最简单的检测方法是直接判断全局对象上是否存在某个属性。比如检测Geolocation API:
if ('geolocation' in navigator) {
// 支持Geolocation
} else {
// 不支持,走降级逻辑
}
这里使用in运算符而不是直接访问navigator.geolocation,原因是某些浏览器可能将该属性定义为undefined,直接判断navigator.geolocation在属性值为假时会出现误判,而in运算符能准确反映属性是否真实存在于对象上。
对于需要通过DOM元素才能检测的特性,可以创建对应元素并检查其方法或属性。以Canvas 2D为例:
function supportCanvas() {
var canvas = document.createElement('canvas');
return !!(canvas.getContext && canvas.getContext('2d'));
}
这里先创建canvas元素,然后确认getContext方法存在并且能返回2D上下文。使用双重否定!!将结果转换为布尔值,可以让调用方得到明确的true或false。类似模式还可以用于检测video元素能否播放特定编码格式:
function supportVideoType(type) {
var video = document.createElement('video');
return !!(video.canPlayType && video.canPlayType(type));
}
需要注意的是,video.canPlayType的返回值可能是probably、maybe或空字符串,而不是布尔值。上面示例将其转换为布尔值,可以满足大多数判断需求。若需要更细粒度的结果,可以保留返回值做进一步处理。
还有一类特性检测涉及CSS属性支持情况。可以使用CSS.supports方法,或者通过设置元素的style对象来探测。例如检测浏览器是否支持CSS Grid:
if (window.CSS && CSS.supports('display', 'grid')) {
// 支持Grid布局
} else {
// 降级到Flex或浮动
}
CSS.supports的兼容性已经很好,对于老式浏览器,可以退回到动态设置style属性的方式。核心思想是一样的:浏览器会静默忽略不认识的CSS属性或值,检测该属性是否被保留即可判断支持情况。
三、使用Modernizr进行批量检测与按需加载
当页面依赖大量HTML5特性时,手写几十个检测函数会显得繁琐。Modernizr是一个专门做特性检测的JavaScript库,它会在页面加载后自动运行一系列检测,并将结果以CSS类名形式添加到html元素上。例如支持Flexbox时html标签会带有flexbox类,不支持时则是no-flexbox。开发者可以直接在CSS中利用这些类名写降级样式:
.flexbox .container {
display: flex;
}
.no-flexbox .container {
display: table;
}
Modernizr的自定义构建功能可以选择只检测项目需要的特性,从而减小文件体积。使用时可以通过Modernizr对象读取检测结果,例如Modernizr.canvas、Modernizr.localstorage等。它还支持异步加载polyfill,当某项特性检测失败时,通过Modernizr.load可以按需引入对应的补丁脚本,避免所有用户都下载不必要的polyfill。
不过,Modernizr并非适用于所有项目。对于只依赖两三个特性的小型项目,引入一整个检测库反而增加请求成本。此时更推荐手写轻量检测函数,或者使用像core-js这样的polyfill直接补齐能力。选择工具时需要权衡检测范围和加载体积。
四、特性检测中的常见误区与最佳实践
一个常见的误区是把特性检测写成浏览器判断。例如“if (ie) { ... } else if (chrome) { ... }”这样的代码即便通过了测试,也会在未来新的浏览器版本面前变得脆弱。更合理的做法是让特性检测的结果直接驱动功能分支,而不是先判断浏览器再选择特性。例如需要读取文件时,应该检测window.FileReader是否存在,而不是先判断是不是IE10以上。
另一个容易忽略的点是:某些特性虽然存在,但可能以实验性前缀形式出现。以全屏API为例,标准方法是element.requestFullscreen,但部分浏览器旧版本使用webkitRequestFullscreen或mozRequestFullScreen。检测时需要遍历这些前缀方法:
function getFullscreenFn() {
var el = document.createElement('div');
if (el.requestFullscreen) return 'requestFullscreen';
if (el.webkitRequestFullscreen) return 'webkitRequestFullscreen';
if (el.mozRequestFullScreen) return 'mozRequestFullScreen';
return null;
}
这样做可以兼容带前缀的实现,同时保持代码的整洁。如果标准方法可用,就优先使用标准方法。需要注意的是,vendor前缀在未来的浏览器中可能被移除,因此标准方法应放在最前面检测。
特性检测并不是万能的。有些行为差异无法通过简单的属性存在与否来发现,例如不同浏览器对input类型为date的渲染行为差异很大,单靠检测是否能创建date类型的input并不能说明它的交互体验是否完善。这种情况下需要在检测基础上增加实际的功能测试或使用polyfill来统一行为。总之,把特性检测作为第一道防线,同时配合渐进增强策略,可以让HTML5页面在更多环境下稳定运行。