web messaging与Woker分类:漫谈postMessage跨线程跨页面通信
Author:zhoulujun Date:
web messaging
跨文档通信(cross-document messaging):跨就是我们国内更为熟知的HTML5 window.postMessage()应用的那种通信;
通道通信(channel messaging): 伴随着server-sent事件以及web sockets, 跨文档通信和通道通信成为HTML5 通信接口“套件”中有用的一部分。
window.postMessage
window.postMessage() 方法可以安全地实现跨源通信。
iframe_contentWindow.postMessage(message, targetOrigin, [transfer]);
message发送的数据,它将会被结构化克隆算法序列化。符串,结构对象、数据对象(如:File和ArrayBuffer)或是数组都是可以的。
targetOrigin:接受方。通过窗口的origin属性来指定哪些窗口能接收到消息事件,字符串"*"(表示无限制)或者指定URI。
transfer:Transferable 对象。 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。
其他window可以监听分发的message:window.addEventListener("message", callback, false);
window.postMessage安全问题
如果您不希望从其他网站接收message,请不要为message事件添加任何事件侦听器。 这是一个完全万无一失的方式来避免安全问题。
如果您确实希望从其他网站接收message,请始终使用origin和source属性验证发件人的身份。无法检查origin和source属性会导致跨站点脚本攻击。—— 任何窗口都可以向任何其他窗口发送消息,并且您不能保证未知发件人不会发送恶意消息。 但是,验证身份后,您仍然应该始终验证接收到的消息的语法。 否则,您信任只发送受信任邮件的网站中的安全漏洞可能会在您的网站中打开跨网站脚本漏洞。
使用postMessage将数据发送到其他窗口时,始终指定精确的目标origin,而不是*。
无法检查origin和source属性会导致跨站点脚本攻击。
worker.postMessage
Worker 接口是Web Workers API 的一部分,代表一个后台任务,创建一个专用Web worker,它只执行URL指定的脚本,并且在工作线程中执行。主从线程通过 postMessage发送消息和 onmessage onmessage 接受消息
worker 将运行在与当前 window不同的另一个全局上下文中,这个上下文由一个对象表示,标准情况下为DedicatedWorkerGlobalScope (标准 workers 由单个脚本使用; 共享workers使用SharedWorkerGlobalScope)。
除了无法读取DOM对象(包括:document、window、parent)、本地文件、对话框(alert/confirm/prompt),大部分 window 对象的方法和属性是可以使用的,如: WebSockets、IndexedDB、 XMLHttpRequest 等,具体查看 Functions and classes available to workers
Woker分类
Shared Workers 一个Shared Workers可以被多个脚本使用——即使这些脚本正在被不同的window、iframe或者worker访问。,只要这些workers处于同一主域。共享worker 比专用 worker 稍微复杂一点 — 脚本必须通过活动端口进行通讯(MessageChannel的port1与port2间传递 )。详情请见
SharedWorker
。Broadcast Channel: 可以实现同 源 下浏览器不同窗口,Tab页,frame或者 iframe 下的 浏览器上下文 (通常是同一个网站下不同的页面)之间的简单通讯。
在任何页面new BroadcastChannel('mychannel')并postMessage,其他页面的BroadcastChannel实例onmessage 都能收收到消息
它与postMessage的区别就是:BroadcastChannel只能用于同源的页面之间进行通信,而postMessage却可以用于任何的页面之间的通信,换句话说,BroadcastChannel可以认为是postMessage的一个实例,它承担了postMessage的一个方面的功能。
Service Workers 一般作为web应用程序、浏览器和网络(如果可用)之间的代理服务。他们旨在(除开其他方面)创建有效的离线体验,拦截网络请求,以及根据网络是否可用采取合适的行动,更新驻留在服务器上的资源。他们还将允许访问推送通知和后台同步API。
Service worker运行在worker上下文,因此它不能访问DOM。相对于驱动应用的主JavaScript线程,它运行在其他线程中,所以不会造成阻塞。它设计为完全异步,同步API(如XHR和localStorage)不能在service worker中使用。
不同于普通Worker,Service Worker 是一个浏览器中的进程而不是浏览器内核下的线程(Service Worker是走的另外的线程,可以理解为在浏览器背后默默运行的一个线程,或者说是独立于当前页面的一段运行在浏览器后台进程里的脚本。)因此它在被注册安装之后,能够被在多个页面中使用,也不会因为页面的关闭而被销毁。
出于对安全问题的考虑,Service Worker 只能被使用在 https 或者本地的 localhost 环境下。
subworker: worker 能够生成更多的 worker。这就是所谓的subworker(还是Woker),它们必须托管在同源的父页面内。而且,subworker 解析 URI 时会相对于父 worker 的地址而不是自身页面的地址。这使得 worker 更容易记录它们之间的依赖关系。
Chrome Workers: It works exactly like a standard Worker,you can do so by using ChromeWorker instead of the standard Worker object。我的理解是 只是在chrome 跑的worker 。详情请见ChromeWorker
Woker作用
Worker作用在《浏览器层面优化前端性能(1):Chrom组件与进程/线程模型分析》里面讲过
要尽量避免JS执行时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞的感觉。Web Worker 异步优化下》
创建Worker时,JS引擎向浏览器申请开一个子线程(子线程是浏览器开的,完全受主线程控制,而且不能操作DOM)
JS引擎线程与worker线程间通过特定的方式通信(postMessage API,需要通过序列化对象来与线程交互特定的数据)
JS引擎是单线程的,这一点的本质仍然未改变,Worker可以理解是浏览器给JS引擎开的外挂,专门用来解决那些大量计算问题。
SharedWorker是浏览器所有页面共享的,不能采用与Worker同样的方式实现,因为它不隶属于某个Render进程,可以为多个Render进程共享使用。所以Chrome浏览器为SharedWorker单独创建一个进程来运行JavaScript程序,在浏览器中每个相同的JavaScript只存在一个SharedWorker进程,不管它被创建多少次。
页面A发送数据给worker:window.worker.port.postMessage('get'),然后打开页面B,调用window.worker.port.postMessage('get'),即可收到页面A发送给worker的数据。
worker 属性与方法
postMessage(data, transferList);
data:发送的数据,会被 结构化克隆 ( structured clone)
transferList:Transferable对象的数组,用于传递所有权。如果一个对象的所有权被转移,在发送它的上下文中将变为不可用(中止),并且只有在它被发送到的worker中可用。
可转移对象是如ArrayBuffer,MessagePort或ImageBitmap的实例对象。transferList数组中可默认不传,但不可传入null。一般为MessageChannel port
terminate()
立即终止 Worker 的行为. 本方法并不会等待 worker 去完成它剩余的操作;worker 将会被立刻停止
onmessage(event)
Worker 接口的onmessage属性表示一个EventHandler事件处理函数,当message 事件发生时,该函数被调用。这些事件所属MessageEvent类型,且当Worker子线程返回一条消息时被调用
event: event 对象 event.data 为 structured clone 数据
onerror() onmessageerror
onmessageerror 事件处理器接口是一个EventListener, 在 MessageEvent 类型的事件 messageerror 触发时调用 — 也就是说, 它收到的消息是不能进行序列化的 deserialized.
Woker性能优化
worker.terminate()
使用完毕,为了节省系统资源,必须关闭 Worker。
// 主线程 worker.terminate(); // Worker 线程 self.close();
worker.postMessage(arrayBuffer, [arrayBuffer])
主线程与 Worker 之间的通信内容,可以是文本,也可以是对象。需要注意的是,这种通信是拷贝关系,即是传值而不是传址,Worker 对通信内容的修改,不会影响到主线程。事实上,浏览器内部的运行机制是,先将通信内容串行化,然后把串行化后的字符串发给 Worker,后者再将它还原。这会造成性能问题!为了解决这个问题,JavaScript 允许主线程把二进制数据直接转移给子线程,但是一旦转移,主线程就无法再使用这些二进制数据了,这是为了防止出现多个线程同时修改数据的麻烦局面。这种转移数据的方法,叫做Transferable Objects。
var ab = new ArrayBuffer(1); worker.postMessage(ab, [ab]);
对于大的数据处理,不会产生性能负担。
同页面的 Web Worker
Worker 载入的是一个单独的 JavaScript 脚本文件,但是也可以载入与主线程在同一个网页的代码。减少网络加载耗时
<script id="worker" type="app/worker"> addEventListener('message', function () { postMessage('some message'); }, false); </script> <script> var blob = new Blob([document.querySelector('#worker').textContent]); var url = window.URL.createObjectURL(blob); var worker = new Worker(url); worker.onmessage = function (e) { // e.data === 'some message' }; </script>
先将嵌入网页的脚本代码(注意必须指定<script>标签的type属性是一个浏览器不认识的值),转成一个二进制对象,然后为这个二进制对象生成 URL,再让 Worker 加载这个 URL。
woker 在时间循环中执行顺序
worker 因为JavaScript 新开一个线程,执行worker代码。shareWoker因为不同tab(一个tab一个进程),因而新开一个进程。
// 主线程 let woker = new Worker('./test2.js'); woker.onmessage = (res) => { console.log(res.data); }; setTimeout(()=>{ console.log('main') setTimeout(()=>{ console.log('main2') setTimeout(()=>{ console.log('main3') },0) },0) },0) // woker 线程,test2.js setTimeout(() => { self.postMessage('woker1'); }, 0); self.postMessage('woker2');
输出顺序为,每次都不一样,以为有网络请求呀
main main2 woker2 main3 woker1
main main2 woker2 woker1 main3
main main2 main3 woker2 woker1
如果是桶页面内,顺序就机会不会变
<script id="worker" type="app/worker"> setTimeout(() => { self.postMessage('woker1'); }, 0); self.postMessage('woker2'); </script> <script> // let woker = new Worker('./test2.js'); var blob = new Blob([document.querySelector('#worker').textContent]); var url = window.URL.createObjectURL(blob); var worker = new Worker(url); worker.onmessage = (res) => { console.log(res.data); }; setTimeout(()=>{ console.log('main') setTimeout(()=>{ console.log('main2') setTimeout(()=>{ console.log('main3') },0) },0) },0) </script>
这个,还是JavaScript的 event loop 事件机制觉得,推荐阅读《弄懂javascript的执行机制:事件轮询|微任务和宏任务》
在浏览器环境中,常见的 macro task 有 setTimeout、MessageChannel、postMessage、setImmediate。而常见的 micro task 有 MutationObsever 和 Promise.then。需要留意的是:
MessageChannel
Vue中对于 macro task 的实现,优先检测是否支持原生 setImmediate,这是一个高版本 IE 和 Edge 才支持的特性,不支持的话再去检测是否支持原生的MessageChannel,如果也不支持的话就会降级为 setTimeout 0。
MessageChannel用法
MessageChannel创建了一个通信的管道,这个管道有两个端口[port1,port2],可以可以通过postMessage互相通信。
const channel = new MessageChannel(); let port1 = channel.port1; let port2 = channel.port2; port1.onmessage = function (event) { console.log("port1收到来自port2的数据:" + event.data); }; port2.onmessage = function (event) { console.log("port2收到来自port1的数据:" + event.data); }; port1.postMessage("发送给port2"); port2.postMessage("发送给port1");
MessageChannel用法很简单,但是功能却不可小觑。
MessageChannel作用
web worker间通信
多个web worker并想要在两个web worker之间实现通信的时候,MessageChannel就可以派上用场:
var w1 = new Worker("worker1.js"); var w2 = new Worker("worker2.js"); var ch = new MessageChannel(); w1.postMessage("initial port",[ch.port1]); w2.postMessage("initial port",[ch.port2]); w2.onmessage = function(e){ console.log(e.data); }
通过web worker的postMessage方法把两个MessageChannel的port传递给两个web woker,然后就可以通过每个port的postMessage方法传递数据了。
iframe兄弟间通
传统iframe父子间通信:
var iframe1 = document.getElementById('iframe1'); iframe1.postMessage(message, '*');
使用MessagePort.postMessage方法把一条消息和MessageChannel.port2传递给iframe。
var iframe1 = document.getElementById('iframe1'); var iframe2 = document.getElementById('iframe2'); iframe1.contentWindow.postMessage('main','*',[port1]); iframe2.contentWindow.postMessage('main','*',[port2]);
代码地址:channel messaging basic demo
worker_threads兄弟线程通信
nodejs的MessageChannel虽然与浏览器的,实现方式不同,但是用法相同,都是一个模型。在此列出,阐释这种思想。
const {isMainThread, parentPort, threadId, MessageChannel, Worker} = require('worker_threads');
通信事件message事件对象
Message事件的定义可参见这里
data | 包含任意字符串数据,由原始脚本发送 |
---|---|
origin | 一个字符串,包含原始文档的方案、域名以及端口(如:http://domain.example:80) |
lastEventId | 一个字符串,包含了当前的消息事件的唯一标识符。 |
source | 原始文件的窗口的引用。更确切地说,它是一个WindowProxy对象。 |
ports | 一个数组,包含任何MessagePort对象发送消息。 |
在跨文档通信和通道通信中,lastEventId的值一般是个空字符串;lastEventId应用在服务器端发送事件上。发送信息中如果没有ports, 则ports属性值就是个长度为0的数组。
MessageEvent继承DOM事件接口,且属性共享。然而,通信事件并没有冒泡,不能取消,也没有默认行为。
Service Worker
前端缓存分析
前端缓存 大致可以分为 http缓存 与 浏览器缓存
http缓存推荐阅读《浏览器http缓存机制剖析:存储策略与过期策略的机理分析》,我们来分析下 浏览器缓存
storage
cookie、localStorage、sessionStorage
cookie 最大约为4k,每个域名最多50kcookie——不同浏览器限制不一样,一般用来存储关键数据(比如用户登录信息)
localStorage/sessionStorage通常有5MB的存储空间,比如微信文章 不需要改动的资源(如css/js)就基本存储在localStorage里面
推荐阅读《登录状态控制:cookies对比sessionStorage保持信息的分析》
前端数据库:
WebSql和IndexDB,其中WebSql被规范废弃,他们都有大约50MB的最大容量,一般 当页面 store 的数据可以直接存储在里面。
manifest 缓存
已经被废弃,因为他的设计有些不合理的地方,他在缓存静态文件的同时,也会默认缓存html文件。这导致页面的更新只能通过manifest文件中的版本号来决定。所以,应用缓存只适合那种常年不变化的静态网站。如此的不方便,也是被废弃的重要原因。
推荐阅读《html5离线缓存manifest详解》、《HTML5离线存储实战之manifest的那些坑》
Service Worker
Service Worker本质上也是浏览器缓存资源用的,只不过他不仅仅是cache,也是通过worker的方式来进一步优化。
他基于h5的web worker,所以绝对不会阻碍当前js线程的执行,sw最重要的工作原理就是
后台线程:独立于当前网页线程;
网络代理:在网页发起请求时代理,来缓存文件;
这里不再赘述,再开一篇《ServiceWorker工作机制与生命周期:资源缓存与协作通信处理》
参考文章:
MessageChannel是什么,怎么使用? https://www.jianshu.com/p/4f07ef18b5d7
HTML5 postMessage iframe跨域web通信简介 https://www.zhangxinxu.com/wordpress/2012/02/html5-web-messaging-cross-document-messaging-channel-messaging/
Web Worker 使用教程 www.ruanyifeng.com/blog/2018/07/web-worker.html
转载本站文章《web messaging与Woker分类:漫谈postMessage跨线程跨页面通信》,
请注明出处:https://www.zhoulujun.cn/html/webfront/SGML/html5/2020_0615_8465.html