
ES5
约 4382 字大约 15 分钟
2024-07-20
10. 浏览器模型
web
1. 概述
随着电脑计算能力的增强,尤其是多核 CPU 的出现,单线程带来很大的不便,无法充分发挥计算机的计算能力。
Web Worker 的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。这样的好处是,一些计算密集型或高延迟的任务可以交由 Worker 线程执行,主线程(通常负责 UI 交互)能够保持流畅,不会被阻塞或拖慢。
Worker 线程一旦新建成功,就会始终运行,不会被主线程上的活动(比如用户点击按钮、提交表单)打断。这样有利于随时响应主线程的通信。但是,这也造成了 Worker 比较耗费资源,不应该过度使用,而且一旦使用完毕,就应该关闭。
Web Worker 有以下几个使用注意点。
(1)同源限制
分配给 Worker 线程运行的脚本文件,必须与主线程的脚本文件同源。
(2)DOM 限制
Worker 线程所在的全局对象,与主线程不一样,无法读取主线程所在网页的 DOM 对象,也无法使用 document、window、parent 这些对象。但是,Worker 线程可以使用 navigator 对象和 location 对象。
(3)全局对象限制
Worker 的全局对象 WorkerGlobalScope,不同于网页的全局对象 Window,很多接口拿不到。比如,理论上 Worker 线程不能使用 console.log,因为标准里面没有提到 Worker 的全局对象存在 console 接口,只定义了 Navigator 接口和 Location 接口。不过,浏览器实际上支持 Worker 线程使用 console.log,保险的做法还是不使用这个方法。
(4)通信联系
Worker 线程和主线程不在同一个上下文环境,它们不能直接通信,必须通过消息完成。
(5)脚本限制
Worker 线程不能执行 alert()方法和 confirm()方法,但可以使用 XMLHttpRequest 对象发出 AJAX 请求。
(6)文件限制
Worker 线程无法读取本地文件,即不能打开本机的文件系统(file://),它所加载的脚本,必须来自网络。
2. 基本用法
主线程
主线程采用 new 命令,调用 Worker()构造函数,新建一个 Worker 线程。
var worker = new Worker("work.js")
// 发送消息
worker.postMessage("Hello World")
worker.postMessage({ method: "echo", args: ["Work"] })
// 监听线程传回的消息
worker.onmessage = function (event) {
console.log(event.data)
}
// 关闭线程
worker.terminate()
Worker()构造函数的参数是一个脚本文件,该文件就是 Worker 线程所要执行的任务。由于 Worker 不能读取本地文件,所以这个脚本必须来自网络。如果下载没有成功(比如 404 错误),Worker 就会默默地失败。
Worker 线程
Worker 线程内部需要有一个监听函数,监听 message 事件。
self.addEventListener(
"message",
function (e) {
var data = e.data
switch (data.cmd) {
case "start":
self.postMessage("WORKER STARTED: " + data.msg)
break
case "stop":
self.postMessage("WORKER STOPPED: " + data.msg)
self.close() // Terminates the worker.
break
default:
self.postMessage("Unknown command: " + data.msg)
}
},
false
)
3. 数据通信
主线程与 Worker 之间的通信内容,可以是文本,也可以是对象。需要注意的是,这种通信是拷贝关系,即是传值而不是传址,Worker 对通信内容的修改,不会影响到主线程。事实上,浏览器内部的运行机制是,先将通信内容串行化,然后把串行化后的字符串发给 Worker,后者再将它还原。
主线程与 Worker 之间也可以交换二进制数据,比如 File、Blob、ArrayBuffer 等类型,也可以在线程之间发送。
4. 实例:Worker 线程完成轮询
File
1. File 对象
File 对象代表一个文件,用来读写文件信息。它继承了 Blob 对象,或者说是一种特殊的 Blob 对象,所有可以使用 Blob 对象的场合都可以使用它。
最常见的使用场合是表单的文件上传控件(),用户选中文件以后,浏览器就会生成一个数组,里面是每一个用户选中的文件,它们都是 File 实例对象。
// HTML 代码如下
// <input id="fileItem" type="file">
var file = document.getElementById("fileItem").files[0]
file instanceof File // true
上面代码中,file 是用户选中的第一个文件,它是 File 的实例。
同源策略
1. 概述
最初,它的含义是指,A 网页设置的 Cookie,B 网页不能打开,除非这两个网页“同源”。所谓“同源”指的是“三个相同”。 同源政策的目的,是为了保证用户信息的安全,防止恶意的网站窃取数据。
- 协议相同
- 域名相同
- 端口相同(这点可以忽略,详见下文)
随着互联网的发展,同源政策越来越严格。目前,如果非同源,共有三种行为受到限制。
(1) 无法读取非同源网页的 Cookie、LocalStorage 和 IndexedDB。
(2) 无法接触非同源网页的 DOM。
(3) 无法向非同源地址发送 AJAX 请求(可以发送,但浏览器会拒绝接受响应)。
2. Cookie
Cookie 是服务器写入浏览器的一小段信息,只有同源的网页才能共享。如果两个网页一级域名相同,只是次级域名不同,浏览器允许通过设置 document.domain 共享 Cookie。
CORS 通信
CORS 是一个 W3C 标准,全称是“跨源资源共享”(Cross-origin resource sharing),或者通俗地称为“跨域资源共享”。它允许浏览器向跨源的服务器,发出 XMLHttpRequest 请求,从而克服了 AJAX 只能同源使用的限制。
1. 简介
CORS 需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能。
整个 CORS 通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS 通信与普通的 AJAX 通信没有差别,代码完全一样。浏览器一旦发现 AJAX 请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感知。因此,实现 CORS 通信的关键是服务器。只要服务器实现了 CORS 接口,就可以跨源通信。
2.
CORS 请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。
只要同时满足以下两大条件,就属于简单请求。
(1)请求方法是以下三种方法之一。
- HEAD
- GET
- POST
(2)HTTP 的头信息不超出以下几种字段。
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type:只限于三个值 application/x-www-form-urlencoded、multipart/form-data、text/plain
凡是不同时满足上面两个条件,就属于非简单请求。一句话,简单请求就是简单的 HTTP 方法与简单的 HTTP 头信息的结合。
这样划分的原因是,表单在历史上一直可以跨源发出请求。简单请求就是表单请求,浏览器沿袭了传统的处理方式,不把行为复杂化,否则开发者可能转而使用表单,规避 CORS 的限制。对于非简单请求,浏览器会采用新的处理方式。
3. 简单请求
对于简单请求,浏览器直接发出 CORS 请求。具体来说,就是在头信息之中,增加一个 Origin 字段。
Origin 字段用来说明,本次请求来自哪个域(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。
如果 Origin 指定的源,不在许可范围内,服务器会返回一个正常的 HTTP 回应。浏览器发现,这个回应的头信息没有包含 Access-Control-Allow-Origin 字段(详见下文),就知道出错了,从而抛出一个错误,被 XMLHttpRequest 的 onerror 回调函数捕获。注意,这种错误无法通过状态码识别,因为 HTTP 回应的状态码有可能是 200。
如果 Origin 指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8
上面的头信息之中,有三个与 CORS 请求相关的字段,都以 Access-Control-开头。
CORS 请求默认不包含 Cookie 信息(以及 HTTP 认证信息等),这是为了降低 CSRF攻击的风险。但是某些场合,服务器可能需要拿到 Cookie,这时需要服务器显式指定 Access-Control-Allow-Credentials 字段,告诉浏览器可以发送 Cookie。
如果服务器要求浏览器发送 Cookie,Access-Control-Allow-Origin 就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie 依然遵循同源政策,只有用服务器域名设置的 Cookie 才会上传,其他域名的 Cookie 并不会上传,且(跨源)原网页代码中的 document.cookie 也无法读取服务器域名下的 Cookie。
4. 非简单请求
预检请求:
非简单请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求,称为“预检”请求(preflight)。浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些 HTTP 方法和头信息字段。只有得到肯定答复,浏览器才会发出正式的 XMLHttpRequest 请求,否则就报错。这是为了防止这些新增的请求,对传统的没有 CORS 支持的服务器形成压力,给服务器一个提前拒绝的机会,这样可以防止服务器收到大量 DELETE 和 PUT 请求,这些传统的表单不可能跨源发出的请求。
Storage 接口
1. 概述
Storage 接口用于脚本在浏览器保存数据。两个对象部署了这个接口:window.sessionStorage 和 window.localStorage。
sessionStorage 保存的数据用于浏览器的一次会话(session),当会话结束(通常是窗口关闭),数据被清空;localStorage 保存的数据长期存在,下一次访问该网站的时候,网页可以直接读取以前保存的数据。除了保存期限的长短不同,这两个对象的其他方面都一致。
保存的数据都以“键值对”的形式存在。也就是说,每一项数据都有一个键名和对应的值。所有的数据都是以文本格式保存。
IntersectionObserver API
网页开发时,常常需要了解某个元素是否进入了“视口”(viewport),即用户能不能看到它。
传统的实现方法是,监听到 scroll 事件后,调用目标元素(绿色方块)的 getBoundingClientRect()方法,得到它对应于视口左上角的坐标,再判断是否在视口之内。这种方法的缺点是,由于 scroll 事件密集发生,计算量很大,容易造成性能问题。
IntersectionObserver API,可以自动“观察”元素是否可见,Chrome 51+ 已经支持。由于可见(visible)的本质是,目标元素与视口产生一个交叉区,所以这个 API 叫做“交叉观察器”(intersection
1. 简介
InsertSectionObserver 用法
let observer = new IntersectionObserver(callback, options)
observer.observe(target)
上面代码中,IntersectionObserver 是浏览器原生提供的构造函数,接受两个参数:callback 是可见性变化时的回调函数,option 是配置对象(该参数可选)。
IntersectionObserver()的返回值是一个观察器实例。实例的 observe()方法可以指定观察哪个 DOM 节点。
// 开始观察
observer.observe(document.getElementById("example"))
// 停止观察
observer.unobserve(element)
// 关闭观察器
observer.disconnect()
上面代码中,observe()的参数是一个 DOM 节点对象。如果要观察多个节点,就要多次调用这个方法。
observer.observe(elementA)
observer.observe(elementB)
注意,IntersectionObserver API 是异步的,不随着目标元素的滚动同步触发。规格写明,IntersectionObserver 的实现,应该采用 requestIdleCallback(),即只有线程空闲下来,才会执行观察器。这意味着,这个观察器的优先级非常低,只在其他任务执行完,浏览器有了空闲才会执行。
2. IntersectionObserver.observe()
IntersectionObserver 实例的 observe()方法用来启动对一个 DOM 元素的观察。该方法接受两个参数:回调函数 callback 和配置对象 options。
2.1 callback 参数
目标元素的可见性变化时就会调用回调函数,回调函数会触发两次,一次是目标开始可见时, 一次是目标完全不可见(离开视口)时。
3. 应用
惰性加载(lazy
有时,我们希望某些静态资源(比如图片),只有用户向下滚动,它们进入视口时才加载,这样可以节省带宽,提高网页性能。这就叫做“惰性加载”。
有了 IntersectionObserver API,实现起来就很容易了。图像的 HTML 代码可以写成下面这样。
<img src="placeholder.png" data-src="img-1.jpg">
<img src="placeholder.png" data-src="img-2.jpg">
<img src="placeholder.png" data-src="img-3.jpg">
上面代码中,图像默认显示一个占位符,data-src属性是惰性加载的真正图像。
function query(selector) {
return Array.from(document.querySelectorAll(selector));
}
var observer = new IntersectionObserver(
function(entries) {
entries.forEach(function(entry) {
entry.target.src = entry.target.dataset.src;
observer.unobserve(entry.target);
});
}
);
query('.lazy-loaded').forEach(function (item) {
observer.observe(item);
});
上面代码中,只有图像开始可见时,才会加载真正的图像文件。
无限滚动
无限滚动(infinite scroll)指的是,随着网页滚动到底部,不断加载新的内容到页面,它的实现也很简单。
var intersectionObserver = new IntersectionObserver(
function (entries) {
// 如果不可见,就返回
if (entries[0].intersectionRatio <= 0) return;
loadItems(10);
console.log('Loaded new items');
}
);
// 开始观察
intersectionObserver.observe(
document.querySelector('.scrollerFooter')
);
无限滚动时,最好像上例那样,页面底部有一个页尾栏(又称sentinels,上例是.scrollerFooter)。一旦页尾栏可见,就表示用户到达了页面底部,从而加载新的条目放在页尾栏前面。否则就需要每一次页面加入新内容时,都调用observe()方法,对新增内容的底部建立观察。
Page Lifecycle API
Android、iOS 和最新的 Windows 系统可以随时自主地停止后台进程,及时释放系统资源。也就是说,网页可能随时被系统丢弃掉。以前的浏览器 API 完全没有考虑到这种情况,导致开发者根本没有办法监听到系统丢弃页面。
为了解决这个问题,W3C 新制定了一个 Page Lifecycle API,统一了网页从诞生到卸载的行为模式,并且定义了新的事件,允许开发者响应网页状态的各种转换。
Server-Sent Events
1. 简介
服务器向客户端推送数据,有很多解决方案。除了“轮询” 和 WebSocket,HTML 5 还提供了 Server-Sent Events(以下简称 SSE)。
(streaming)。也就是说,发送的不是一次性的数据包,而是一个数据流,会连续不断地发送过来。这时,客户端不会关闭连接,会一直等着服务器发过来的新的数据流。本质上,这种通信就是以流信息的方式,完成一次用时很长的下载。
SSE 就是利用这种机制,使用流信息向浏览器推送信息。它基于 HTTP 协议,目前除了 IE/Edge,其他浏览器都支持。
2. 与 WebSocket 的比较
SSE 与 WebSocket 作用相似,都是建立浏览器与服务器之间的通信渠道,然后服务器向浏览器推送信息。
总体来说,WebSocket 更强大和灵活。因为它是全双工通道,可以双向通信;SSE 是单向通道,只能服务器向浏览器发送,因为 streaming 本质上就是下载。如果浏览器向服务器发送信息,就变成了另一次 HTTP 请求。
SSE 与 WebSocket 作用相似,都是建立浏览器与服务器之间的通信渠道,然后服务器向浏览器推送信息。
总体来说,WebSocket 更强大和灵活。因为它是全双工通道,可以双向通信;SSE 是单向通道,只能服务器向浏览器发送,因为 streaming 本质上就是下载。如果浏览器向服务器发送信息,就变成了另一次 HTTP
但是,SSE 也有自己的优点。
- SSE 使用 HTTP 协议,现有的服务器软件都支持。WebSocket 是一个独立协议。
- SSE 属于轻量级,使用简单;WebSocket 协议相对复杂。
- SSE 默认支持断线重连,WebSocket 需要自己实现断线重连。
- SSE 一般只用来传送文本,二进制数据需要编码后传送,WebSocket 默认支持传送二进制数据。
- SSE 支持自定义发送的消息类型。 因此,两者各有特点,适合不同的场合。
3. 客户端API
EventSource 对象
SSE 的客户端 API 部署在EventSource对象上。下面的代码可以检测浏览器是否支持 SSE。
if ('EventSource' in window) {
// ...
}
使用 SSE 时,浏览器首先生成一个EventSource实例,向服务器发起连接。
var source = new EventSource(url);
上面的url可以与当前网址同域,也可以跨域。跨域时,可以指定第二个参数,打开withCredentials属性,表示是否一起发送 Cookie。
var source = new EventSource(url, { withCredentials: true });
版权所有
版权归属:tuyongtao1