跳到主要内容

浏览器兼容性

问题

如何处理浏览器兼容性问题?常用的兼容方案有哪些?

答案

浏览器兼容性是前端开发的重要课题,涉及 JavaScript、CSS、HTML 和 API 的跨浏览器支持。主要解决方案包括:特性检测Polyfill转译渐进增强

兼容性检测

特性检测

// 检测浏览器是否支持某个特性
function featureDetect(): Record<string, boolean> {
return {
// ES6+ 特性
promises: typeof Promise !== 'undefined',
fetch: typeof fetch !== 'undefined',
proxy: typeof Proxy !== 'undefined',
symbol: typeof Symbol !== 'undefined',

// Web API
serviceWorker: 'serviceWorker' in navigator,
webGL: (() => {
try {
return !!document.createElement('canvas').getContext('webgl');
} catch {
return false;
}
})(),
intersectionObserver: typeof IntersectionObserver !== 'undefined',
resizeObserver: typeof ResizeObserver !== 'undefined',

// CSS 特性
grid: CSS.supports('display', 'grid'),
flexGap: CSS.supports('gap', '1px'),
containerQueries: CSS.supports('container-type', 'inline-size'),

// 输入特性
touchEvents: 'ontouchstart' in window,
pointerEvents: 'PointerEvent' in window,
};
}

// 条件使用
if ('IntersectionObserver' in window) {
// 使用 IntersectionObserver
} else {
// 降级方案
}

CSS 特性检测

// CSS.supports() API
if (CSS.supports('display', 'grid')) {
console.log('支持 Grid 布局');
}

if (CSS.supports('backdrop-filter', 'blur(10px)')) {
console.log('支持背景模糊');
}

// @supports CSS 规则
/*
@supports (display: grid) {
.container {
display: grid;
}
}

@supports not (display: grid) {
.container {
display: flex;
}
}
*/

User-Agent 检测(不推荐)

// ⚠️ 不推荐:UA 字符串可被伪造
function detectBrowser(): { name: string; version: string } {
const ua = navigator.userAgent;

if (ua.includes('Chrome')) {
const match = ua.match(/Chrome\/(\d+)/);
return { name: 'Chrome', version: match?.[1] || '' };
}
if (ua.includes('Firefox')) {
const match = ua.match(/Firefox\/(\d+)/);
return { name: 'Firefox', version: match?.[1] || '' };
}
if (ua.includes('Safari') && !ua.includes('Chrome')) {
const match = ua.match(/Version\/(\d+)/);
return { name: 'Safari', version: match?.[1] || '' };
}

return { name: 'Unknown', version: '' };
}

// ✅ 推荐:使用 User-Agent Client Hints
if ('userAgentData' in navigator) {
const uaData = (navigator as any).userAgentData;
console.log('浏览器:', uaData.brands);
console.log('移动端:', uaData.mobile);
}

Polyfill

Polyfill 为旧浏览器提供新 API 的兼容实现。

常用 Polyfill

// Array.prototype.includes
if (!Array.prototype.includes) {
Array.prototype.includes = function<T>(
searchElement: T,
fromIndex?: number
): boolean {
const o = Object(this);
const len = o.length >>> 0;
if (len === 0) return false;

const n = fromIndex || 0;
let k = Math.max(n >= 0 ? n : len + n, 0);

while (k < len) {
if (o[k] === searchElement ||
(Number.isNaN(o[k]) && Number.isNaN(searchElement))) {
return true;
}
k++;
}
return false;
};
}

// Object.assign
if (typeof Object.assign !== 'function') {
Object.assign = function(target: any, ...sources: any[]): any {
if (target == null) {
throw new TypeError('Cannot convert undefined or null to object');
}

const to = Object(target);

for (const source of sources) {
if (source != null) {
for (const key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
to[key] = source[key];
}
}
}
}

return to;
};
}

fetch Polyfill

// 简化版 fetch polyfill
if (!window.fetch) {
window.fetch = function(
url: string,
options: RequestInit = {}
): Promise<Response> {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();

xhr.open(options.method || 'GET', url);

// 设置请求头
if (options.headers) {
Object.entries(options.headers).forEach(([key, value]) => {
xhr.setRequestHeader(key, value as string);
});
}

xhr.onload = function() {
const response = {
ok: xhr.status >= 200 && xhr.status < 300,
status: xhr.status,
statusText: xhr.statusText,
json: () => Promise.resolve(JSON.parse(xhr.responseText)),
text: () => Promise.resolve(xhr.responseText),
};
resolve(response as unknown as Response);
};

xhr.onerror = function() {
reject(new TypeError('Network request failed'));
};

xhr.send(options.body as any);
});
};
}

按需加载 Polyfill

