第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
bodyIE 中 margin: 8px,其他浏览器 0统一 margin: 0
buttonSafari 里 font-family 继承差异设置为 inherit
audio:not([controls])旧浏览器不隐藏默认控件隐藏控件并设置高度
templateIE 不支持 display: nonedisplay: none
提示:Normalize.css 一般放在你自定义 CSS 之前。我习惯先引入它,再写业务样式,这样所有浏览器都站在同一起跑线上。

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.cssCSS 样式重置按钮、标题、列表在不同浏览器中表现不一致否(CSS)
AutoprefixerCSS 前缀处理flex、grid、transform 等需要加浏览器前缀的属性否(CSS)
PolyfillJavaScript API 填补Promise、fetch、IntersectionObserver 等新 API 在旧浏览器缺失是(JS)

三者协同工作流程

光知道每个工具怎么用还不够,你得知道它们怎么配合。下面这张图是我自己整理的兼容性处理流水线,实际项目中就是按这个顺序走的。

前端兼容性解决方案协同流水线 原始 H5 代码 Normalize.css 统一样式基准 Autoprefixer 添加供应商前缀 Polyfill 填补缺失 API 多平台兼容 H5 页面 iOS · Android · 微信 · 旧版浏览器 ⚠ 注意事项 Normalize.css 在构建前引入;Autoprefixer 在 CSS 编译阶段处理; Polyfill 建议按需加载并使用本地打包,避免依赖第三方服务。

避坑指南与最佳实践

关键词总结:

  • 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,比全局引入省不少体积。

小技巧:如果你只关心现代浏览器(Chrome/Firefox/Safari 最近两个大版本),其实可以完全不用 Polyfill,但 Normalize.css 和 Autoprefixer 我建议还是保留——它们不大会增加维护成本,但能防住很多诡异的样式 bug。

嗯,这三个工具说白了一个帮你管样式基准,一个管 CSS 兼容写法,一个管 JS 能力填补。掌握好它们,多平台兼容的基本盘就稳了。剩下的就是按浏览器覆盖率去调整配置,别贪多也别偷懒。