Safari HTML5兼容性需运行时能力检测而非UA判断,关键特性如customElements、IntersectionObserver、fetch等须按iOS版本分层降级,服务端配合校验与资源适配更可靠。
Safari 不支持某些 HTML5 特性,不是因为你没“开启”它,而是这些特性在对应版本 Safari 中确实没实现——比如 customElements.define() 在 iOS 10 以下完全不可用,srcset 的 w 描述符在旧版 WebKit 中会被当字符串忽略。降级不是补全所有功能,而是识别哪些能 fallback、哪些必须绕过。
别信 navigator.userAgent,它可能伪造、滞后或误导。必须运行时检测:
'WebSocket' in window、'fetch' in window、!!document.createElement('canvas').getContext('2d') 这些是真实能力信号<input type="date">:创建临时元素后检查 input.type === 'date',返回 false 才代表真不支持IntersectionObserver:直接 typeof IntersectionObserver !== 'function',别依赖 Modernizr —— 它的检测逻辑在 Safari 12.1 之前有误判AudioContext)在 Safari 6–12.0 中存在但行为异常:可创建实例但 resume() 报错,需包裹 try/catch 并监听 statechange 事件不是所有特性都值得降级。关键看是否影响核心流程和用户能否感知:
<picture> + srcset:可安全 fallback —— 只要保留 <img src="fallback.jpg">,老 Safari 会加载它;但注意 media 属性可能被忽略,别把关键断点逻辑放里面<video> 的 playsinline 和 webkit-playsinline:iOS 10+ 支持,旧版 Safari 会自动全屏,无法强制内联,只能接受;但加 muted autoplay 能绕过静音限制,这是可操作点localStorage:IE8+、Safari 3.2+ 都支持,但 iOS Safari 隐私模式下会抛出 QuotaExceededError,必须用 try/catch 包裹写入,失败则退到内存缓存Web Components(customElements.define、Shadow DOM):iOS 10 是分水岭,之前版本无 polyfill 能真正模拟,强行引入 webcomponentsjs 会导致白屏或卡死 —— 应直接函数化组件逻辑,用普通 class + data- 属性驱动很多问题不是 API 缺失,而是环境或调用方式触发了 Safari 特有约束:
立即学习“前端免费学习笔记(深入)”;
fetch() 在 iOS 9–10.2 中 POST 请求静默失败:必须显式设置 headers: { 'Content-Type': 'application/json' },否则请求发不出去input[type="date"] 在 Safari 14.1 之前,input.valueAsNumber 返回 NaN,时间戳计算得改用 new Date(input.value).getTime()
file:// 协议时,localStorage、fetch、甚至 canvas.toDataURL() 全部被 Safari 拦截 —— 必须起 HTTP 服务(如 python3 -m http.server 8000)再测MediaSource 或 WebGL,仅靠特征检测不够,还得在 canPlayType() 后加实际加载测试前端降级只是半程,服务端才是兜底关键:
type="date" 的格式校验 —— 它连 2023-02-30 都拦不住,后端必须用正则 /^d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]d|3[01])$/ 再校一遍User-Agent 返回不同资源:Safari 17+ 可返回 webp + srcset,旧版则只给 jpg + 单 src
Safari/[0-9]+.0.*Mobile 并降级为 PNGnavigator.permissions.query({name: 'notifications'}) 或能力探测结果上报的标记最常被忽略的一点:所谓“兼容 Safari”,其实要拆成三段看——iOS 10–12(WebKit 习惯性 bug 多)、iOS 13–15(API 有了但行为不稳定)、iOS 16+(基本对标 Chrome)。同一段降级代码,在 iOS 12 上能跑,在 iOS 14 上可能因 Promise 微任务顺序变化而失效。别写一次就扔,得按真实设备日志迭代。