|
| 1 | +# 《JavaScript 高级程序设计》第九章 : 客户端检测 |
| 2 | + |
| 3 | +## 前言 |
| 4 | + |
| 5 | +本章重点: |
| 6 | + |
| 7 | +- 能力检测与浏览器检测的区别和使用场景。 |
| 8 | +- 了解户代理的历史发展,特别是 `Mozilla` 版本信息的历史。 |
| 9 | +- 使用用户代理进行浏览器检测,但是受限于用户代理会发生变更,所以书上的判断方法以已经失效或者说已不满足当前的场景(新增的 IE11 以及 Edge),但是示例代码中的正则写法还是值得我们去学习的。 |
| 10 | + |
| 11 | +## 能力检测 |
| 12 | + |
| 13 | +能力检测的目的不是识别特定的浏览器,而是识别当前浏览器是否支持特定的功能。 |
| 14 | + |
| 15 | +能力检测的准则是:先检测最常用最通用以及性能更好的方案,后检测妥协的方案,这就如使用 `if...else if...` 语句总会把最可能达到的情况放在之前判断,提高命中率。 |
| 16 | + |
| 17 | +示例: |
| 18 | + |
| 19 | +```javascript |
| 20 | +function getById(id) { |
| 21 | + if (document.getElementById) { |
| 22 | + return document.getElementById(id); |
| 23 | + } else if (document.all) { |
| 24 | + return document.all[id]; |
| 25 | + } else { |
| 26 | + throw new Error("element cannot be retrieved by ID"); |
| 27 | + } |
| 28 | +} |
| 29 | +``` |
| 30 | + |
| 31 | +> IE5 之前的版本不支持 `getElementById`,但支持 document.all 的方式获取 |
| 32 | +
|
| 33 | +更精准的能力检测不仅要检测浏览器是否支持特定的能力,还要在支持的基础上进一步判断能力的类型是否满足我们所需。 |
| 34 | + |
| 35 | +```javascript |
| 36 | +typeof object.sort === "function"; |
| 37 | +``` |
| 38 | + |
| 39 | +我们建议在进行类型判断的时候尽可能的多使用 `typeof` ,因为对于不存在或未定义的标识符通过 `typeof` 检测并不会报错,而是返回 `undefined`。 |
| 40 | + |
| 41 | +但是“类型检测”的滥用往往有可能会给我们造成潜在的 bug,这种情况一般都出于浏览器的兼容性问题,因此在使用的时候,要求我们开发人员要有一个整体性的考量和把控。 |
| 42 | + |
| 43 | +```javascript |
| 44 | +typeof document.createElement === "function"; //false by IE8- |
| 45 | +typeof document.createElement === "object"; //true by IE8- |
| 46 | + |
| 47 | +typeof document.createElement === "function"; //true by IE9+、Chrome、Firefox |
| 48 | +``` |
| 49 | + |
| 50 | +> 在 IE8 及以下版本中对于 `DOM` 的实现是通过调用系统 `COM` 组件方式实现的,所以对于一些 DOM 方法的类型检测就会与其它浏览器的结果不同。 |
| 51 | +
|
| 52 | +“能力检测”结合“类型检测”是一种渐进增强式的检测过程,前者判断有没有,存不存在,而后者则是在存在的基础上,继续判断能力的类型,进行更准确的判断。但这种组合依然无法解决能力本身的兼容性问题,即我们要判断的能力是存在的,类型也是我们所需的,但是“能力”的完善和支持程度在不同的浏览器中并不相同。 |
| 53 | + |
| 54 | +例如我们通过调用 `Array.prototype.slice.call(NodeList, 0)` 可以将 `Like Array` 格式的 `NodeList`对象转换为数组对象,但是这种能力在 `IE8-` 版本中是不被支持的,直接调用会产生错误。 |
| 55 | + |
| 56 | +```javascript |
| 57 | +By IE8- |
| 58 | +Array.prototype.slice.call(box.childNodes, 0) // Array.prototype.slice: 'this' 不是 JavaScript 对象 |
| 59 | +``` |
| 60 | + |
| 61 | +> 问题的原因依然是因为 IE8 及以下的版本中其 DOM 是采用 COM 组件的方式实现,因此 box.childNodes 返回的 nodeList 是一个 COM 对象,而非一个原生的 `NodeList` 对象,虽然 IE9 支持通过借用数组的 `slice` 方法将一个 nodeList 对象转换为数组,但是直到 IE11 才真正的实现了 `NodeList` 类型对象。 |
| 62 | +
|
| 63 | +报错本身也是一种状态反馈,我们完全可以捕获报错,然后在错误处理中采用降级的方案,即通过遍历 `NodeList` 来生成一个数组,从而保证该种能力在所有浏览器中都兼容。 |
| 64 | + |
| 65 | +```javascript |
| 66 | +var arr = []; |
| 67 | +try { |
| 68 | + Array.prototype.slice.call(box.childNodes, 0); |
| 69 | +} catch (e) { |
| 70 | + for (var k in box.childNodes) { |
| 71 | + arr.push(box.childNodes[k]); |
| 72 | + } |
| 73 | +} |
| 74 | +``` |
| 75 | + |
| 76 | +因此,我认为一个完善的“能力检测”方案是结合了 “类型检测”与“错误检测”一起使用。 |
| 77 | + |
| 78 | +## 浏览器检测 |
| 79 | + |
| 80 | +浏览器检测的核心就是“用户代理检测”。 |
| 81 | +需要明确的是“用户代理检测”并不可靠,因为浏览器厂商在发布版本的时候都有可能会对用户代理进行修改,特别是国内的套壳浏览器厂商,更为严重。 |
| 82 | + |
| 83 | +“用户代理检测” 的技术核心在于 `navigator` 对象的 `userAgent` 属性,这个属性原本是按照 `HTTP 1.0/1.1` 的规范,用来定义浏览器向服务端发送用户代理字符串,以标明浏览器的名称、版本等。但现在常用于前端领域对浏览器类型以及版本进行检测。 |
| 84 | + |
| 85 | +不论何种浏览器,通常来说,`navigator.userAgent` 属性中都包含以下信息: |
| 86 | + |
| 87 | +- Mozilla/版本号 |
| 88 | +- 操作系统的信息:`Windows NT 版本号` |
| 89 | +- 计算机操作系统的位长:`WOW64、x64、Win64` |
| 90 | +- 浏览器名称与版本号: `Chrome/版本号`、`Firefox/版本号`、`Edge/版本号`、`MSIE 版本号` |
| 91 | +- 浏览器内核:`Trident`、`Gecko`、`AppleWebKit` |
| 92 | + |
| 93 | +`Mozilla/版本号` 源于 `Netspace` 公司第一个公开发行版 `Netspace Navigator 2` 的用户代理字符串。 |
| 94 | + |
| 95 | +``` |
| 96 | +Mozilla/2.02 [lang] (WinNt; I) |
| 97 | +``` |
| 98 | + |
| 99 | +到了 `Netspace Navigator 3` 的时候则为: |
| 100 | + |
| 101 | +``` |
| 102 | +Mozilla/3.0 (Win95; U) |
| 103 | +``` |
| 104 | + |
| 105 | +而这个时候刚发布的 `IE3` 则考虑 `Netspace` 公司在浏览器市场中占据的无与伦比的份额,为了使很多服务器也支持自家的浏览器,因此也在用户代理中加入了 `Mozilla 2.0` 的标识。 |
| 106 | + |
| 107 | +随着 `Microsoft` 与 `Netspace` 公司在浏览器市场并驾齐驱时,`Netspace Navigator 4` 的用户代理则为 `Mozilla 4.0` 同时期的 `IE4.0` 也对应调整为 `Mozilla 4.0` 而后期随着网景公司的败亡,从 `IE4.0 ~ IE8` 都是 `Mozilla 4.0` 没有变过。 |
| 108 | + |
| 109 | +随着 `IE` 的发展与市场的占有率步入黄昏,微软紧急推出了重大改进版的 `IE9.0`,再一次将用户代理中的 Mozilla 版本标识升级到了 `Mozilla 5.0`,在当时所有的现代浏览器 `Safir`、`Chrome`、`Firefox` 等也都是 `Mozilla 5.0`,`IE9` 的修改也只是紧随其后,一直到目前都是如此。 |
| 110 | + |
| 111 | +这些浏览器之所以都将 `Mozilla 5.0` 加入自己的用户代理中,实际的原因则是为了继承 `Netspace Navigator` 这笔古老的财富,确保过去的用户代理检测脚本以及提供网页内容的服务器更好的兼容自己。而后期的发展则越来越没有底线,比如 `IE11`、`Microsoft Edge`、`Safari` 、`Chrome` 等都在自己的用户代理中都加入了 `like Gecko` 关键字,但 `Gecko` 则是 `Firefox` 的浏览器内核,最早使用的则是 `Netspace Navigator 6` 中,所以我们可以认为这是 IE/Chrome/Safari 集体占火狐的便宜,事情已经不可避免的往更糟糕的方向倾斜,就目前而言 Chrome 里面会含有 `Safari` 的信息,而 `Microsoft Edge` 中则会同时含有 `Chrome` 与 `Safari` 两者的信息。这也就为我们通过 `navigator.userAgent` 来判断浏览类型以及版本又增加了难度。 |
| 112 | + |
| 113 | +下面是搜集到的主流浏览器较新版本的用户代理头信息,以供我们分析: |
| 114 | + |
| 115 | +``` |
| 116 | +IE7 |
| 117 | +"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 10.0; WOW64; Trident/7.0; .NET4.0C; .NET4.0E; .NET CLR 2.0.50727; .NET CLR 3.0.30729; .NET CLR 3.5.30729; Tablet PC 2.0)" |
| 118 | +
|
| 119 | +IE8 |
| 120 | +"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 10.0; WOW64; Trident/7.0; .NET4.0C; .NET4.0E; .NET CLR 2.0.50727; .NET CLR 3.0.30729; .NET CLR 3.5.30729; Tablet PC 2.0)" |
| 121 | +
|
| 122 | +IE9 |
| 123 | +"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 10.0; WOW64; Trident/7.0; .NET4.0C; .NET4.0E; .NET CLR 2.0.50727; .NET CLR 3.0.30729; .NET CLR 3.5.30729; Tablet PC 2.0)" |
| 124 | +
|
| 125 | +IE10 |
| 126 | +"Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 10.0; WOW64; Trident/7.0; .NET4.0C; .NET4.0E; .NET CLR 2.0.50727; .NET CLR 3.0.30729; .NET CLR 3.5.30729; Tablet PC 2.0)" |
| 127 | +
|
| 128 | +IE11 |
| 129 | +"Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; .NET4.0C; .NET4.0E; .NET CLR 2.0.50727; .NET CLR 3.0.30729; .NET CLR 3.5.30729; Tablet PC 2.0; rv:11.0) like Gecko" |
| 130 | +
|
| 131 | +Edge |
| 132 | +"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36 Edge/17.17134" |
| 133 | +
|
| 134 | +Firefox |
| 135 | +"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:68.0) Gecko/20100101 Firefox/68.0" |
| 136 | +
|
| 137 | +Chrome |
| 138 | +"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36" |
| 139 | +
|
| 140 | +Safari |
| 141 | +"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.1 Safari/605.1.15" |
| 142 | +``` |
| 143 | + |
| 144 | +简单的分析,我们可以发现 `Firefox` 的用户代理信息最简洁,其中含有 `Firefox/68` ,`Gecko` 等关键信息,但 `Gecko` 在 IE11、Chrome、Safari、Edge 中都存在,所以我们只能判断是否同时存在 `Firefox` 与 `Gecko` 。 |
| 145 | + |
| 146 | +```javascript |
| 147 | +var ua = navigator.userAgent; |
| 148 | +if (/Firefox\/(\S+)/.test(ua) && ua.indexOf("Gecko") != -1) { |
| 149 | + //Firefox |
| 150 | + //RegExp.$1 - version |
| 151 | +} |
| 152 | +``` |
| 153 | + |
| 154 | +接着第二个少的要数 `Chrome`,但是 `Chrome` 关键字也在很多浏览器中被占用了,所以判断是否为谷歌浏览器,还借助浏览器的提供商属性 `vendor`。 |
| 155 | + |
| 156 | +```javascript |
| 157 | +var ua = navigator.userAgent; |
| 158 | +if (/Chrome\/(\S+)/.test(ua) && navigator.vendor.indexOf("Google") != -1) { |
| 159 | + //Chrome |
| 160 | +} |
| 161 | +``` |
| 162 | + |
| 163 | +接着是 `Safari` ,它的用户代理中有 `Version` 来作为版本标识的关键字,且存在 `Safari` 但没有 `Chrome` 关键字,因此满足这两个条件我们就可以认为是 `Safari`。 |
| 164 | + |
| 165 | +```javascript |
| 166 | +var ua = navigator.userAgent; |
| 167 | +if ( |
| 168 | + /Version\/(\S+)/.test(ua) && |
| 169 | + /(?=.Sfari)(?!.Chrome)/.test(ua) && |
| 170 | + /Apple/.test(navigator.vendor) |
| 171 | +) { |
| 172 | + //Safari |
| 173 | +} |
| 174 | +``` |
| 175 | + |
| 176 | +> 其中 `navigator.vendor` 用于返回浏览器的提供商,但该属性仅限于 PC 端的 Safari 浏览器。 |
| 177 | +
|
| 178 | +对于 `Edge` 这个奇葩浏览器而言,它具有 `Safari` 、`Chrome` 以及 `Gecko` 等关键信息,但所幸的是,它也具有了一个唯一标识自己的 `Edge` 字符。 |
| 179 | + |
| 180 | +```javascript |
| 181 | +var ua = navigator.userAgent; |
| 182 | +if (/Edge\/(\S+)/.test(ua)) { |
| 183 | + //Edge |
| 184 | +} |
| 185 | +``` |
| 186 | + |
| 187 | +最后便是 `IE7~IE11`,非常简单,判断是否有 `Trident` 或 `MSIE` 即可,对于 `IE11` 就比较特殊了,它没有 `MSIE` 标识,但是它有 `rv:11.0` 来表示版本。 |
| 188 | + |
| 189 | +```javascript |
| 190 | +var ua = navigator.userAgent; |
| 191 | +if (/(?:Trident)? | (?:MSIE)?/) { |
| 192 | + if (/MSIE ([^;]+)/.test(ua)) { |
| 193 | + //IE7-IE10 |
| 194 | + } |
| 195 | + if (/rv:([^\)]+)/.test(ua)) { |
| 196 | + //IE11 |
| 197 | + } |
| 198 | +} |
| 199 | +``` |
| 200 | + |
| 201 | +在掌握了每种浏览器的判断后,现在我们来整合这些判断方法,整合的过程会遵循以下原则: |
| 202 | + |
| 203 | +- 将常用的浏览器尽量放在前面检测。 |
| 204 | +- 减少判断的分支条件,尽可能快的定位浏览器的种类。 |
| 205 | + |
| 206 | +下面是具体实现的代码,我们采用模块模式(简单工厂模式)来设计,最后暴漏出一个对象,以方便后期扩展。 |
| 207 | + |
| 208 | +```javascript |
| 209 | +function getClientInfo() { |
| 210 | + var ua = navigator.userAgent; |
| 211 | + var pf = navigator.platform; |
| 212 | + var browser; |
| 213 | + var ver; |
| 214 | + var platform; |
| 215 | + var client = {}; |
| 216 | + |
| 217 | + //Chrome/Edge/Safari |
| 218 | + if (/AppleWebKit/.test(ua)) { |
| 219 | + if ( |
| 220 | + /Version\/(\S+)/.test(ua) && |
| 221 | + /(?=.Sfari)(?!.Chrome)/.test(ua) && |
| 222 | + /Apple/.test(navigator.vendor) |
| 223 | + ) { |
| 224 | + browser = "Safari"; |
| 225 | + ver = RegExp.$1; |
| 226 | + } else if (/Edge\/(\S+)/.test(ua)) { |
| 227 | + browser = "Edge"; |
| 228 | + ver = RegExp.$1; |
| 229 | + } else if ( |
| 230 | + /Chrome\/(\S+)/.test(ua) && |
| 231 | + navigator.vendor.indexOf("Google") != -1 |
| 232 | + ) { |
| 233 | + browser = "Chrome"; |
| 234 | + ver = RegExp.$1; |
| 235 | + } |
| 236 | + } else if (/Firefox\/(\S+)/.test(ua) && ua.indexOf("Gecko") != -1) { |
| 237 | + //Firfox |
| 238 | + browser = "Firefox"; |
| 239 | + ver = RegExp.$1; |
| 240 | + } else if (/(?:Trident)? | (?:MSIE)?/.test(ua)) { |
| 241 | + //IE |
| 242 | + browser = "IE"; |
| 243 | + if (/MSIE ([^;]+)/.test(ua)) { |
| 244 | + ver = RegExp.$1; |
| 245 | + } |
| 246 | + if (/rv:([^\)]+)/.test(ua)) { |
| 247 | + ver = RegExp.$1; |
| 248 | + } |
| 249 | + } |
| 250 | + |
| 251 | + //检测系统 |
| 252 | + if (pf.indexOf("Win") != -1) { |
| 253 | + platform = "Win"; |
| 254 | + } else if (pf.indexOf("Mac") != -1) { |
| 255 | + platform = "Mac"; |
| 256 | + } else if (pf.indexOf("X11") != -1 || pf.indexOf("Linux") != -1) { |
| 257 | + platform = "Linux"; |
| 258 | + } |
| 259 | + |
| 260 | + client.ver = ver ? ver : void 0; |
| 261 | + client.browser = browser ? browser : void 0; |
| 262 | + client.platform = platform ? platform : void 0; |
| 263 | + |
| 264 | + if (typeof client.__proto__ != undefined) { |
| 265 | + client.__proto__ = null; |
| 266 | + } |
| 267 | + |
| 268 | + return client; |
| 269 | +} |
| 270 | +``` |
| 271 | + |
| 272 | +对于 IE 浏览器,`IE10` 以下的版本我们可以通过专有的注释标签,直接在 HTML 中快速定位 IE 的版本。 |
| 273 | + |
| 274 | +```html |
| 275 | +<!--[if IE 7]> |
| 276 | + //仅限IE7 |
| 277 | +<![endif]--> |
| 278 | + |
| 279 | +<!--[if lte IE 7]> |
| 280 | + //仅限小于等于IE7 |
| 281 | +<![endif]--> |
| 282 | + |
| 283 | +<!--[if gt IE 7]> |
| 284 | + //仅限大于IE7 |
| 285 | +<![endif]--> |
| 286 | +``` |
| 287 | + |
| 288 | +注释标签的潜在优势,就是按需加载资源,例如 IE 浏览器需要加载执行 `A` 脚本,而非 IE 则加载执行 `B` 脚本。 |
| 289 | + |
| 290 | +```html |
| 291 | +<!--[if IE]> |
| 292 | + <script src="A.js"></script> |
| 293 | +<![endif]--> |
| 294 | + |
| 295 | +<!--[if !IE]><!--> |
| 296 | +<script src="B.js"></script> |
| 297 | +<!--<![endif]--> |
| 298 | +``` |
| 299 | + |
| 300 | +下面对 IE 条件注释中关键字含义的说明 |
| 301 | + |
| 302 | +- `gt` : 大于 |
| 303 | +- `lt` : 小于 |
| 304 | +- `lte` : 小于等于 |
| 305 | +- `gte` : 大于等于 |
| 306 | +- `!` : 非,取反。 |
| 307 | + |
| 308 | +> IE 浏览器独有的条件注释仅支持 IE9 及以下版本。 |
| 309 | +
|
| 310 | +IE 的条件注释不仅仅只可以写在 HTML 中,实际上还可以结合 JS 脚本来动态的向浏览器中插入: |
| 311 | + |
| 312 | +```javascript |
| 313 | +var _IE = (function() { |
| 314 | + var version = null; |
| 315 | + //增加性能 |
| 316 | + if (document.all || window.ActiveXObject || "ActiveXObject" in window) { |
| 317 | + var div = document.createElement("div"), |
| 318 | + all = div.getElementsByTagName("i"), |
| 319 | + version = 5; |
| 320 | + while ( |
| 321 | + ((div.innerHTML = "<!--[if gt IE " + ++version + "]><i></i><![endif]-->"), |
| 322 | + all[0]) |
| 323 | + ); |
| 324 | + //while循环的第一个参数:循环的第一个参数是一个执行的语句,只要当前IE的版本号大于依次循环生成的版本号,那么就往div里面加一个i标签。 |
| 325 | + //while循环的第二个参数是控制循环的条件。 |
| 326 | + -[1] && version == 6 ? (version = 10) : ""; // 用于解决在IE10 时不支持条件注释的问题。 |
| 327 | + } |
| 328 | + return version; |
| 329 | +})(); |
| 330 | +``` |
0 commit comments