浏览器兼容性问题实战:从踩坑到优雅解决的前端经验
项目初期的技术选型
上个月接了个老系统改造的活,客户要求支持到 IE11 —— 没错,就是那个已经停止支持但某些国企还在用的 IE11。我一开始是拒绝的,但架不住需求方反复强调“必须兼容”,只好硬着头皮上。项目本身是个数据看板,用 Vue 3 + Vite 搭的,原本跑在现代浏览器里丝滑得很,结果一测 IE11,直接白屏。
问题出在哪儿?Vite 默认不处理 IE 兼容,打包出来的代码全是 ES6+ 语法,IE11 当然不认识。我一开始想直接切回 Webpack,但团队已经习惯了 Vite 的开发体验,而且项目中期了,换构建工具成本太高。最后决定:保留 Vite,但加 polyfill 和转译。
最大的坑:polyfill 到底怎么打才不炸
我第一反应是装 @vitejs/plugin-legacy,官方插件,应该稳。配完之后,IE11 能打开了,但页面卡得像 PPT。打开 DevTools(是的,IE11 的 F12 工具虽然丑但能用),发现 bundle 文件大了将近 3 倍,光 core-js 就占了 800KB。更糟的是,某些第三方库(比如用 Proxy 实现响应式的)根本没法 polyfill,直接报错。
折腾了半天,发现问题出在“全量引入”。我一开始图省事,直接 import ‘core-js/stable’,结果把所有 polyfill 都塞进去了。后来改成按需引入,只补项目实际用到的 API:
// main.js
import 'core-js/es/promise'
import 'core-js/es/array/includes'
import 'core-js/es/object/assign'
import 'core-js/es/symbol'
但这样维护起来很痛苦,每次用个新 API 都得手动加一行。后来想到用 Babel 的 preset-env 配合 usage 模式,自动分析代码用到的特性。但 Vite 默认不用 Babel……于是我又加了 @vitejs/plugin-legacy 的底层配置,强制它走 Babel 转译。
// vite.config.js
import legacy from '@vitejs/plugin-legacy'
export default {
plugins: [
legacy({
targets: ['ie >= 11'],
additionalLegacyPolyfills: ['regenerator-runtime/runtime']
})
]
}
注意这里要额外加 regenerator-runtime/runtime,不然 async/await 在 IE11 里会报错。这个细节我踩了两次坑,第一次忘了加,第二次加错位置,放到了 legacy 插件外面,结果没生效。
CSS 兼容:你以为只是前缀问题?
JS 兼容搞定了,CSS 又来一记重拳。项目里用了大量 Flexbox 和 Grid,IE11 对 Flex 的支持是“半残”状态——比如 flex: 1 在 IE11 里可能失效,得拆成 flex: 1 1 0%。更头疼的是 Grid,IE11 根本不支持,只能降级成 float 或 inline-block。
我试过用 Autoprefixer 自动加前缀,但发现它对 IE11 的 Flex 补丁支持有限。最后只能手动改关键布局:
/* 原来的写法 */
.container {
display: flex;
flex: 1;
}
/* IE11 兼容写法 */
.container {
display: -ms-flexbox;
display: flex;
-ms-flex: 1 1 0%;
flex: 1 1 0%;
}
还有个隐藏雷区:CSS 自定义属性(var(–xxx))。IE11 完全不支持,而我们的主题系统重度依赖它。临时方案是用 PostCSS 插件 postcss-custom-properties 把变量编译成静态值,但这样就失去了运行时换肤的能力。权衡之后,我们砍掉了 IE11 下的动态主题功能,只保留默认主题——毕竟客户说“能看就行”。
事件和 DOM API 的暗坑
你以为兼容到这儿就完了?太天真。项目里有个拖拽排序功能,用的是 addEventListener('touchstart'),在 iOS Safari 上没问题,但在 IE11 里 touch 事件压根不触发(因为 IE11 只支持 mouse 事件)。我一开始想用 Modernizr 检测,但又不想引入额外库,最后写了个简单的判断:
function isTouchDevice() {
return 'ontouchstart' in window || navigator.maxTouchPoints > 0
}
const startEvent = isTouchDevice() ? 'touchstart' : 'mousedown'
element.addEventListener(startEvent, handleDragStart)
类似的还有 Element.closest() 方法,IE11 不支持。我直接 copy 了 MDN 上的 polyfill:
if (!Element.prototype.closest) {
Element.prototype.closest = function(s) {
let el = this
do {
if (el.matches(s)) return el
el = el.parentElement || el.parentNode
} while (el !== null && el.nodeType === 1)
return null
}
}
这种小补丁零零碎碎加了十来个,每次测试都得在 IE11 里点一遍,累得半死。
最终的解决方案
综合下来,我们的兼容方案是:
- 构建层面:Vite + @vitejs/plugin-legacy,targets 设为 ie >= 11
- JS 补丁:按需引入 core-js + 手动添加缺失的 DOM API polyfill
- CSS 补丁:Autoprefixer + 手动修复 Flex/Grid 问题 + 放弃 CSS 变量
- 交互降级:touch/mouse 事件双写,复杂动画在 IE11 里直接禁用
最后打包体积增加了约 40%,首屏加载从 1.2s 涨到 2.5s(在 IE11 虚拟机里测的),但客户能接受。值得一提的是,我们没做完整的自动化测试,只在 BrowserStack 上跑了几个关键路径,所以可能还有边缘 case 没覆盖到——但上线两周了,没收到 IE11 相关的 bug 反馈,估计问题不大。
回顾与反思
这次兼容 IE11 的经历让我深刻体会到:**兼容不是技术问题,是成本问题**。如果时间充裕,我可能会建议客户放弃 IE11,或者至少只做基础功能兼容。但现实是,需求方往往不理解技术债的代价。
做得好的地方:及时用 plugin-legacy 避免了重构构建流程;对核心功能做了充分测试。
可以优化的地方:CSS 变量那块其实可以用 JS 动态注入 style 标签来实现运行时换肤,但当时赶工期没搞;另外,有些 polyfill 其实可以通过条件加载(比如只在 IE11 里引入)来减少现代浏览器的包体积,这个后续可以试试。
最后提醒一句:如果你的新项目还要支持 IE11,一定要在立项阶段就明确告知各方兼容成本,别等到开发中期才甩锅给前端。我们这次差点延期,就是因为前期没评估清楚。
以上是我踩坑后的总结,希望对你有帮助。有更优的实现方式欢迎评论区交流,比如你们是怎么处理 IE11 下的 CSS Grid 的?我至今觉得那块写得特别丑。

暂无评论