// 动态加载 Polyfill
async function loadPolyfills(): Promise<void> {
const polyfills: Promise<any>[] = [];

if (!('IntersectionObserver' in window)) {
polyfills.push(import('intersection-observer'));
}

if (!('fetch' in window)) {
polyfills.push(import('whatwg-fetch'));
}

if (!('Promise' in window)) {
polyfills.push(import('es6-promise/auto'));
}

await Promise.all(polyfills);
}

// 应用入口
loadPolyfills().then(() => {
import('./app');
});

Polyfill.io 服务

<!-- 根据浏览器 UA 返回需要的 polyfill -->
<script src="https://polyfill.io/v3/polyfill.min.js?features=es2015,es2016,es2017,fetch"></script>

<!-- 指定特性 -->
<script src="https://polyfill.io/v3/polyfill.min.js?features=IntersectionObserver,ResizeObserver"></script>

Babel 转译

Babel 将现代 JavaScript 转换为兼容旧浏览器的代码。

配置示例

// babel.config.js
module.exports = {
presets: [
[
'@babel/preset-env',
{
// 目标浏览器
targets: {
browsers: ['> 1%', 'last 2 versions', 'not dead'],
// 或使用 browserslist 配置
},

// Polyfill 策略
useBuiltIns: 'usage', // 按需引入
corejs: 3,

// 模块格式
modules: false, // 保留 ES modules
},
],
'@babel/preset-typescript',
],
plugins: [
'@babel/plugin-transform-runtime',
],
};

browserslist 配置

// package.json 或 .browserslistrc
{
"browserslist": [
"> 1%",
"last 2 versions",
"not dead",
"not IE 11"
]
}

// 不同环境使用不同配置
{
"browserslist": {
"production": [
"> 0.5%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version"
]
}
}

转译示例

// 源代码
const arr = [1, 2, 3];
const doubled = arr.map(x => x * 2);
const obj = { ...arr, foo: 'bar' };
const result = arr.includes(2);

// 转译后(针对 IE11)
var arr = [1, 2, 3];
var doubled = arr.map(function(x) {
return x * 2;
});
var obj = Object.assign({}, arr, { foo: 'bar' });
var result = arr.indexOf(2) !== -1;

PostCSS 处理

PostCSS 处理 CSS 兼容性问题。

Autoprefixer

// postcss.config.js
module.exports = {
plugins: [
require('autoprefixer'),
],
};
/* 源代码 */
.container {
display: flex;
user-select: none;
}

/* 处理后 */
.container {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}

PostCSS Preset Env

// postcss.config.js
module.exports = {
plugins: [
[
'postcss-preset-env',
{
stage: 2,
features: {
'nesting-rules': true,
'custom-properties': true,
},
autoprefixer: {
grid: true,
},
},
],
],
};
/* 源代码 - 使用现代 CSS 特性 */
:root {
--primary: #007bff;
}

.card {
color: var(--primary);

& .title {
font-weight: bold;
}
}

/* 处理后 - 兼容旧浏览器 */
.card {
color: #007bff;
}

.card .title {
font-weight: bold;
}

CSS 兼容方案

渐进增强

/* 基础样式 - 所有浏览器 */
.layout {
display: block;
}

/* 增强 - 支持 Flexbox */
@supports (display: flex) {
.layout {
display: flex;
gap: 1rem;
}
}

/* 增强 - 支持 Grid */
@supports (display: grid) {
.layout {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
}
}

优雅降级

