第二章:浏览器内核与版本差异——Trident、Gecko、WebKit、Blink的API实现差异
聊到浏览器兼容性,绕不开的就是内核。说白了,内核就是浏览器的“发动机”,不同的发动机决定了你能跑多快、能跑哪些路。
我记得刚入行那会儿,最怕听到“IE6”。那会儿做网站,得先搞定IE6,再去管别的浏览器。后来Chrome出来了,Firefox也争气,再后来Edge也换了芯。这十几年,我真是看着这四个内核“打架”长大的。
内核的前世今生
现在市面上常见的内核,其实就四个:
- Trident:IE的亲爹,后面Edge继承了一段时间,但后来Edge也“叛变”了。
- Gecko:Firefox的专属发动机,一直很稳。
- WebKit:Safari和早期Chrome用的,苹果的看家本领。
- Blink:Chrome从WebKit分出来的分支,现在大部分国产浏览器也是它的孩子。
为什么会有差异?归根结底是标准实现的时间差和解释的灵活性。
W3C定了个标准,好比定了个菜谱。大厨们(浏览器厂商)看着菜谱做菜,但每个人手劲不一样、火候掌握不一样,做出来的菜味道自然有微妙差别。
Trident的“遗老”问题
Trident内核,我现在想到它头都疼。为什么?因为它跟标准“离太远了”。
我曾经接手过一个政府项目,老系统必须跑在IE11上。你猜怎么着?一个简单的CSS圆角,在其他浏览器上一行代码搞定,在IE里得写一堆滤镜。
1. CSS滤镜与渐变
IE不支持标准的filter和gradient,它有自己的“亲儿子”:
/* 现代浏览器 */
background: linear-gradient(to bottom, #000, #fff);
/* Trident专属 */
filter: progid:DXImageTransform.Microsoft.Gradient(
startColorStr='#000000', endColorStr='#ffffff', GradientType=0
);
我建议,除非业务强求,否则果断放弃IE10以下。你想想看,为了0.01%的用户,维护两套代码,不划算。
2. 事件模型的“特立独行”
Trident的attachEvent vs 标准的addEventListener。这个坑我踩过太多次了。
// 教科书式的兼容写法
function addEvent(el, event, handler) {
if (el.addEventListener) {
el.addEventListener(event, handler, false);
} else if (el.attachEvent) {
el.attachEvent('on' + event, handler);
} else {
el['on' + event] = handler;
}
}
注意,attachEvent里的this指向window,不是触发元素。嗯,这里要注意,一不小心就翻车。
Gecko的“固执己见”
Gecko,Firefox的御用内核。它很守规矩,但有时候规矩得让人抓狂。
1. innerText vs textContent
这个坑很有意思。Gecko只认textContent,而innerText是IE发明的,后来也被其他浏览器接受了。但Gecko一直“傲娇”到很晚才支持。
// 兼容写法
const text = el.textContent || el.innerText || '';
2. CSS前缀的“拖沓”
我记得Chrome和Safari早早地用上了-webkit-前缀,而Gecko用-moz-。最头疼的是,一些实验性API,Gecko的实现跟标准文档总有偏差。
比如,scroll-behavior: smooth;,Gecko在v36才支持,而Chrome v61才开始支持。中间那段日子,用户只能“硬跳”。
WebKit的“封闭花园”
WebKit,Safari的专属内核。它有个特点:跟标准接轨,但总喜欢加点“私货”。
1. -webkit-前缀的“泛滥”
早期的Chrome和Safari共用WebKit,但Chrome后来分家成了Blink。
WebKit的私有前缀不仅多,而且有的属性后来成了标准。比如-webkit-sticky,现在已经是标准的position: sticky了。
div {
position: -webkit-sticky; /* 老Safari */
position: sticky; /* 标准 */
top: 0;
}
但要注意,-webkit-sticky在旧版Safari上不支持overflow为hidden的父元素。这个坑,我在做H5活动页的时候栽过。
2. 性能差异:requestAnimationFrame
WebKit对这个API的实现有个特点:页面不可见时,它不调用了。
这其实很合理,省电嘛。但如果你用它做定时器,严格依赖帧率,就得小心。我建议用setTimeout兜底,或者检测document.hidden状态。
function animLoop(callback) {
let timer;
function loop() {
callback();
timer = requestAnimationFrame(loop);
}
timer = requestAnimationFrame(loop);
}
Blink的“激进与融合”
Blink从WebKit分出来之后,直接脱离了苹果的“管制”。它更激进,也更贴近标准。
1. 剪贴板API
Blink率先支持了navigator.clipboard,而其他内核还在用document.execCommand。
async function copyToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
} catch {
// 回退方案
const textarea = document.createElement('textarea');
textarea.value = text;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
}
}
你猜怎么着?这个API在HTTPS环境下才能用。本地开发用localhost没问题,上生产必须配SSL。
2. 容器查询的支持
Blink在v105率先支持了@container,而Safari和Firefox到现在(2024年)才陆续跟上。
如果你的项目主要面向桌面Chrome用户,大胆用。但如果是移动端,我建议再等等,毕竟Safari在iOS上的话语权很重。
@container,结果iOS用户一打开,布局全乱了。从那以后,我学会了先查caniuse,再落代码。
差异溯源:表格说话
我整理了一份常见API差异的对照表,你们可以直接拿去当开发参考:
| API / 特性 | Trident (IE11) | Gecko (Firefox 120+) | WebKit (Safari 17+) | Blink (Chrome 120+) |
|---|---|---|---|---|
CSS padding和margin百分比基准 |
参照父容器宽度 | 参照父容器宽度 | 参照父容器宽度 | 参照父容器宽度 |
clearfix方案 |
依赖zoom:1触发hasLayout |
使用::after伪元素 |
使用::after伪元素 |
使用::after伪元素 |
clipboard API |
仅execCommand |
支持Async Clipboard | 支持Async Clipboard | 优先支持Async Clipboard |
CSS Grid布局 |
不支持 | 支持(v52+) | 支持(v10.1+) | 支持(v57+) |
position: sticky |
不支持 | 支持(v32+) | 支持(v6.1+) | 支持(v23+) |
ResizeObserver |
不支持 | 支持(v69+) | 支持(v13.1+) | 支持(v64+) |
SVG favicon |
不支持 | 支持 | 支持 | 支持 |
CSS env() 安全区域 |
不支持 | 支持(v101+) | 支持(v11+) | 支持(v69+) |
你看,光一个position: sticky,Trident就彻底扑街了。所以,如果你的用户里还有IE11,这个属性你就得降级成position: relative加JS监听滚动。
适配策略:我的三件套
面对这些差异,我一般用三招:
- 特性检测(Feature Detection):用
if (typeof xxx !== 'undefined')或者@supports规则。 - 前缀处理:借助Autoprefixer这个工具自动生成,别手写
-webkit-,你会疯的。 - Polyfill:对缺失的API(比如
IntersectionObserver),用第三方库给它补上。
我个人习惯:能检测就检测,不能检测就降级,降级不了就报错。别在IE上浪费太多工时,你的项目又不是给博物馆做的。
对了,还有一点:移动端优先考虑WebKit/Blink。因为iOS上只能用Safari(WebKit),Android上大部分是Chrome(Blink)。Gecko在移动端的份额,基本可以忽略。