本节摘要:浏览器版本纷杂,新特性在老设备上可能缺失。兼容策略有三件工具:特性查询让样式渐进增强,补齐脚本给旧环境填上缺的能力,转译把新语法翻成旧方言。本节讲三者的分工与选择,给看板定兼容基线,并演示"同一功能在新旧两代设备上的两级体验"。
青云精密的车间里有三种屏:办公室的新显示器跑最新内核浏览器,车间平板是五年前采购的旧版内核,仓库角落还有一台工控机跑着更老的系统内置浏览器。同一份看板代码,新屏上活色生香,旧平板上每年都会有几个新特性"不认识"——样式规则被忽略、脚本方法不存在甚至直接报错中断。兼容问题的本质就这一句:特性在目标环境里存在与否,代码说了不算,环境说了算。
// "特性检测":先问环境有没有,再用 if (typeof CSS === "undefined" || !CSS.supports("display", "grid")) { /* 老环境走旧方案 */ } // 惯例:不认设备型号,只认特性本身——型号永远列不完,特性查一下就知道 // 脚本侧的特性检测三连 if (typeof structuredClone === "function") { const copy = structuredClone(orders); // 新环境:原生深拷贝 } else { const copy = JSON.parse(JSON.stringify(orders)); // 旧环境:序列化往返当替代——够用但函数字段会丢 } const supportsGrid = CSS.supports("display", "grid"); // 样式侧的特性查询接口:脚本里也能问样式能力
/* 渐进增强的样式写法:先写基础版,再给支持者加码 */ .stat-row { display: flex; /* 基础方案:弹性盒,覆盖面广 */ flex-wrap: wrap; gap: 12px; } @supports (gap: 12px) { /* 特性查询:支持间距参数的环境才生效这条增强 */ .stat-row { gap: 16px; } } /* 写法心法:基础版保证"能用",增强版锦上添花; 千万别反过来写成"先全量、不支持再降级"——降级路径永远测不全 */
兼容工具箱里三件家什各管一段。特性查询管样式:某条规则只在支持它的环境生效,旧环境自然落到基础版。补齐脚本管接口:检测到环境缺某个新接口时,当场注入一个等价实现,让代码统一按新接口写。转译管语法:构建阶段把新语法自动翻译成老环境认识的旧语法,开发者全程用新写法,产物却是旧方言。
| 工具 | 管什么 | 谁来做 | 适用场景 |
|---|---|---|---|
| 特性查询 | 样式规则按环境取舍 | 样式班手写 | 新样式特性的渐进增强 |
| 补齐脚本 | 缺失的接口当场补实现 | 引入现成补丁库 | 缺的是新接口而非语法 |
| 转译 | 新语法翻译成旧语法 | 构建工具自动 | 团队全用新写法、产物要跑旧环境 |
三件的边界一句话:缺"能力"用补齐,缺"规则支持"用特性查询,缺"语法支持"用转译。第 6 章讲构建工具链时转译会正式登场,本节把前两件用透。
// 补齐脚本的手写示例:看板用到的"数组找末项" if (!Array.prototype.at) { // 检测:环境缺这个接口才补,有则绝不覆盖 Array.prototype.at = function (index) { if (index < 0) index += this.length; // 负数从尾部数 return this[index]; }; } // 此后代码统一写 orders.at(-1) 取最新一张工单, // 新旧环境行为一致——补齐的使命就是把差异抹平在入口 // 工程实践:手写补齐仅限一两处;缺得多就引入补丁库, // 按看板实际用到的特性定制清单,别全家桶照单全收
<!-- 补丁库的引入方式与位置 --> <head> <script nomodule src="polyfills.legacy.js" defer></script> <!-- nomodule 标记:只有不支持模块的老环境才执行这一份 --> <script type="module" src="board.js" defer></script> <!-- 新环境走模块版;两份产物,各自认领自己的设备 --> </head> <!-- 这套"双产物"策略是转译工具的标配输出,6.2 节正式讲 -->
兼容是无底洞,基线必须先定。定基线的依据不是技术情怀,是设备台账:看板的用户是谁、他们用什么。青云精密的台账显示,最老的工控机只有一个人偶尔查件数,车间平板却有二十几台天天用——基线定在平板那一代,工控机给"仅查询"的降级体验即可。
/* 基线落地:看板的两级体验 */ /* 一级(基线设备,车间平板):全功能 */ .board-layout { display: grid; grid-template-columns: 1fr 280px; grid-template-areas: "header header" "main aside" "footer footer"; } /* 二级(更老设备):单列基础版 */ @supports not (display: grid) { /* 特性查询的否定式:不支持网格的设备走这份 */ .board-layout { display: block; } .board-aside { margin-top: 24px; } /* 侧栏退化为下坠区块 */ } /* 二级版的原则:保数据不保装饰——工单照看、登记照填, 布局朴素一点完全可接受 */
// 脚本侧的同款分级 function boot() { if (!window.fetch) { // 基线之下的设备:不加载远程数据,只启用本地存储的离线版 renderOrders(loadOrders()); showErrorBar("当前设备较旧,已切换离线模式:数据不联网更新"); return; } // 基线设备:完整流程——拉取、渲染、存储 bootOnline().catch(() => renderOrders(loadOrders())); // 在线失败也回落到离线版:降级路径层层兜底 }
兼容工作最容易翻车的地方是"只写了降级、没测过降级"。特性查询的否定分支、补齐脚本的旧路径,恰恰是新设备上永远不走、旧设备上天天在走的代码。验收手段之一:用开发者工具的设备模拟降级内核(若工具支持),或真机连上那台旧平板过一遍核心路径——登记、推进、刷新三个动作能走通,降级就合格。
兼容验收清单(基线设备 + 最老设备各过一遍): - 版面:基线设备全功能版面;最老设备单列基础版面不破 - 数据:登记与推进两设备行为一致(存储接口两代都有) - 请求:基线设备在线拉取;最老设备明确提示离线模式 - 无中断:最老设备控制台无致命报错——脚本中断比样式缺失致命得多 - 双产物:模块版与旧版各认各的设备,互不干扰
⚠️ 常见坑:用浏览器型号或用户代理字符串判断环境写分支。型号字符串可伪造、可更新、种类无穷,分支永远追不全;正路永远是特性检测——问能力不问出身。
💡 关键直觉:把兼容写成"基础版人人可用、增强版能者多得",而不是"全量版人人适配、缺啥再打补丁"。前者的降级路径天然存在且可测,后者的补丁路径写不完也测不完。
老设备安顿好了,下一节给流水线提速:性能优化,用数据说话。