/* 现代浏览器 */
.gradient-text {
background: linear-gradient(90deg, #ff6b6b, #4ecdc4);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}

/* 不支持时降级 */
@supports not (-webkit-background-clip: text) {
.gradient-text {
color: #ff6b6b;
}
}

CSS 变量降级

.button {
/* 降级值放前面 */
background-color: #007bff;
/* CSS 变量放后面 */
background-color: var(--primary-color, #007bff);
}

常见兼容性问题

1. Flexbox 兼容

.flex-container {
display: -webkit-box; /* iOS 6-, Safari 3.1-6 */
display: -webkit-flex; /* Safari 6.1+ */
display: -ms-flexbox; /* IE 10 */
display: flex;

-webkit-box-orient: horizontal;
-webkit-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row;

-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
}

/* Flexbox gap 降级 */
.flex-with-gap {
display: flex;
flex-wrap: wrap;
margin: -0.5rem; /* 负 margin 模拟 gap */
}

.flex-with-gap > * {
margin: 0.5rem;
}

/* 支持 gap 的浏览器 */
@supports (gap: 1rem) {
.flex-with-gap {
gap: 1rem;
margin: 0;
}

.flex-with-gap > * {
margin: 0;
}
}

2. Grid 兼容

/* IE 11 Grid 语法 */
.grid-container {
display: -ms-grid;
display: grid;

-ms-grid-columns: 1fr 1fr 1fr;
grid-template-columns: repeat(3, 1fr);

-ms-grid-rows: auto auto;
grid-template-rows: auto auto;
}

/* 子元素定位 */
.grid-item:nth-child(1) {
-ms-grid-column: 1;
-ms-grid-row: 1;
}

/* 现代 Grid 特性降级 */
.grid {
display: flex;
flex-wrap: wrap;
}

.grid > * {
flex: 1 1 calc(33.333% - 1rem);
}

@supports (display: grid) {
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
}

.grid > * {
flex: unset;
}
}

3. position: sticky 降级

// Polyfill
function stickyPolyfill(element: HTMLElement, top: number = 0): void {
if (CSS.supports('position', 'sticky')) return;

const offsetTop = element.offsetTop;

window.addEventListener('scroll', () => {
if (window.scrollY >= offsetTop - top) {
element.style.position = 'fixed';
element.style.top = `${top}px`;
} else {
element.style.position = 'static';
}
});
}

4. 滚动行为

html {
/* 降级值 */
scroll-behavior: auto;
/* 现代浏览器 */
scroll-behavior: smooth;
}
// JS Polyfill
function smoothScroll(target: HTMLElement): void {
if ('scrollBehavior' in document.documentElement.style) {
target.scrollIntoView({ behavior: 'smooth' });
} else {
// 手动实现平滑滚动
const targetPosition = target.getBoundingClientRect().top + window.scrollY;
const startPosition = window.scrollY;
const distance = targetPosition - startPosition;
const duration = 500;
let start: number | null = null;

function animation(currentTime: number): void {
if (start === null) start = currentTime;
const progress = Math.min((currentTime - start) / duration, 1);
const ease = progress < 0.5
? 2 * progress * progress
: 1 - Math.pow(-2 * progress + 2, 2) / 2;

window.scrollTo(0, startPosition + distance * ease);

if (progress < 1) {
requestAnimationFrame(animation);
}
}

requestAnimationFrame(animation);
}
}

开发工具

Can I Use

// 查看特性支持情况
// https://caniuse.com/

// 在构建工具中使用
// browserslist 会自动使用 caniuse 数据

Modernizr

// 特性检测库
// npm install modernizr

// 自定义构建
// https://modernizr.com/download

// 使用
if (Modernizr.webgl) {
initWebGL();
} else {
showFallback();
}

常见面试问题

Q1: 什么是渐进增强和优雅降级?

答案

策略描述适用场景
渐进增强先保证基础功能,再为现代浏览器增强内容优先的网站
优雅降级先实现完整功能,再为旧浏览器做降级功能优先的应用
/* 渐进增强 */
.box { border: 1px solid #000; }
@supports (box-shadow: 0 0 5px #000) {
.box { box-shadow: 0 0 5px #000; }
}

/* 优雅降级 */
.box { box-shadow: 0 0 5px #000; }
@supports not (box-shadow: 0 0 5px #000) {
.box { border: 1px solid #000; }
}

Q2: Polyfill 和 Transpiler 的区别?

答案

类型作用示例
Polyfill运行时补充缺失的 APIPromisefetchArray.includes
Transpiler编译时转换语法箭头函数 → 普通函数,async/await → Promise
// Polyfill: 添加缺失的方法
if (!Array.prototype.includes) {
Array.prototype.includes = function() { /* ... */ };
}

// Transpiler: 转换语法
// 源码: const fn = () => {};
// 编译后: var fn = function() {};

Q3: 如何检测浏览器是否支持某个 CSS 特性?

答案

// 1. CSS.supports() API
if (CSS.supports('display', 'grid')) {
console.log('支持 Grid');
}

// 2. @supports CSS 规则
/*
@supports (display: grid) {
.container { display: grid; }
}
*/

// 3. 创建元素检测
function supportsProperty(property: string, value: string): boolean {
const el = document.createElement('div');
el.style.cssText = `${property}: ${value}`;
return el.style.length > 0;
}

Q4: 如何配置 Babel 实现按需 Polyfill?

答案

// babel.config.js
module.exports = {
presets: [
['@babel/preset-env', {
useBuiltIns: 'usage', // 按需引入
corejs: 3,
targets: '> 0.5%, not dead',
}],
],
};

useBuiltIns 选项:

  • false: 不引入 polyfill
  • 'entry': 入口处全量引入
  • 'usage': 按使用引入(推荐)

Q5: browserslist 配置有什么作用?

答案

browserslist 定义目标浏览器范围,被多个工具共享:

  • Babel: 决定转译程度
  • Autoprefixer: 决定添加哪些前缀
  • PostCSS Preset Env: 决定转换哪些 CSS 特性
  • ESLint: 检查 API 兼容性
// package.json
{
"browserslist": [
"> 1%", // 全球使用率 > 1%
"last 2 versions", // 每个浏览器最近 2 个版本
"not dead", // 官方仍在维护
"not IE 11" // 排除 IE 11
]
}

相关链接