第4章 前端兼容性解决方案:Polyfill、Normalize.css、Autoprefixer
说到前端兼容性,你肯定遇到过这种场景:在 Chrome 上跑得好好的页面,换到旧版 Safari 就崩了,或者 iOS 上的样式死活跟 Android 对不齐。这背后的问题其实就三类——CSS 样式差异、浏览器前缀缺失、JavaScript API 不支持。今天我就把三个最趁手的工具讲透:Polyfill、Normalize.css、Autoprefixer。
我最早做移动端 H5 时,以为写一套代码就能跑通所有平台。后来被用户投诉按钮在不同手机上长得不一样,才老老实实补上了这些方案。说白了,它们就是帮你把浏览器之间的“方言”翻译成通用语。
4.1 Normalize.css:让样式回归统一
你想,每个浏览器都有自己那套默认样式。比如 <h1> 在 Chrome 上是 2em,在 IE 里可能是 1.8em。手动重置每个标签?不现实。Normalize.css 就是干这个的——它不会粗暴地把所有 margin/padding 清零,而是保留语义的同时,让各浏览器的默认行为对齐。
我的经验:以前用过 CSS Reset,但发现 button 和 input 的 line-height 被 reset 后,在移动端反而更难调。换成 Normalize.css 后,保留了些浏览器本身的合理默认值,像 strong 的加粗、abbr 的下划线都还在,省了不少事。
典型用法:
npm install normalize.css
// 在入口文件引入
import 'normalize.css';
或者直接 CDN:
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/normalize.css@8.0.1/normalize.css">
它还修复了哪些具体问题?看这张表:
| 标签/属性 | 浏览器差异 | Normalize.css 的处理 |
|---|---|---|
html | 部分浏览器设置 line-height 为 1.15 | 统一为 1.15 |
body | IE 中 margin: 8px,其他浏览器 0 | 统一 margin: 0 |
button | Safari 里 font-family 继承差异 | 设置为 inherit |
audio:not([controls]) | 旧浏览器不隐藏默认控件 | 隐藏控件并设置高度 |
template | IE 不支持 display: none | display: none |
4.2 Autoprefixer:CSS 前缀自动化
写 display: flex 的时候,你是不是也纠结过要不要加 -webkit-?Autoprefixer 就是解决这个痛点的。它会根据 Can I Use 数据,自动给 CSS 属性加上必要的浏览器前缀。
举个例子,你只写:
.box {
display: flex;
user-select: none;
}
经过 Autoprefixer 处理后,变成:
.box {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
但要注意,它依赖 browserslist 配置来决定“目标浏览器”。我在 package.json 里通常这么写:
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
这样只会给市场占有率超过 1% 且还在维护的浏览器加前缀,不会无脑全加。你想想,如果给 IE6 也加 -webkit-,那就是垃圾代码了。
browserslist 写成了 "last 1 version",结果 iOS 12 的 Safari 不识别 position: sticky,因为没有加 -webkit- 前缀。后来改成 "last 2 versions" 才解决。建议至少覆盖两个大版本。
使用方式:配合构建工具。Webpack 里用 postcss-loader,Vite 内置支持,Gulp 也有插件。
// postcss.config.js
module.exports = {
plugins: [
require('autoprefixer')
]
}
4.3 Polyfill:填补缺失的 JS API
这是三个工具里最“强”的一个。Polyfill 会检测浏览器是否支持某个 API,如果不支持,就用 JavaScript 模拟一份。比如 Promise 在 IE 里没有,Polyfill 帮你实现一个。比如 fetch、Array.from、Object.assign,都能补上。
我记得做某款金融 H5 时,Android 4.4 的 WebView 居然不支持 Intl.DateTimeFormat,格式化日期全挂了。我插入了 intl-polyfill,问题瞬间解决。不过要注意,Polyfill 会增加包体积,尤其是一些大型 polyfill(比如 core-js)。
推荐按需加载:
import 'core-js/stable/array/from';
import 'core-js/stable/promise';
// 只加载你实际用到的 polyfill
或者用 Polyfill.io 服务,自动识别浏览器:
<script src="https://polyfill.io/v3/polyfill.min.js?features=Promise,Array.from,fetch"></script>
这里要小心:Polyfill.io 是第三方服务,如果它挂了,你的功能也会受影响。我一般只在开发或原型阶段用它,生产环境改用本地打包的 polyfill。
| 工具 | 作用层面 | 典型场景 | 是否增加 JS 体积 |
|---|---|---|---|
| Normalize.css | CSS 样式重置 | 按钮、标题、列表在不同浏览器中表现不一致 | 否(CSS) |
| Autoprefixer | CSS 前缀处理 | flex、grid、transform 等需要加浏览器前缀的属性 | 否(CSS) |
| Polyfill | JavaScript API 填补 | Promise、fetch、IntersectionObserver 等新 API 在旧浏览器缺失 | 是(JS) |
三者协同工作流程
光知道每个工具怎么用还不够,你得知道它们怎么配合。下面这张图是我自己整理的兼容性处理流水线,实际项目中就是按这个顺序走的。
避坑指南与最佳实践
关键词总结:
- Normalize.css —— 先用它做样式基底,再写项目样式。
- Autoprefixer —— 配置好 browserslist,别给死掉的浏览器加前缀。
- Polyfill —— 只补你实际用的 API,监控包体积。
- 顺序:样式层先 normalize 再前缀处理,JS 层按需 polyfill,互不冲突。
我曾经犯过一个错:在团队项目里同时用了 Normalize.css 和第三方 UI 库(比如 Antd Mobile),结果 UI 库自己的 reset 把 Normalize.css 覆盖了,导致部分样式错乱。后来我规定:Normalize.css 必须放在所有 CSS 之前,业务样式在后,UI 库的样式也要在 Normalize 之后。顺带一提,如果你用 Tailwind CSS,它的 Preflight 其实已经包含了类似 Normalize 的功能,可以二选一,别重复。
至于 Polyfill 的时机,我建议在入口文件的头部就执行检测。比如用 @babel/preset-env 配合 useBuiltIns: 'usage',它只会在用到某个 API 的文件里注入对应的 polyfill,比全局引入省不少体积。
嗯,这三个工具说白了一个帮你管样式基准,一个管 CSS 兼容写法,一个管 JS 能力填补。掌握好它们,多平台兼容的基本盘就稳了。剩下的就是按浏览器覆盖率去调整配置,别贪多也别偷懒。