往期
10-30 客户端检测:用户代理检测、软硬件检测
用户代理字符串最受争议的地方就是,在很长一段时间里,浏览器都通过在用户代理字符串包含错误或误导性信息来欺骗服务器。要理解背后的原因,必须回顾一下自 Web 出现之后用户代理字符串的历史。
用户代理的历史
早期的浏览器
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36
想要知道自己代码运行在什么浏览器上,大部分开发者会分析 window.navigator.userAgent 返回的字符串值,如果我想试图改变的话,这样做是不行的。
有些浏览器提供伪私有的__defineGetter__方法,利用它可以篡改用户代理字符串
对付这种造假是一件吃力不讨好的事。检测用户代理是否以这种方式被篡改过是可能的,但总体来看还是一场猫捉老鼠的游戏。
与其劳心费力检测造假,不如更好地专注于浏览器识别。如果相信浏览器返回的用户代理字符串,那就可以用它来判断浏览器。如果怀疑脚本或浏览器可能篡改这个值,那最好还是使用能力检测。
软件与硬件的检测
注意 浏览器也可能会利用 Google Location Service(Chrome 和 Firefox)等服务确定位置。有时候,你可能会发现自己并没有 GPS,但浏览器给出的坐标却非常精确。浏览器会收集所有可用的无线网络,包括 Wi-Fi 和蜂窝信号。拿到这些信息后,再去查询网络数据库。这样就可以精确地报告出你的设备位置。
navigator 对象还暴露了 NetworkInformation API,可以通过 navigator.connection 属性使用。这个 API 提供了一些只读属性,并为连接属性变化事件处理程序定义了一个事件对象。
浏览器检测硬件的能力相当有限。不过,navigator 对象还是通过一些属性提供了基本信息。
-
处理器核心数 navigator.hardwareConcurrency 属性返回浏览器支持的逻辑处理器核心数量,包含表示核心数的一个整数值(如果核心数无法确定,这个值就是 1)。关键在于,这个值表示浏览器可以并行执行的最大工作线程数量,不一定是实际的 CPU 核心数。
-
设备内存大小 navigator.deviceMemory 属性返回设备大致的系统内存大小,包含单位为 GB 的浮点数(舍入为最接近的 2 的幂:512MB 返回 0.5,4GB 返回 4)。
-
最大触点数 navigator.maxTouchPoints 属性返回触摸屏支持的最大关联触点数量,包含一个整数值。
10-26 客户端检测:基于能力检测进行浏览器分析
基于能力检测进行浏览器分析
恰当的使用能力分析可以帮助我们精准的分析浏览器
并且使用能力检测而非 用户代理检测 的优点在于:伪造用户代理字符串是简单的,而伪造能力是费劲的,类似于面试吹牛逼,用户代理相当于自己自我介绍,当然可以吹的天花烂坠,可是能力检测就相当于自己要写代码了,所以还是考验真正的东西比较靠谱些
书上介绍了一下内容
- 检测特性(类似浏览器支持什么插件啊,有没有
DOM LEVEL1的能力啦等等 - 检测浏览器(就是判断出你是什么浏览器)
局限:
通过检测一种或者一组能力,并不总能确定使用的是那个浏览器
能力检测适用于决定下一步怎么做,而不是去判断是什么浏览器
用户代理检测
通过浏览器的用户代理字符串 确定用户使用的是什么浏览器。
用户代理字符串 包含在每个 HTTP 请求的头部。在 Javascript 中可以通过 navigator.userAgent
用户代理字符串最受争议的地方就是,在很长一段时间里,浏览器都通过在用户代理字符串包含 错误或误导性信息来欺骗服务器。要理解背后的原因,必须回顾一下自 Web 出现之后用户代理字符串 的历史。
明天在看,历史有点长呢...
10-24、25 客户端检测: 能力检测
浏览器的种类有很多,但是各个浏览器之间还是有差距的
任何时候,我们都应该去设计普适的方案,下下策才是为特定的浏览器做补救措施
能力检测
有的时候我们不太关心它(浏览器)是个什么牛马,只需要判断一下它有没有这个功能,判断一下它有没有这个能力!
检测能力的思路很简单
if (object.propertyInQuestion) {
// 使用object.propertyInQuestion
}
比方说 ie5 没有 document.getElementById() 这个方法,但是有 document.all() 来替代,那么代码就可以这样写
function getElement(id) {
if (document.getElementById) {
return document.getElementById(id);
} else if (document.all) {
return document.all[id];
} else {
throw new Error("No way to retrieve element!");
}
}
书上说,应该先检测最常用的方法,这些方案都可以进行优化。这是因为大多数环境都是可以避免无谓的检测的。 其次就是检测切实需要的特征,某个能力的存在并不代表其他能力也存在。
安全能力检测
能力检测最有效的场景是检测能力是否存在的同时,验证其是否能够展现出预期的行为。前一节中的例子依赖将测试对象的成员转换类型,然后再确定它是否存在。虽然这样能够确定检测的对象成员存在,但不能确定它就是你想要的。来看下面的例子,这个函数尝试检测某个对象是否可以排序:
// 不要这样做!错误的能力检测,只能检测到能力是否存在
function isSortable(object) {
return !!object.sort;
}
这个函数尝试通过检测对象上是否有 sort()方法来确定它是否支持排序。问题在于,即使这个对象有一个 sort 属性,这个函数也会返回 true:
let result = isSortable({ sort: true });
可以这么理解:简单地测试到一个属性存在并不代表这个对象就可以排序。更好的方式是检测 sort 是不是函数
// 好一些,检测 sort 是不是函数
function isSortable(object) {
return typeof object.sort == "function";
}
上面的代码中使用的 typeof 操作符可以确定 sort 是不是函数,从而确认是否可以调用它对数据进行排序。
进行能力检测时应该尽量使用 typeof 操作符,但光有它还不够。尤其是某些宿主对象并不保证对 typeof 测试返回合理的值。最有名的例子就是 Internet Explorer(IE)。在多数浏览器中,下面的代码都会在 document.createElement() 存在时返回 true:
// 不适用于 IE8 及更低版本
function hasCreateElement() {
return typeof document.createElement == "function";
}
但在 IE8 及更低版本中,这个函数会返回 false。这是因为 typeof document.createElement 返回"object"而非"function"。前面提到过,DOM 对象是宿主对象,而宿主对象在 IE8 及更低版本中是通过 COM 而非 JScript 实现的。因此,document.createElement()函数被实现为 COM 对象, typeof 返回"object"。IE9 对 DOM 方法会返回 "function"。
10-23 BOM: navigator、screen、history
navigator 对象
现在已经成为客户端标识浏览器的标准,只要浏览器启用 JavaScript,navigator 对象就一定存在。但是与其他 BOM 对象一样,每个浏览器都支持自己的属性。
检测插件
注册处理程序
现代浏览器支持 navigator 上的(在 HTML5 中定义的) registerProtocolHandler()方法。这个方法可以把一个网站注册为处理某种特定类型信息应用程序。随着在线 RSS 阅读器和电子邮件客户端的流行,可以借助这个方法将 Web 应用程序注册为像桌面软件一样的默认应用程序。
navigator.registerProtocolHandler(
"web+burger",
"https://developer.mozilla.org/?url=%s",
"sha"
);
screen 对象
window 的另一个属性 screen 对象,是为数不多的几个在编程中很少用的 JavaScript对象。这个对象中保存的纯粹是客户端能力信息,也就是浏览器窗口外面的客户端显示器的信息,比如像素宽度和像素高度。每个浏览器都会在 screen 对象上暴露不同的属性。下表总结了这些属性。
history对象
history 对象表示当前窗口首次使用以来用户的导航历史记录。因为 history 是 window 的属性所以每个window 都有自己的 history 对象。出于安全考虑,这个对象不会暴露用户访问过的 URL,但可以通过它在不知道实际 URL 的情况下前进和后退。
go()方法可以在用户历史记录中沿任何方向导航,可以前进也可以后退。这个方法只接收一个参数,这个参数可以是一个整数,表示前进或后退多少步。负值表示在历史记录中后退(类似点击浏览器的“后退”按钮 ),而正值表示在历史记录中前进(类似点击浏览器的“前进”按钮 。下面来看几个例子:
// 后退一页
history.go(-1);
// 前进一页
history.go(1);
// 前进两页
history.go(2);
go()有两个简写方法: back()和 forward()。顾名思义,这两个方法模拟了浏览器的后退按钮和 前进按钮:
// 后退一页
history.back();
// 前进一页
history.forward();
history 对象还有一个length 属性,表示历史记录中有多个条目。这个属性反映了历史记录的数量,包括可以前进和后退的页面。对于窗口或标签页中加载的第一个页面,history.length 等于1.通过以下方法测试这个值,可以确定用户浏览器的起点是不是你的页面:
if (history.length == 1){
// 这是用户窗口中的第一个页面
}
history 对象通常被用于创建“后退”和“前进”按钮,以及确定页面是不是用户历史记录中的第一条记录。
注意: 如果页面 URL 发生变化,则会在历史记录中生成一个新条目。对于 2009 年以来发布的主流浏览器,这包括改变 URL 的散列值 (因此,把 location,hash 设置为一个新值会在这些浏览器的历史记录中增加一条记录)。这个行为常被单页应用程序框架用来模拟前进和后退,这样做是为了不会因导航而触发页面刷新。
历史状态管理
现代 Web 应用程序开发中最难的环节之一就是历史记录管理。用户每次点击都会触发页面刷新的时代早已过去,“后退”和“前进”按钮对用户来说就代表“帮我切换一个状态”的历史也就随之结束了。为解决这个问题,首先出现的是 hashchange 事件 (第 17 章个绍事件时会讨论)。HTML5 也为history 对象增加了方便的状态管理特性。
let stateObject = { foo: "bar" };
window.addEventListener("popstate", (event) => {
let state = event.state;
console.log("state:>>", state);
if (state) {
// handle State
}
});
history.pushState(stateObject, "My title", "index.html#1");
history.back();
基于这个状态,应该把页面重置为状态对象所表示的状态(因为浏览器不会自动为你做这些)。记住,页面初次加载时没有状态。因此点击“后退”按钮直到返回最初页面时,event.state 会为null
可以通过 history.state 获取当前的状态对象,也可以使用 replacestate() 并传人与 pushstate() 同样的前两个参数来更新状态。
更新状态不会创建新历史记录,只会覆盖当前状态:
history.replaceState((newFoo: "newBar"],"New title");
传给 pushstate()和 replacestate()的 state 对象应该只包含可以被序列化的信息。因此 DOM 元素之类并不适合放到状态对象里保存。
注意: 使用HTML5 状态管理时,要确保通过 pushstate()创建的每个“假”URL 背后都对应着服务器上一个真实的物理 URL。否则,单击“刷新”按钮会导致 404 错误。所有单页应用程序(SPA,Single Page Application)框架都必须通过服务器或客户端的某些配置解决这个问题。
10-20 BOM: location
location 是最有用的 BOM 对象之一,提供了当前窗口中加载文档的信息,以及通常的导航功能。这个对象独特的 地方在于,它既是window的属性,也是 document 的属性。也就是说,window.location 和 document.location 指向同一个对象。
location 对象不仅保存着当前加载文档的信息,也保存着把 URL 解析为离散片段后能够通过属性访问的信息。这些解析后的属性在下表中有详细说明(location前缀是必需的)
假 设 浏 览 器 当 前 加 载 的 URL 是 http://foouser:barpassword@www.wrox.com:80/WileyCDA/?q= javascript#contents,location 对象的内容如下表所示。
查询字符串
location 的多数信息都可以通过上面的属性获取。但是 URL 中的查询字符串并不容易使用。虽然 location.search 返回了从问号开始直到 URL 末尾的所有内容,但没有办法逐个访问每个查询参数。下面的函数解析了查询字符串,并返回一个以每个查询参数为属性的对象:
URLSearchParams
URLSearchParams 提供了一组标准API方法,通过它们可以检查和修改查询字符串。给 URLSearchParams 构造函数传入一个查询字符串,就可以创建一个实例。这个实例上暴露了get()、
set() 和 delete() 等方法,可以对查询字符串执行相应操作。下面来看一个例子:
let qs = '?q=javascript&num=10';
let searchParams = new URLSearchParams(qs);
console.log(searchParams.toString()); // " q=javascript&num=10"
searchParams.has('num'); // true
searchParams.get('num'); // 10
searchParams.set('page', '3');
console.log(searchParams.toString()); // " q=javascript&num=10&page=3"
searchParams.delete('q');
console.log(searchParams.toString()); // " num=10&page=3"
操作地址
可以通过修改 location 对象修改浏览器的地址。首先,最常见的是使用 assign()方法并传入一个 URL,如下所示:
location.assign("https://www.bing.com/");
这行代码会立即启动导航到新 URL 的操作,同时在浏览器历史记录中增加一条记录。如果给 location.href 或 window.location 设置一个 URL,也会以同一个 URL 值调用 assign()方法。比如,下面两行代码都会执行与显式调用 assign()一样的操作:
window.location = "https://www.bing.com/";
location.href = "https://www.bing.com/";
在这 3 种修改浏览器地的方法中,设置 location.href 是最常见的。
修改 location 对象的属性也会修改当前加载的页面。其中,hash、search、hostname、pathname 和 port 属性被设置为新值之后都会修改当前 URL,如下面的例子所示:
// 假设当前URL为 http://www.wrox.com/WileyCDA/
// 把URL修改为 http://www.wrox.com/WileyCDA/#section1
location.hash = "#section1";
// 把URL修改为 http://www.wrox.com/WileyCDA/?q=javascript
location.search = "?q=javascript";
// 把URL修改为 http://www.somewhere.com/WileyCDA/
location.hostname = "www.somewhere.com";
// 把URL修改为 http://www.somewhere.com/mydir/
location.pathname = "mydir";
// 把URL修改为 http://www.somewhere.com:8080/WileyCDA/
location.port = 8080;
除了 hash 之外,只要修改 location 的一个属性,就会导致页面重新加载新 URL。
注意: 修改 hash 的值会在浏览器历史中增加一条新纪录. 在早期的 IE 中,点击 后退 和 前进 按钮不会更新 hash 的值,只有点击包含散列的 URL 才会更新 hash 的值
在以前面提到的方式修改 URL 之后,浏览器历史记录中就会增加相应的记录。当用户单击“后退” 按钮时,就会导航到前一个页面。如果不希望增加历史记录,可以使用 replace()方法。这个方法接 收一个 URL 参数,但重新加载后不会增加历史记录。调用 replace()之后,用户不能回到前一页。
<!DOCTYPE html>
<html>
<head>
<title>You won't be able to get back here</title>
</head>
<body>
<p>Enjoy this page for a second, because you won't be coming back here.</p>
<script>
setTimeout(() => location.replace("http://www.wrox.com/"), 1000);
</script>
</body>
</html>
浏览器加载这个页面 1 秒之后会重定向到www.wrox.com。此时,“后退”按钮是禁用状态,即不能返回这个示例面,除非手动输入完整的URL。
reload(),它能重新加载当前显示的页面。调用它不穿参数,页面会以最有效的方式重新加载。也就是说,如果页面自上次请求以来没有修改过,浏览器可能会 从缓存中加载页面。如果想强制从服务器重新加载,可以像下面这样给 reload()传个 true