1. 项目概述为什么客户端检测这件事值得我们花时间“雕琢”代码“从丑陋到优雅让代码越变越美”——这个标题乍看像在聊设计美学但放在客户端检测这个具体场景里它其实直指一个被长期忽视的工程现实绝大多数人写的客户端检测逻辑不是功能不对而是“难看”得让人头皮发麻。这里的“丑陋”不是指缩进不规范或变量名不够语义化而是指结构混乱、边界模糊、责任错位、扩展乏力、测试困难甚至在上线后连自己都不敢轻易动它。我做过不下二十个中大型前端项目几乎每个都经历过这样的阶段初期用navigator.userAgent.indexOf(Chrome) -1快速搞定中期加了 iOS 版本判断开始套 if-else 嵌套三层后期要兼容鸿蒙、折叠屏、车机系统代码里塞满了 !/HarmonyOS/.test(ua) /Mobile/.test(ua)这类“祖传条件链”改一行测三天上线前全员祈祷。核心关键词“客户端检测方法”背后藏着三个真实痛点准确性、可维护性、前瞻性。准确性是底线——不能把 iPad 当成 iPhone也不能把 Chrome on Android 误判为 Safari可维护性是生命线——新设备一出你得花 20 分钟定位哪段正则该改而不是花 2 小时翻文档、查 UA 字符串、再手动验证前瞻性是护城河——当 WebKit 内核的国产浏览器开始占据 15% 市场份额你的检测模块是否能不改主干逻辑就接入这篇文章不讲“如何用正则匹配 UA”那只是工具我要带你拆解的是一套可落地、可演进、可测试的客户端检测方法论它适用于所有需要做设备/浏览器/内核/OS 识别的场景比如H5 活动页适配不同安卓厂商的 WebView 行为、管理后台根据终端类型加载不同交互组件、小程序转 H5 时的兜底降级策略。无论你是刚写完第一个if (isIOS)的新人还是正在重构遗留系统的资深工程师这套思路都能让你少踩至少三类典型坑一是“UA 字符串幻觉”以为 UA 是铁板一块实则千变万化二是“检测即业务”把判断逻辑和业务逻辑搅在一起改个判断就得动整个支付流程三是“静态硬编码”版本号写死在 if 里等 iOS 18 发布那天你的兼容列表还在 16.4。我试过七种主流方案纯 UA 正则、第三方库UAParser、bowser、服务端 UA 解析 客户端缓存、基于特性检测的渐进式方案、Web API 组合navigator.platformnavigator.vendormatchMedia、自研轻量解析器、以及混合式分层架构。最终沉淀下来的不是某一行代码而是一套分层抽象 责任分离 渐进增强的设计骨架。它不追求“一次写完永不动”而是让每次新增一个设备型号、升级一个浏览器版本、支持一个新特性都变成一次可预测、可验证、可回滚的微小变更。下面我们就从这个骨架的底层逻辑开始一层层剥开“优雅”的真实构成。2. 核心设计思路为什么放弃“一把梭哈”选择分层抽象与责任分离2.1 传统方案的三大结构性缺陷几乎所有初学者写的客户端检测都默认采用“单点决策”模式一个函数输入 UA 字符串输出一个{ os: iOS, browser: Safari, version: 17.4 }对象。这看似简洁实则埋下三颗定时炸弹第一颗是数据污染。UA 字符串本身就是一个“不可信信源”。它可被用户修改开发者工具覆盖、可被中间代理篡改某些企业网络、可被浏览器主动弱化Chrome 110 默认裁剪 UA 中的 Chrome 版本号只保留Chrome/110.0.0.0。更麻烦的是同一台设备上不同场景 UA 差异巨大微信内置浏览器的 UA 里会塞入MicroMessenger但它的内核可能是 WKWebView 或 X5QQ 浏览器在安卓上用 Blink在 iOS 上却用 WKWebView而国内某超级 App 的 WebViewUA 里甚至不带Mobile关键字。如果你的判断逻辑全押在 UA 上等于把业务稳定性的命门交给了一个随时可能撒谎的“证人”。第二颗是逻辑耦合。检测结果往往直接驱动业务分支“如果是 iOS 且版本 15则隐藏某个动画”、“如果是华为手机则走特殊上报通道”。一旦检测逻辑和业务逻辑写在同一层修改检测规则比如新增对鸿蒙 NEXT 的识别就必须同步修改所有业务调用点。我见过一个电商项目因为要支持折叠屏展开状态检测不得不在 17 个文件里搜索isFoldable逐个确认是否影响下单按钮样式——这不是开发这是考古。第三颗是演进僵化。当团队决定引入“基于特性检测”作为 UA 的补充时比如用CSS.supports(display: grid)判断 CSS Grid 支持度你会发现旧架构根本无法平滑接入。因为原有模块只认“字符串解析结果”不接受“运行时能力返回值”。强行塞进去要么重写整个检测引擎要么搞出两套并行的判断体系最终代码里出现if (detectByUA().os Android || detectByFeature().hasGrid)这种“双轨制”怪胎。提示不要试图用更复杂的正则去修复 UA 的不可靠性。正则越复杂越容易漏掉边缘 case也越难维护。真正的解法是承认 UA 的局限性并为它找到合适的“搭档”。2.2 分层抽象把检测拆成“谁在说”“说了什么”“信多少”三层我们重构的核心是把“客户端检测”这个笼统概念拆解为三个职责清晰、可独立演进的层级采集层Who Said It只负责“获取原始信号”不作任何判断。它聚合所有可用信源navigator.userAgent、navigator.platform、navigator.vendor、window.screen.width/height、matchMedia((hover: hover))、CSS.supports()、甚至window.chrome全局对象是否存在。这一层的目标是“尽可能多拿”哪怕拿到的是矛盾信息比如 UA 说 iOS但navigator.platform返回Win32——这在某些模拟器里真实存在。它输出一个原始信号包RawSignal形如{ ua: Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1, platform: iPhone, vendor: Apple Computer, Inc., screen: { width: 390, height: 844 }, features: { cssGrid: true, webp: true, touch: true } }解析层What Was Said接收 RawSignal进行结构化解析。它不关心信号真假只做“翻译”把 UA 字符串拆解为osName、osVersion、browserName、browserVersion、engineName、engineVersion等标准字段把screen.width和screen.height结合matchMedia推断设备类型手机/平板/桌面把features映射为能力标签hasCssGrid、supportsWebP。关键在于这一层输出的是标准化中间表示IR而非最终业务结论。例如它可能输出{ os: { name: iOS, version: 17.4, family: Unix }, browser: { name: Safari, version: 17.4, engine: WebKit }, device: { type: mobile, model: iPhone 14 Pro }, capabilities: [cssGrid, webp, touch] }决策层How Much to Trust这才是真正面向业务的接口。它接收 IR结合预设的置信度策略Confidence Policy输出带权重的判断结果。例如isIOS()当os.name iOS且os.version可解析时置信度 0.95若仅凭navigator.platform iPhone推断则置信度 0.7。isWebView()当ua包含MicroMessenger或MQQBrowser且browser.engine ! WebKit时置信度 0.9若仅凭!window.chrome !window.opr推断则置信度 0.6。isFoldable()当screen.width 400 screen.height 800 matchMedia((min-width: 800px)).matches时置信度 0.85。这种分层不是为了炫技而是为了解耦。采集层可以随时增加新信源比如未来接入navigator.userAgentDataAPI只要输出格式不变解析层和决策层完全不受影响解析层可以升级 UA 解析算法比如从正则改为语法树解析只要 IR 字段定义一致业务代码零修改决策层可以动态调整置信度阈值比如灰度发布时把isFoldable()的触发阈值从 0.85 降到 0.7无需动到底层。2.3 责任分离检测逻辑与业务逻辑的物理隔离分层之后必须强制“划清界限”。我们约定所有业务代码只能调用决策层提供的、带明确语义的布尔函数如isIOS(),isWebView(),hasTouch()绝不能直接访问解析层的 IR 对象更不能碰采集层的原始信号。这带来两个硬性约束决策层函数必须无副作用、纯函数化。isIOS()只能读取 IR不能发起网络请求、不能修改全局状态、不能依赖时间戳。这样它才能被 Jest 单元测试轻松覆盖也能在服务端渲染SSR环境中安全执行因为 SSR 没有navigator对象但我们可以通过预注入 IR 来模拟。业务代码必须封装检测判断。禁止出现if (detect.isIOS detect.osVersion 15.0)这样的裸判断。正确姿势是// ✅ 好业务逻辑清晰检测细节被封装 if (client.isIOSBelow15()) { disableSmoothScroll(); } // ❌ 坏业务逻辑和检测细节混杂无法复用难以测试 const detect client.parse(); if (detect.os.name iOS compareVersion(detect.os.version, 15.0) 0) { disableSmoothScroll(); }isIOSBelow15()这个函数就是决策层对外暴露的“业务语义接口”。它的内部实现可以是function isIOSBelow15(): boolean { const ir this.parse(); // 获取解析层 IR if (!ir.os || ir.os.name ! iOS) return false; // 使用语义化版本比较而非字符串比对 return semver.lt(ir.os.version, 15.0); }这样当未来需要支持 iOS 18 时你只需修改isIOSBelow15()的内部逻辑比如改成 18.0所有调用点自动生效且测试用例只需验证这个函数的行为无需关心底层 UA 如何解析。3. 核心细节解析采集、解析、决策三层的实操要点与避坑指南3.1 采集层如何安全、全面、兼容地获取原始信号采集层的目标是“宁可多拿不可漏拿”但必须规避常见陷阱。以下是我在多个项目中验证过的实操要点UA 字符串永远做防御性读取navigator.userAgent在部分环境如某些 WebView、禁用 JS 的浏览器可能为undefined或空字符串。错误写法// ❌ 危险未检查 UA 是否存在直接调用 indexOf if (navigator.userAgent.indexOf(iPhone) -1) { ... }正确写法是封装一个安全读取器function getUA(): string { // 优先尝试现代 APIChrome 101 支持 if (userAgentData in navigator navigator.userAgentData) { try { // 注意userAgentData.getHighEntropyValues() 是异步且需权限的此处仅作示意 return navigator.userAgentData.uaList?.[0]?.ua || ; } catch (e) { // 权限不足或异常降级 } } // 降级到传统 UA return navigator.userAgent || ; }更重要的是永远不要假设 UA 是“干净”的。实际采集到的 UA 可能包含\n、\t、乱码字符甚至恶意注入的脚本片段虽然概率低但安全起见。因此解析前必须清洗const rawUA getUA().trim().replace(/[\r\n\t]/g, ); // 避免正则被换行符破坏也防止后续处理出错Platform 与 Vendor它们比 UA 更可靠但有历史包袱navigator.platform在桌面端通常返回Win32、MacIntel、Linux x86_64在移动端返回iPhone、iPad、Android。它的优势是不可伪造浏览器厂商不会允许 JS 修改它劣势是粒度粗Android不区分手机/平板iPhone不区分型号。navigator.vendor则更有趣Safari 返回Apple Computer, Inc.Chrome 返回Google Inc.Firefox 返回Mozilla Foundation。但它在部分国产浏览器中可能为空或返回null。实操中我们发现platform是判断设备大类iOS/Android/Desktop的黄金信源。例如platform iPhone || platform iPad→ 几乎 100% 是 iOS 设备极少数模拟器除外但业务场景可忽略platform.includes(Win) || platform.includes(Mac) || platform.includes(Linux)→ 桌面端platform Android→ 安卓设备注意部分安卓平板会返回Linux armv8l需结合其他信号注意platform在 iOS 13 的 Safari 中曾短暂返回MacIntel因桌面版 Safari 的 UA 伪装但苹果很快修复。当前iOS 17已回归iPhone/iPad。不过为防万一我们的采集层会同时记录platform和ua供解析层交叉验证。特性检测不是“替代”而是“补全”特性检测Feature Detection是 UA 检测的天然互补。UA 告诉你“它声称是什么”特性检测告诉你“它实际能做什么”。但新手常犯两个错误一是过度依赖单一特性如只用window.Promise判断 ES6 支持却忽略Promise.allSettled的兼容性差异二是忽略特性检测的性能成本如频繁调用CSS.supports()。我们的采集层对特性检测做了三点优化批量缓存将常用特性检测封装为一个惰性求值对象在首次调用时统一执行后续直接返回缓存值。const features { get cssGrid() { if (this._cssGrid undefined) { this._cssGrid CSS.supports(display, grid); } return this._cssGrid; }, _cssGrid: undefined as boolean | undefined, // 其他特性... };分层采样对高成本特性如matchMedia查询只在必要时采集。例如isFoldable()决策函数触发时才去查询matchMedia((min-width: 800px))而非在采集层就全部执行。语义化包装不直接暴露原生 API而是提供业务友好接口。例如hasTouch()不是简单返回ontouchstart in window而是综合(ontouchstart in window || navigator.maxTouchPoints 0)并排除某些误报场景如 Windows 触控笔记本在桌面模式下maxTouchPoints 0但实际无触控。3.2 解析层从 UA 字符串到结构化 IR 的精准翻译解析层是整个架构的“翻译官”其质量直接决定上层决策的可靠性。我们摒弃了所有第三方 UA 解析库UAParser.js 体积大、更新慢、对国产浏览器支持弱选择自研轻量解析器核心原则是以白名单为主黑名单为辅以确定性规则优先启发式推断兜底。白名单驱动的 UA 解析引擎我们维护一个 JSON 格式的 UA 白名单数据库ua-patterns.json按优先级排序。每条规则包含pattern: 正则表达式使用i标志不区分大小写os: 操作系统信息name, versionPattern, familybrowser: 浏览器信息name, versionPattern, enginedevice: 设备信息type, modelPatternconfidence: 该规则的初始置信度0.7~0.95例如针对 iOS Safari 的规则{ pattern: iPhone.*OS (\\d)_(\\d), os: { name: iOS, versionPattern: $1.$2, family: Unix }, browser: { name: Safari, engine: WebKit }, device: { type: mobile, modelPattern: iPhone $1,$2 }, confidence: 0.95 }解析流程是遍历白名单对 UA 字符串执行pattern.exec()第一个匹配成功的规则即为“主规则”提取其versionPattern中的捕获组填充版本号。若无匹配则启用“兜底规则”如.*AppleWebKit.*Mobile.*→iOS置信度 0.7。为什么不用 UAParserUAParser 的解析逻辑是“贪婪匹配”它会尝试匹配所有可能的浏览器组合导致在复杂 UA如微信内置浏览器中产生歧义。例如一个微信 UAMozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.46(...)。UAParser 可能同时匹配到Safari和WeChat规则最终返回哪个取决于内部优先级而这个优先级并不透明。我们的白名单规则明确声明MicroMessenger规则优先级高于Safari因此微信 UA 必定被识别为WeChat浏览器Safari仅作为内核信息保留。这种可控性是业务稳定性的基石。版本号解析语义化比较是刚需字符串比较17.4 17.10会返回true因为10 4字典序这是灾难性的。我们必须使用语义化版本比较SemVer。但并非所有版本号都符合 SemVer 规范如Chrome/110.0.5481.177中的110.0.5481.177是四段式。我们的解析层内置一个灵活的parseVersion(str)函数function parseVersion(versionStr: string): number[] { // 移除非数字非点字符如 17.4.1 (18D52) → 17.4.1 const clean versionStr.replace(/[^0-9.]/g, ); // 拆分为数字数组自动补零至 4 位便于比较 const parts clean.split(.).map(p parseInt(p, 10)); while (parts.length 4) parts.push(0); return parts.slice(0, 4); } // 比较函数[17,4,0,0] vs [17,10,0,0] → -1小于 function compareVersion(v1: string, v2: string): number { const a parseVersion(v1); const b parseVersion(v2); for (let i 0; i 4; i) { if (a[i] b[i]) return -1; if (a[i] b[i]) return 1; } return 0; }这个函数确保compareVersion(17.4, 17.10)返回-1正确而非1字符串比较错误结果。3.3 决策层置信度策略与业务语义接口的设计艺术决策层是用户业务开发者唯一接触的接口它的设计直接决定了代码的“优雅度”。我们坚持三个设计信条语义清晰、置信可调、组合自由。语义清晰函数名即契约每一个导出的函数其名称必须精确描述其业务意图而非技术实现。例如isIOS()表示“当前环境是 iOS 系统”不关心是 Safari 还是微信 WebView。isSafari()表示“当前浏览器是 Safari”不关心它运行在 iOS 还是 macOS。isWebView()表示“当前运行在 WebView 容器中”不关心是 X5 还是 WKWebView。isFoldable()表示“当前设备具备可折叠形态”不关心是硬件传感器还是 CSS 媒体查询推断。这种命名杜绝了歧义。当业务同学说“需要在折叠屏上隐藏导航栏”你直接给他isFoldable()他不需要理解 UA 解析原理只需要知道“调用这个函数返回 true 就隐藏”。置信可调让判断结果带上“可信度”标签我们不返回简单的true/false而是返回一个DecisionResult对象interface DecisionResult { value: boolean; // 最终布尔结果 confidence: number; // 置信度0.0 ~ 1.0 source: string; // 决策依据如 ua-ios-pattern, platform-iPhone reason?: string; // 简短说明如 Matched iOS UA pattern with high confidence }业务代码可以按需使用// ✅ 默认行为只关心布尔值 if (client.isFoldable().value) { ... } // ✅ 高级用法根据置信度做灰度 const foldable client.isFoldable(); if (foldable.value foldable.confidence 0.85) { // 高置信度启用完整折叠屏体验 enableFoldableUI(); } else if (foldable.value) { // 中置信度启用简化版 enableBasicFoldableUI(); }置信度的计算不是拍脑袋。我们为每种信源设定基准值并根据交叉验证动态调整UA pattern match0.95白名单精确匹配platform iPhone0.9平台不可伪造navigator.vendor Apple Computer, Inc.0.85厂商标识较可靠matchMedia((min-width: 800px)).matches0.7媒体查询易受缩放、横竖屏影响当多个信源指向同一结论时置信度会叠加非简单相加而是按对数衰减公式计算例如UA says iOS0.95 platform says iPhone0.9 → 最终置信度 0.97。组合自由用逻辑运算符构建复杂业务条件决策层提供and()、or()、not()等高阶函数让业务方能自由组合原子判断// 业务需求在 iOS 15 的 Safari 中启用新动画 const useNewAnimation client.and( client.isIOS(), client.isSafari(), client.isIOSAtLeast(15.0) ); // 业务需求在非微信、非 QQ 的 WebView 中禁用某功能 const disableInWebView client.or( client.isWebView(), client.not(client.isWeChat()), client.not(client.isQQBrowser()) );这些组合函数内部会智能合并信源避免重复解析。例如and(isIOS(), isSafari())会复用同一个 UA 解析结果而不是分别解析两次。4. 实操过程从零搭建一个可运行的客户端检测模块4.1 项目初始化与目录结构我们以一个标准的 TypeScript 项目为例兼容 Vue/React/Angular创建一个独立的myorg/client-detect包。目录结构如下src/ ├── index.ts # 入口导出 ClientDetector 类 ├── collector/ # 采集层 │ ├── index.ts # 采集器主逻辑 │ └── features.ts # 特性检测封装 ├── parser/ # 解析层 │ ├── index.ts # 解析器主逻辑 │ └── patterns/ # UA 白名单规则 │ ├── ios.ts │ ├── android.ts │ └── wechat.ts ├── decision/ # 决策层 │ ├── index.ts # 决策函数集合 │ └── policies.ts # 置信度策略配置 └── types/ # 类型定义 └── index.ts这种结构确保各层物理隔离便于团队分工前端 A 负责 collectorB 负责 parserC 负责 decision也方便单元测试单独 mocking 某一层。4.2 采集层实现安全、健壮的原始信号获取src/collector/index.ts的核心是Collector类export class Collector { private cache: RawSignal | null null; // 主采集方法返回缓存或重新采集 collect(): RawSignal { if (this.cache) return this.cache; const ua this.getSafeUA(); const platform this.getSafePlatform(); const vendor this.getSafeVendor(); const screen this.getScreenInfo(); const features this.collectFeatures(); this.cache { ua, platform, vendor, screen, features }; return this.cache; } private getSafeUA(): string { // 如前文所述的安全 UA 获取逻辑 let ua ; if (typeof navigator ! undefined) { ua navigator.userAgent || ; } return ua.trim().replace(/[\r\n\t]/g, ); } private getSafePlatform(): string { if (typeof navigator ! undefined navigator.platform) { return navigator.platform; } return ; } private getSafeVendor(): string { if (typeof navigator ! undefined navigator.vendor) { return navigator.vendor; } return ; } private getScreenInfo(): ScreenInfo { if (typeof window undefined) { return { width: 0, height: 0 }; } return { width: window.screen.width, height: window.screen.height, }; } private collectFeatures(): Features { // 特性检测使用前文所述的惰性缓存模式 return { cssGrid: CSS.supports(display, grid), webp: this.hasWebPSupport(), touch: this.hasTouchSupport(), // ...其他特性 }; } // 其他辅助方法... }关键点在于collect()方法的缓存机制。它保证在整个页面生命周期内原始信号只采集一次避免重复读取navigator对象某些 WebView 下多次读取可能返回不同值。4.3 解析层实现白名单驱动的 UA 翻译器src/parser/index.ts的核心是Parser类它依赖patterns目录下的规则import { IOS_PATTERN } from ./patterns/ios; import { ANDROID_PATTERN } from ./patterns/android; // ...导入其他规则 export class Parser { private patterns: PatternRule[] [ IOS_PATTERN, ANDROID_PATTERN, WECHAT_PATTERN, // ...按优先级排序 ]; parse(rawSignal: RawSignal): IntermediateRepresentation { const { ua, platform, vendor, screen, features } rawSignal; let ir: PartialIntermediateRepresentation {}; // 第一步UA 解析主路径 const uaMatch this.matchUA(ua); if (uaMatch) { ir { ...ir, ...uaMatch.ir }; ir.confidence uaMatch.confidence; } // 第二步平台/厂商交叉验证提升置信度 if (platform platform.toLowerCase().includes(iphone)) { ir.os { ...ir.os, name: iOS, family: Unix }; ir.device { ...ir.device, type: mobile }; ir.confidence Math.max(ir.confidence || 0.5, 0.9); } // 第三步特性补全如根据 touch 特性推断 device.type if (features.touch screen.width 500) { ir.device { ...ir.device, type: mobile }; } // 确保 IR 结构完整 return this.ensureIRStructure(ir); } private matchUA(ua: string): { ir: PartialIntermediateRepresentation, confidence: number } | null { for (const rule of this.patterns) { const match rule.pattern.exec(ua); if (match) { const ir: PartialIntermediateRepresentation {}; // 根据 rule.os, rule.browser 等填充 ir 字段 // 使用 rule.versionPattern 提取版本号 return { ir, confidence: rule.confidence }; } } return null; } private ensureIRStructure(ir: PartialIntermediateRepresentation): IntermediateRepresentation { // 填充默认值确保所有字段存在 return { os: { name: Unknown, version: 0.0.0, family: Unknown }, browser: { name: Unknown, version: 0.0.0, engine: Unknown }, device: { type: unknown, model: Unknown }, capabilities: [], ...ir, }; } }patterns/ios.ts示例export const IOS_PATTERN: PatternRule { pattern: /iPhone.*OS (\d)_(\d)/i, os: { name: iOS, versionPattern: $1.$2, family: Unix, }, browser: { name: Safari, engine: WebKit, }, device: { type: mobile, modelPattern: iPhone $1,$2, }, confidence: 0.95, };这个设计让新增一个设备如鸿蒙变得极其简单只需在patterns/harmony.ts中添加一条新规则然后把它插入patterns数组的合适位置通常在 Android 之后iOS 之前无需修改任何解析逻辑。4.4 决策层实现语义化接口与置信度引擎src/decision/index.ts导出ClientDetector类它是业务方的唯一入口import { Collector } from ../collector; import { Parser } from ../parser; import { DecisionResult, ConfidencePolicy } from ./policies; export class ClientDetector { private collector: Collector; private parser: Parser; private policy: ConfidencePolicy; constructor(policy: ConfidencePolicy defaultPolicy) { this.collector new Collector(); this.parser new Parser(); this.policy policy; } // 原子决策函数 isIOS(): DecisionResult { const ir this.getIR(); const isIOS ir.os.name iOS; const confidence this.policy.getConfidence(os, iOS, isIOS, ir); return { value: isIOS, confidence, source: os-name, reason: OS name matched iOS }; } isSafari(): DecisionResult { const ir this.getIR(); const isSafari ir.browser.name Safari; const confidence this.policy.getConfidence(browser, Safari, isSafari, ir); return { value: isSafari, confidence, source: browser-name, reason: Browser name matched Safari }; } isFoldable(): DecisionResult { const ir this.getIR(); // 综合 UA、platform、screen、media query 多个信号 const isFoldable this.isFoldableByScreen(ir) || this.isFoldableByUA(ir); const confidence this.policy.getConfidence(foldable, true, isFoldable, ir); return { value: isFoldable, confidence, source: foldable-combined, reason: Combined signals indicate foldable }; } // 组合函数 and(...decisions: (() DecisionResult)[]): DecisionResult { const results decisions.map(fn fn()); const value results.every(r r.value); const confidence this.policy.combineConfidence(results, and); return { value, confidence, source: combined-and, reason: And of ${results.length} decisions }; } // 工具方法 private getIR(): IntermediateRepresentation { const raw this.collector.collect(); return this.parser.parse(raw); } private isFoldableByScreen(ir: IntermediateRepresentation): boolean { // 实现基于屏幕尺寸和媒体查询的折叠屏判断 if (typeof window undefined) return false; const