From b8269cfed8000b4250234c3eec9c1936a59e0e5a Mon Sep 17 00:00:00 2001 From: xiejie <745007854@qq.com> Date: Fri, 10 Dec 2021 15:50:32 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=B5=8F=E8=A7=88=E5=99=A8?= =?UTF-8?q?=E7=AC=94=E9=9D=A2=E8=AF=95=E9=A2=98=E9=83=A8=E5=88=86=E6=95=99?= =?UTF-8?q?=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{1. 浏览器渲染流程.md => 浏览器渲染流程.md} | 8 +- 02. 资源提示关键词/资源提示关键词.md | 235 ++++ 03. 浏览器的组成部分/浏览器的组成部分.md | 217 +++ 04. 浏览器的离线存储/File API.md | 399 ++++++ .../File API课堂代码/.vscode/settings.json | 3 + .../File API课堂代码/index.html | 72 + .../File API课堂代码/test.txt | 1 + 04. 浏览器的离线存储/IndexedDB.md | 884 ++++++++++++ .../IndexedDB课堂代码/.vscode/settings.json | 3 + 04. 浏览器的离线存储/IndexedDB课堂代码/db.js | 330 +++++ .../IndexedDB课堂代码/index.html | 63 + 04. 浏览器的离线存储/WebSQL.md | 223 +++ 04. 浏览器的离线存储/浏览器离线存储概述.md | 53 + 05. 浏览器的缓存/浏览器缓存.md | 483 +++++++ .../浏览器缓存Demo/.vscode/settings.json | 3 + 05. 浏览器的缓存/浏览器缓存Demo/server.js | 37 + .../浏览器缓存Demo/static/css/index.css | 25 + .../浏览器缓存Demo/static/html/index.html | 29 + .../浏览器缓存Demo/static/img/ok.png | Bin 0 -> 162319 bytes .../浏览器缓存Demo/static/js/index.js | 5 + .../BroadCast实现跨标签页通信/.vscode/settings.json | 3 + .../BroadCast实现跨标签页通信/index.html | 26 + .../BroadCast实现跨标签页通信/index2.html | 22 + .../IndexedDB实现跨标签页通信/.vscode/settings.json | 3 + .../IndexedDB实现跨标签页通信/db.js | 87 ++ .../IndexedDB实现跨标签页通信/index.html | 46 + .../IndexedDB实现跨标签页通信/index2.html | 80 ++ .../.vscode/settings.json | 3 + .../ServiceWorker实现跨标签页通信/index.html | 31 + .../ServiceWorker实现跨标签页通信/index2.html | 21 + .../ServiceWorker实现跨标签页通信/sw.js | 8 + .../.vscode/settings.json | 3 + .../SharedWorker实现跨标签页通信/index.html | 27 + .../SharedWorker实现跨标签页通信/index2.html | 26 + .../SharedWorker实现跨标签页通信/worker.js | 15 + .../Websocket实现跨标签页通信/.vscode/settings.json | 3 + .../Websocket实现跨标签页通信/index.html | 29 + .../Websocket实现跨标签页通信/index2.html | 32 + .../node_modules/.package-lock.json | 28 + .../node_modules/ws/LICENSE | 19 + .../node_modules/ws/README.md | 493 +++++++ .../node_modules/ws/browser.js | 8 + .../node_modules/ws/index.js | 13 + .../node_modules/ws/lib/buffer-util.js | 126 ++ .../node_modules/ws/lib/constants.js | 12 + .../node_modules/ws/lib/event-target.js | 266 ++++ .../node_modules/ws/lib/extension.js | 203 +++ .../node_modules/ws/lib/limiter.js | 55 + .../node_modules/ws/lib/permessage-deflate.js | 511 +++++++ .../node_modules/ws/lib/receiver.js | 612 +++++++++ .../node_modules/ws/lib/sender.js | 422 ++++++ .../node_modules/ws/lib/stream.js | 159 +++ .../node_modules/ws/lib/subprotocol.js | 62 + .../node_modules/ws/lib/validation.js | 124 ++ .../node_modules/ws/lib/websocket-server.js | 485 +++++++ .../node_modules/ws/lib/websocket.js | 1220 +++++++++++++++++ .../node_modules/ws/package.json | 61 + .../node_modules/ws/wrapper.mjs | 8 + .../Websocket实现跨标签页通信/package-lock.json | 44 + .../Websocket实现跨标签页通信/package.json | 15 + .../Websocket实现跨标签页通信/server.js | 38 + .../cookie实现跨标签页通信/.vscode/settings.json | 3 + .../cookie实现跨标签页通信/index.html | 19 + .../cookie实现跨标签页通信/index2.html | 24 + .../.vscode/settings.json | 3 + .../postMessage实现跨标签页通信/index.html | 34 + .../postMessage实现跨标签页通信/index2.html | 20 + .../storage实现跨标签页通信/.vscode/settings.json | 3 + .../storage实现跨标签页通信/index.html | 19 + .../storage实现跨标签页通信/index2.html | 23 + 06. 跨标签页通信/跨标签页通信.md | 662 +++++++++ 07. Web Worker/web worker.md | 256 ++++ .../web worker课堂代码/.vscode/settings.json | 3 + 07. Web Worker/web worker课堂代码/index.html | 32 + 07. Web Worker/web worker课堂代码/worker.js | 5 + 浏览器.dio | 15 +- 浏览器笔面试题目.txt | 8 - 77 files changed, 9630 insertions(+), 21 deletions(-) rename 01. 浏览器的渲染流程/{1. 浏览器渲染流程.md => 浏览器渲染流程.md} (95%) create mode 100644 02. 资源提示关键词/资源提示关键词.md create mode 100644 03. 浏览器的组成部分/浏览器的组成部分.md create mode 100644 04. 浏览器的离线存储/File API.md create mode 100644 04. 浏览器的离线存储/File API课堂代码/.vscode/settings.json create mode 100644 04. 浏览器的离线存储/File API课堂代码/index.html create mode 100644 04. 浏览器的离线存储/File API课堂代码/test.txt create mode 100644 04. 浏览器的离线存储/IndexedDB.md create mode 100644 04. 浏览器的离线存储/IndexedDB课堂代码/.vscode/settings.json create mode 100644 04. 浏览器的离线存储/IndexedDB课堂代码/db.js create mode 100644 04. 浏览器的离线存储/IndexedDB课堂代码/index.html create mode 100644 04. 浏览器的离线存储/WebSQL.md create mode 100644 04. 浏览器的离线存储/浏览器离线存储概述.md create mode 100644 05. 浏览器的缓存/浏览器缓存.md create mode 100644 05. 浏览器的缓存/浏览器缓存Demo/.vscode/settings.json create mode 100644 05. 浏览器的缓存/浏览器缓存Demo/server.js create mode 100644 05. 浏览器的缓存/浏览器缓存Demo/static/css/index.css create mode 100644 05. 浏览器的缓存/浏览器缓存Demo/static/html/index.html create mode 100644 05. 浏览器的缓存/浏览器缓存Demo/static/img/ok.png create mode 100644 05. 浏览器的缓存/浏览器缓存Demo/static/js/index.js create mode 100644 06. 跨标签页通信/BroadCast实现跨标签页通信/.vscode/settings.json create mode 100644 06. 跨标签页通信/BroadCast实现跨标签页通信/index.html create mode 100644 06. 跨标签页通信/BroadCast实现跨标签页通信/index2.html create mode 100644 06. 跨标签页通信/IndexedDB实现跨标签页通信/.vscode/settings.json create mode 100644 06. 跨标签页通信/IndexedDB实现跨标签页通信/db.js create mode 100644 06. 跨标签页通信/IndexedDB实现跨标签页通信/index.html create mode 100644 06. 跨标签页通信/IndexedDB实现跨标签页通信/index2.html create mode 100644 06. 跨标签页通信/ServiceWorker实现跨标签页通信/.vscode/settings.json create mode 100644 06. 跨标签页通信/ServiceWorker实现跨标签页通信/index.html create mode 100644 06. 跨标签页通信/ServiceWorker实现跨标签页通信/index2.html create mode 100644 06. 跨标签页通信/ServiceWorker实现跨标签页通信/sw.js create mode 100644 06. 跨标签页通信/SharedWorker实现跨标签页通信/.vscode/settings.json create mode 100644 06. 跨标签页通信/SharedWorker实现跨标签页通信/index.html create mode 100644 06. 跨标签页通信/SharedWorker实现跨标签页通信/index2.html create mode 100644 06. 跨标签页通信/SharedWorker实现跨标签页通信/worker.js create mode 100644 06. 跨标签页通信/Websocket实现跨标签页通信/.vscode/settings.json create mode 100644 06. 跨标签页通信/Websocket实现跨标签页通信/index.html create mode 100644 06. 跨标签页通信/Websocket实现跨标签页通信/index2.html create mode 100644 06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/.package-lock.json create mode 100644 06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/LICENSE create mode 100644 06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/README.md create mode 100644 06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/browser.js create mode 100644 06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/index.js create mode 100644 06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/lib/buffer-util.js create mode 100644 06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/lib/constants.js create mode 100644 06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/lib/event-target.js create mode 100644 06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/lib/extension.js create mode 100644 06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/lib/limiter.js create mode 100644 06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/lib/permessage-deflate.js create mode 100644 06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/lib/receiver.js create mode 100644 06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/lib/sender.js create mode 100644 06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/lib/stream.js create mode 100644 06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/lib/subprotocol.js create mode 100644 06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/lib/validation.js create mode 100644 06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/lib/websocket-server.js create mode 100644 06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/lib/websocket.js create mode 100644 06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/package.json create mode 100644 06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/wrapper.mjs create mode 100644 06. 跨标签页通信/Websocket实现跨标签页通信/package-lock.json create mode 100644 06. 跨标签页通信/Websocket实现跨标签页通信/package.json create mode 100644 06. 跨标签页通信/Websocket实现跨标签页通信/server.js create mode 100644 06. 跨标签页通信/cookie实现跨标签页通信/.vscode/settings.json create mode 100644 06. 跨标签页通信/cookie实现跨标签页通信/index.html create mode 100644 06. 跨标签页通信/cookie实现跨标签页通信/index2.html create mode 100644 06. 跨标签页通信/postMessage实现跨标签页通信/.vscode/settings.json create mode 100644 06. 跨标签页通信/postMessage实现跨标签页通信/index.html create mode 100644 06. 跨标签页通信/postMessage实现跨标签页通信/index2.html create mode 100644 06. 跨标签页通信/storage实现跨标签页通信/.vscode/settings.json create mode 100644 06. 跨标签页通信/storage实现跨标签页通信/index.html create mode 100644 06. 跨标签页通信/storage实现跨标签页通信/index2.html create mode 100644 06. 跨标签页通信/跨标签页通信.md create mode 100644 07. Web Worker/web worker.md create mode 100644 07. Web Worker/web worker课堂代码/.vscode/settings.json create mode 100644 07. Web Worker/web worker课堂代码/index.html create mode 100644 07. Web Worker/web worker课堂代码/worker.js delete mode 100644 浏览器笔面试题目.txt diff --git a/01. 浏览器的渲染流程/1. 浏览器渲染流程.md b/01. 浏览器的渲染流程/浏览器渲染流程.md similarity index 95% rename from 01. 浏览器的渲染流程/1. 浏览器渲染流程.md rename to 01. 浏览器的渲染流程/浏览器渲染流程.md index 16bf454..3641326 100644 --- a/01. 浏览器的渲染流程/1. 浏览器渲染流程.md +++ b/01. 浏览器的渲染流程/浏览器渲染流程.md @@ -104,7 +104,7 @@ div>a>span { 在这一过程中,不是简单的将两者合并就行了。渲染树只会包括需要显示的节点和这些节点的样式信息,如果某个节点是 *display: none* 的,那么就不会在渲染树中显示。 -当浏览器生成渲染树以后,就会根据渲染树来进行布局(也可以叫做回流),然后调用 *GPU* 绘制,合成图层,显示在屏幕上。对于这一部分的内容因为过于底层,还涉及到了硬件相关知识,这里就不再继续展开内容了。 +当浏览器生成渲染树以后,就会根据渲染树来进行布局(也可以叫做回流),之后确定每一个像素点的信息(重绘),然后调用 *GPU* 绘制,合成图层,显示在屏幕上。对于这一部分的内容因为过于底层,还涉及到了硬件相关知识,这里就不再继续展开内容了。 ## 阻塞渲染 @@ -132,7 +132,7 @@ div>a>span { - 重绘:当节点需要更改外观而不会影响布局的,比如改变 *color* 就叫称为重绘 - 回流:布局或者几何属性需要改变就称为回流。 -回流必定会发生重绘,重绘不一定会引发回流。因此回流所需的成本比重绘高得多,改变父节点里的子节点很可能会导致父节点的一系列回流。 +**回流必定会发生重绘,重绘不一定会引发回流。**因此回流所需的成本比重绘高得多,改变父节点里的子节点很可能会导致父节点的一系列回流。 当页面布局和几何信息发生变化的时候,就需要回流。比如以下情况: @@ -150,7 +150,7 @@ div>a>span { ### 现代浏览器的优化机制 -现代的浏览器都是很聪明的,由于每次重排都会造成额外的计算消耗,因此大多数浏览器都会通过队列化修改并批量执行来优化重排过程。 +现代的浏览器都是很聪明的,由于每次重排(回流)都会造成额外的计算消耗,因此大多数浏览器都会通过队列化修改并批量执行来优化重排过程。 浏览器会将修改操作放入到队列里,直到过了一段时间或者操作达到了一个阈值,才清空队列。 @@ -174,7 +174,7 @@ div>a>span { #### 1. 最小化重绘和回流 -由于重绘和重排可能代价比较昂贵,因此最好就是可以减少它的发生次数。为了减少发生次数,我们可以合并多次对DOM和样式的修改,然后一次处理掉。考虑这个例子: +由于重绘和重排可能代价比较昂贵,因此最好就是可以减少它的发生次数。为了减少发生次数,我们可以合并多次对 *DOM* 和样式的修改,然后一次处理掉。考虑这个例子: ```js const el = document.getElementById('test'); diff --git a/02. 资源提示关键词/资源提示关键词.md b/02. 资源提示关键词/资源提示关键词.md new file mode 100644 index 0000000..b69396b --- /dev/null +++ b/02. 资源提示关键词/资源提示关键词.md @@ -0,0 +1,235 @@ +# 资源提示关键词 + + + +在上一篇文章中,我们介绍了浏览器的渲染流程,这篇文章中,我们将重点聚焦在渲染阻塞上,来详细看一下渲染阻塞以及一些常见的解决方法。 + +本文主要包含以下内容: + +- 渲染阻塞回顾 +- *defer* 和 *async* +- *preload* +- *prefetch* +- *prerender* +- *preconnect* + + + +## 渲染阻塞回顾 + +我们都知道,*HTML* 用于描述网页的整体结构。为了理解 *HTML*,浏览器必须将它转为自己能够理解的格式,也就是 *DOM*(文档对象模型) + +浏览器引擎有一段特殊的代码,称为解析器,用于将数据从一种格式转换为另一种格式。 + +image-20211206161457653 + +浏览器一点一点地构建 *DOM*。一旦第一块代码进来,它就会开始解析 *HTML*,将节点添加到树结构中。 + +![ezgif-2-2688553063](https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2021-12-06-081522.gif) + +构建出来的 *DOM* 对象,实际上有 *2* 个作用: + +- *HTML* 文档的结构以对象的方式体现出来,形成我们常说的 *DOM* 树 + +- 作为外界的接口供外界使用,例如 *JavaScript*。当我们调用诸如 *document.getElementById* 的方法时,返回的元素是一个 *DOM* 节点。每个 *DOM* 节点都有许多可以用来访问和更改它的函数,用户看到的内容也会相应地发生变化。 + + + +![ezgif-2-01a1ded8c4](https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2021-12-06-081639.gif) + +*CSS* 样式会被映射为 *CSSOM*( *CSS* 对象模型),它和 *DOM* 很相似,但是针对的是 *CSS* 而不是 *HTML*。 + +在构建 *CSSOM* 的时候,无法进行增量构建(不像构建 *DOM* 一样,解析到一个 *DOM* 节点就扔到 *DOM* 树结构里面),因为 *CSS* 规则是可以相互覆盖的,浏览器引擎需要经过复杂的计算才能弄清楚 *CSS* 代码如何应用于 *DOM*。 + +![image-20211206161700033](https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2021-12-06-081700.png) + +当浏览器正在构建 *DOM* 时,如果它遇到 *HTML* 中的 `` 标记,它必须立即执行它。如果脚本是外部的,则必须先下载脚本。 + +过去,为了执行脚本,必须暂停解析。解析会在 *JavaScript* 引擎执行完脚本中的代码后再次启动。 + +image-20211206161717368 + + + +为什么解析必须停止呢? + +原因很简单,这是因为 *Javascript* 脚本可以改变 *HTML* 以及根据 *HTML* 生成的 *DOM* 树结构。例如,脚本可以通过使用 *document.createElement( )* 来添加节点从而更改 *DOM* 结构。 + +![image](https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2021-12-06-081740.gif) + +这也是为什么我们建议将 *script* 标签写在 *body* 元素结束标签前面的原因。 + +```html + + + +``` + +*defer* 表示加载后续文档元素的过程将和 *script.js* 的加载并行进行(异步),但是 *script.js* 的执行要在所有元素解析完成之后,*DOMContentLoaded* 事件触发之前完成。也就是说,下载 *JS* 文件的时候不会阻塞 *DOM* 树的构建,然后等待 *DOM* 树构建完毕后再执行此 *JS* 文件。 + +```html + +``` + +具体加载瀑布图如下图所示: + +image-20211208112125053 + + + +## *preload* + +*preload* 顾名思义就是一种预加载的方式,它通过声明向浏览器声明一个需要提前加载的资源,当资源真正被使用的时候立即执行,就无需等待网络的消耗。 + +```html + + + + + +``` + +在上面的代码中,会先加载 *style1.css* 和 *main1.js* 文件(但不会生效),在随后的页面渲染中,一旦需要使用它们,它们就会立即可用。 + +可以使用 *as* 来指定将要预加载的内容类型。 + +![image-20211208112151152](https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2021-12-08-032151.png) + + + +*preload* 指令的一些优点如下: + +- 允许浏览器设置资源优先级,从而允许 *Web* 开发人员优化某些资源的交付。 + +- 使浏览器能够确定资源类型,因此它可以判断将来是否可以重用相同的资源。 + +- 浏览器可以通过引用 *as* 属性中定义的内容来确定请求是否符合内容安全策略。 + +- 浏览器可以根据资源类型发送合适的 *Accept* 头(例如:*image/webp* ) + + + +## *prefetch* + +*prefetch* 是一种利用浏览器的空闲时间加载页面将来可能用到的资源的一种机制,通常可以用于加载非首页的其他页面所需要的资源,以便加快后续页面的首屏速度。 + +*prefetch* 加载的资源可以获取非当前页面所需要的资源,并且将其放入缓存至少 *5* 分钟(无论资源是否可以缓存)。并且,当页面跳转时,未完成的 *prefetch* 请求不会被中断; + +它的用法跟 *preload* 是一样的: + +```html + +``` + + + +***DNS prefetching*** + +*DNS prefetching* 允许浏览器在用户浏览时在后台对页面执行 *DNS* 查找。这最大限度地减少了延迟,因为一旦用户单击链接就已经进行了 *DNS* 查找。 + +通过将 *rel="dns-prefetch"* 标记添加到链接属性,可以将 *DNS prefetching* 添加到特定 *URL*。建议在诸如 *Web* 字体、*CDN* 之类的东西上使用它。 + +```html + + + + +``` + + + +## *prerender* + +*prerender* 与 *prefetch* 非常相似,*prerender* 同样也是会收集用户接下来可能会用到的资源。 + +不同之处在于 *prerender* 实际上是在后台渲染整个页面。 + +```html + +``` + + + +## *preconnect* + +我们要讨论的最后一个资源提示是 *preconnect*。 + +*preconnect* 指令允许浏览器在 *HTTP* 请求实际发送到服务器之前设置早期连接。 + +我们知道,浏览器要建立一个连接,一般需要经过 *DNS* 查找,*TCP* 三次握手和 *TLS* 协商(如果是 *https* 的话),这些过程都是需要相当的耗时的。所以 *preconnet*,就是一项使浏览器能够预先建立一个连接,等真正需要加载资源的时候就能够直接请求了。 + +![image-20211208112216614](https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2021-12-08-032217.png) + + + +以下是为 *CDN URL* 启用 *preconnect* 的示例。 + +```html + +``` + +在上面的代码中,浏览器会进行以下步骤: + +- 解释 *href* 的属性值,判断是否是合法的 *URL*。如果是合法的 *URL*,然后继续判断 *URL* 的协议是否是 *http* 或者 *https*,如果不是合法的 *URL*,则结束处理。 +- 如果当前页面 *host* 不同于 *href* 属性中的 *host*,那么将不会带上 *cookie*,如果希望带上 *cookie* 等信息,可以加上 *crossorign* 属性。 + + + +------- + + + +-*EOF*- diff --git a/03. 浏览器的组成部分/浏览器的组成部分.md b/03. 浏览器的组成部分/浏览器的组成部分.md new file mode 100644 index 0000000..1e9003b --- /dev/null +++ b/03. 浏览器的组成部分/浏览器的组成部分.md @@ -0,0 +1,217 @@ +# 浏览器的组成部分 + + + +什么是浏览器? + +*Web* 浏览器简称为浏览器,是一种用于访问互联网上信息的应用软件。浏览器的主要功能是从服务器检索 *Web* 资源并将其显示在 *Web* 浏览器窗口中。 + +*Web* 资源通常是 *HTML* 文档,但也可能是 *PDF*、图像、音频、视频或其他类型的内容。资源的位置是通过使用 *URI*(统一资源标识符)指定的。 + +浏览器包含结构良好的组件,这些组件执行一系列任务让浏览器窗口能显示 *Web* 资源。 + +本文我们就来聊一聊关于浏览器的组成部分。 + +下图是关于浏览器的架构图: + +![image-20211126131413497](https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2021-11-26-051413.png) + + + +一个 *Web* 浏览器中,主要组件有: + +- 用户界面(*user interface*) + +- 浏览器引擎(*browser engine*) + +- 渲染引擎(*rendering engine*) + +- 网络(*networking*) + +- *JS* 解释器(*JavaScript interpreter*) + +- 用户界面后端(*UI backend*) + +- 数据存储(*data storage*) + +下面我们来具体看一下每一个部分的作用。 + + + +## 用户界面(*user interface*) + +用户界面用于呈现浏览器窗口部件,比如地址栏、前进后退按钮、书签、顶部菜单等。 + +如下图所示: + +![image-20211126131451095](https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2021-11-26-051451.png) + +## 浏览器引擎(*browser engine*) + +它是 *UI* 和渲染引擎之间的桥梁。接收来自 *UI* 的输入,然后通过操纵渲染引擎将网页或者其他资源显示在浏览器中。 + + + +## 渲染引擎(*rendering engine*) + +负责在浏览器窗口上显示请求的内容。例如,用户请求一个 *HTML* 页面,则它负责解析 *HTML* 文档和 *CSS*,并将解析和格式化的内容显示在屏幕上。我们平时说的浏览器内核就是指这部分。 + +现代网络浏览器的渲染引擎: + +- *Firefox:Gecko Software* + +- *Safari:WebKit* + +- *Chrome、Opera* (*15* 版本之后):*Blink* + +- *Internet Explorer:Trident* + +在第一小节我们已经介绍过渲染引擎渲染页面的整体流程了,这里做一个简单的复习。 + +为了在屏幕上绘制像素(第一次渲染),浏览器在从网络接收数据(*HTML、CSS、JavaScript*)后必须经过一系列称为关键渲染路径的过程/技术。这包括 *DOM*、*CSSOM*、渲染树、布局和绘画。 + +### 从数据到 *DOM* + +来自网络层的请求内容以二进制流格式在渲染引擎中接收(通常为 *8kb* 块)。然后将原始字节转换为 *HTML* 文件的字符(基于字符编码)。 + +然后将字符转换为标记。词法分析器执行词法分析,将输入分解为标记。在标记化期间,文件中的每个开始和结束标记都被考虑在内。它知道如何去除不相关的字符,如空格和换行符。然后解析器进行语法分析,通过分析文档结构,应用语言语法规则来构建解析树。 + +解析过程是迭代的。它将向词法分析器询问新的标记,如果语言语法规则匹配,则该标记将被添加到解析树中。然后解析器将要求另一个令牌。如果没有规则匹配,解析器将在内部存储令牌并不断询问令牌,直到找到与所有内部存储的令牌匹配的规则。如果未找到规则,则解析器将引发异常。这意味着该文档无效并且包含语法错误。 + +这些节点在称为 *DOM*(文档对象模型)的树数据结构中链接,该结构建立了父子关系、相邻兄弟关系。 + + + +![image-20211126131513877](https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2021-11-26-051513.png) + + + +### *CSS* 数据到 *CSSOM* + +*CSS* 数据的原始字节被转换成字符、标记、节点,最后在 *CSSOM*(*CSS* 对象模型)中。 因为 *CSS* 存在层叠机制,该机制决定了将什么样式应用于元素,也就是说,元素的样式数据可以来自父项(通过继承)或设置为元素本身。因此浏览器必须递归遍历 *CSS* 树结构并确定特定元素的样式。 + + + +![image-20211126131534063](https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2021-11-26-051535.png) + +### *DOM* 和 *CSSOM* 渲染树 + +*DOM* 树包含有关 *HTML* 元素关系的信息,而 *CSSOM* 树包含有关如何设置这些元素样式的信息。 + +渲染引擎会将样式信息和 *HTML* 元素关系信息进行汇总,用于创建另一棵树,称为“渲染树”。 + +渲染树包含具有视觉属性(如颜色和尺寸)的矩形。矩形按正确的顺序显示在屏幕上。 + + + +![image-20211126131552411](https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2021-11-26-051553.png) + + + +### 布局 + +在构建渲染树之后,它会经历一个“布局”过程。布局过程的输出是一个“盒子模型”,它精确地捕获视口内每个元素的确切位置和大小:所有相对测量值都转换为屏幕上的绝对像素。 + +在下面的屏幕截图中,您可以看到为 *body* 元素计算的“框模型”(边距、边框、填充、宽度和高度)信息。 + +![image-20211126131610965](https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2021-11-26-051611.png) + +### 绘制 + +在这一阶段渲染树会被遍历,并且会只用 *UI* 后端层绘制每个节点。这个过程也被称为“光栅化”。在这个阶段,渲染树中每个节点的计算布局信息被转换为屏幕上的实际像素。 + +绘画是一个渐进的过程,其中一些部分被解析和渲染,而该过程继续处理来自网络的项目的其余部分。 + +![image-20211126131631548](https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2021-11-26-051631.png) + + + +### 整体流程图 + +渲染整体流程如下图所示: + +![image-20211126131710384](https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2021-11-26-051710.png) + + + +## 网络(*networking*) + +该模块处理浏览器内的各种网络通信。它使用一组通信协议,如 *HTTP、HTTPs、FTP*,同时通过 *URL* 获取请求的资源。 + + + +## *JS* 解释器(*JavaScript interpreter*) + +*JavaScript* 是一种脚本语言,允许我们动态更新 *Web* 内容、控制由浏览器的 *JS* 引擎执行的多媒体和动画图像。 + +*DOM* 和 *CSSOM* 为 *JS* 提供了一个接口,可以改变 *DOM* 和 *CSSOM*。由于浏览器不确定特定的 *JS* 会做什么,它会在遇到 *script* 标签后立即暂停 *DOM* 树的构建。 + +每个脚本都是一个解析拦截器,会让 *DOM* 树的构建停止。 + +*JS* 引擎在从服务器获取并输入 *JS* 解析器后立即开始解析代码。它将它们转换为机器理解的代表性对象。在抽象句法结构的树表示中存储所有解析器信息的对象称为对象语法树(AST)。这些对象被送入一个解释器,该解释器将这些对象翻译成字节码。 + +这些是即时 (*JIT*) 编译器,这意味着从服务器下载的 *JavaScript* 文件在客户端的计算机上实时编译。解释器和编译器是结合在一起的。解释器几乎立即执行源代码;编译器生成客户端系统直接执行的机器代码。 + +不同的浏览器使用不同的 *JS* 引擎: + +- *Chrome*: *V8* (*JavaScript* 引擎) (*Node JS* 建立在此之上) + +- *Mozilla*: *SpiderMonkey* (旧称“松鼠鱼”) + +- *Microsoft Edge*:*Chakra* + +- *Safari*:*JavaScriptCore / Nitro WebKit* + + + +## 用户界面后端(*UI backend*) + +用于绘制基本的窗口小部件,比如下拉列表、文本框、按钮等,向上提供公开的接口,向下调用操作系统的用户界面。 + + + +## 数据存储(*data storage*) + +这是一个持久层。浏览器可能需要在本地保存各种数据,例如 *cookie*。浏览器还支持 *localStorage、IndexedDB、WebSQL* 和 *FileSystem* 等存储机制。 + +我们将在下一篇文章讨论浏览器的离线存储。 + + + +## 总结 + +最后,我们对浏览器的组成部分进行一个总结。 + +浏览器由以下几个部分组成: + +1. 用户界面(*user interface*) + + 用于呈现浏览器窗口部件,比如地址栏、前进后退按钮、书签、顶部菜单等 + +2. 浏览器引擎(*browser engine*) + + 用户在用户界面和渲染引擎中传递指令 + +3. 渲染引擎(*rendering engine*) + + 负责解析 *HTML*、*CSS*,并将解析的内容显示到屏幕上。我们平时说的浏览器内核就是指这部分。 + +4. 网络(*networking*) + + 用户网络调用,比如发送 *http* 请求 + +5. 用户界面后端(*UI backend*) + + 用于绘制基本的窗口小部件,比如下拉列表、文本框、按钮等,向上提供公开的接口,向下调用操作系统的用户界面。 + +6. *JS* 解释器(*JavaScript interpreter*) + + 解释执行 *JS* 代码。我们平时说的 *JS* 引擎就是指这部分。 + +7. 数据存储(*data storage*) + + 用户保存数据到磁盘中。比如 *cookie、localstorage* 等都是使用的这部分功能。 + +------ + +-*EOF*- \ No newline at end of file diff --git a/04. 浏览器的离线存储/File API.md b/04. 浏览器的离线存储/File API.md new file mode 100644 index 0000000..a0162a2 --- /dev/null +++ b/04. 浏览器的离线存储/File API.md @@ -0,0 +1,399 @@ +# *File API* + + + +本文主要包含以下内容: + +- *File API* 介绍 +- *File* 对象 + - 构造函数 + - 实例属性和实例方法 +- *FileList* 对象 +- *FileReader* 对象 +- 综合实例 + + + +## *File API* 介绍 + +我们知道,*HTML* 的 *input* 表单控件,其 *type* 属性可以设置为 *file*,表示这是一个上传控件。 + +```html + +``` + +选择文件前: + +image-20211202102038796 + +选择文件后: + +image-20211202102056757 + +这种做法用户体验非常的差,我们无法**在客户端**对用户选取的文件进行 *validate*,无法读取文件大小,无法判断文件类型,无法预览。 + +如果是多文件上传,*JavaScript* 更是回天乏力。 + +```html + +``` + + + +image-20211202102115626 + +但现在有了 *HTML5* 提供的 *File API*,一切都不同了。该接口允许 *JavaScript* 读取本地文件,但并不能直接访问本地文件,而是要依赖于用户行为,比如用户在 *type='file'* 控件上选择了某个文件或者用户将文件拖拽到浏览器上。 + +*File Api* 提供了以下几个接口来访问本地文件系统: + +- *File*:单个文件,提供了诸如 *name、file size、mimetype* 等只读文件属性 + +- *FileList*:一个类数组 *File* 对象集合 + +- *FileReader*:异步读取文件的接口 + +- *Blob*:文件对象的二进制原始数据 + + + +## *File* 对象 + +*File* 对象代表一个文件,用来读写文件信息。它继承了 *Blob* 对象,或者说是一种特殊的 *Blob* 对象,所有可以使用 *Blob* 对象的场合都可以使用它。 + +最常见的使用场合是表单的文件上传控件(\<*input type="file"*>),用户选中文件以后,浏览器就会生成一个数组,里面是每一个用户选中的文件,它们都是 *File* 实例对象。 + +```html + +``` + +```js +// 获取 DOM 元素 +var file = document.getElementById('file'); +file.onchange = function(event){ + var files = event.target.files; + console.log(files); + console.log(files[0] instanceof File); +} +``` + +上面代码中,*files[0]* 是用户选中的第一个文件,它是 *File* 的实例。 + +image-20211202102135071 + + + +### 构造函数 + +浏览器原生提供一个 *File( )* 构造函数,用来生成 *File* 实例对象。 + +```js +new File(array, name [, options]) +``` + +*File( )* 构造函数接受三个参数。 + +- *array*:一个数组,成员可以是二进制对象或字符串,表示文件的内容。 + +- *name*:字符串,表示文件名或文件路径。 + +- *options*:配置对象,设置实例的属性。该参数可选。 + +第三个参数配置对象,可以设置两个属性。 + +- *type*:字符串,表示实例对象的 *MIME* 类型,默认值为空字符串。 + +- *lastModified*:时间戳,表示上次修改的时间,默认为 *Date.now( )*。 + +下面是一个例子。 + +```js +var file = new File( + ['foo'], + 'foo.txt', + { + type: 'text/plain', + } +); +``` + +### 实例属性和实例方法 + +*File* 对象有以下实例属性。 + +- *File.lastModified*:最后修改时间 + +- *File.name*:文件名或文件路径 + +- *File.size*:文件大小(单位字节) + +- *File.type*:文件的 *MIME* 类型 + +```js +var file = new File( + ['foo'], + 'foo.txt', + { + type: 'text/plain', + } +); +console.log(file.lastModified); // 1638340865992 +console.log(file.name); // foo.txt +console.log(file.size); // 3 +console.log(file.type); // text/plain +``` + +在上面的代码中,我们创建了一个 *File* 文件对象实例,并打印出了该文件对象的诸如 *lastModified、name、size、type* 等属性信息。 + +*File* 对象没有自己的实例方法,由于继承了 *Blob* 对象,因此可以使用 *Blob* 的实例方法 *slice( )*。 + + + +## *FileList* 对象 + +*FileList* 对象是一个类似数组的对象,代表一组选中的文件,每个成员都是一个 *File* 实例。 + +在最上面的那个示例中,我们就可以看到触发 *change* 事件后,*event.target.files* 拿到的就是一个 *FileList* 实例对象。 + +它主要出现在两个场合。 + +- 文件控件节点(\<*input type="file"*>)的 *files* 属性,返回一个 *FileList* 实例。 + +- 拖拉一组文件时,目标区的 *DataTransfer.files* 属性,返回一个 *FileList* 实例。 + +```html + + + + +``` + +上面代码中,文件控件的 *files* 属性是一个 *FileList* 实例。 + +*FileList* 的实例属性主要是 *length*,表示包含多少个文件。 + +*FileList* 的实例方法主要是 *item( )*,用来返回指定位置的实例。它接受一个整数作为参数,表示位置的序号(从零开始)。 + +但是,由于 *FileList* 的实例是一个类似数组的对象,可以直接用方括号运算符,即 *myFileList[0]* 等同于 *myFileList.item(0)*,所以一般用不到 *item( )* 方法。 + + + +## *FileReader* 对象 + +*FileReader* 对象用于读取 *File* 对象或 *Blob* 对象所包含的文件内容。 + +浏览器原生提供一个 *FileReader* 构造函数,用来生成 *FileReader* 实例。 + +```js +var reader = new FileReader(); +``` + +*FileReader* 有以下的实例属性。 + +- *FileReader.error*:读取文件时产生的错误对象 + +- *FileReader.readyState*:整数,表示读取文件时的当前状态。一共有三种可能的状态,*0* 表示尚未加载任何数据,*1* 表示数据正在加载,*2* 表示加载完成。 + +- *FileReader.result*:读取完成后的文件内容,有可能是字符串,也可能是一个 *ArrayBuffer* 实例。 + +- *FileReader.onabort*:*abort* 事件(用户终止读取操作)的监听函数。 + +- *FileReader.onerror*:*error* 事件(读取错误)的监听函数。 + +- *FileReader.onload*:*load* 事件(读取操作完成)的监听函数,通常在这个函数里面使用 *result* 属性,拿到文件内容。 + +- *FileReader.onloadstart*:*loadstart* 事件(读取操作开始)的监听函数。 + +- *FileReader.onloadend*:*loadend* 事件(读取操作结束)的监听函数。 + +- *FileReader.onprogress*:*progress* 事件(读取操作进行中)的监听函数。 + +下面是监听 *load* 事件的一个例子。 + +```html + + + + +``` + +上面代码中,每当文件控件发生变化,就尝试读取第一个文件。如果读取成功( *load* 事件发生),就打印出文件内容。 + +*FileReader* 有以下实例方法。 + +- *FileReader.abort( )*:终止读取操作,*readyState* 属性将变成 *2*。 + +- *FileReader.readAsArrayBuffer( )*:以 *ArrayBuffer* 的格式读取文件,读取完成后 *result* 属性将返回一个 *ArrayBuffer* 实例。 + +- *FileReader.readAsBinaryString( )*:读取完成后,*result* 属性将返回原始的二进制字符串。 + +- *FileReader.readAsDataURL( )*:读取完成后,*result* 属性将返回一个 *Data URL* 格式( *Base64* 编码)的字符串,代表文件内容。对于图片文件,这个字符串可以用于 \<*img*> 元素的 *src* 属性。注意,这个字符串不能直接进行 *Base64* 解码,必须把前缀 `data:*/*;base64,` 从字符串里删除以后,再进行解码。 + +- *FileReader.readAsText( )*:读取完成后,*result* 属性将返回文件内容的文本字符串。该方法的第一个参数是代表文件的 *Blob* 实例,第二个参数是可选的,表示文本编码,默认为 *UTF-8*。 + +下面是一个读取图片文件的例子。 + +```html + + +``` + +```js +// 获取 DOM 元素 +var file = document.getElementById('file'); +file.onchange = function () { + var preview = document.querySelector('img'); + var file = document.querySelector('input[type=file]').files[0]; + var reader = new FileReader(); + + reader.addEventListener('load', function () { + preview.src = reader.result; + }, false); + + if (file) { + reader.readAsDataURL(file); + } +}; +``` + +上面代码中,用户选中图片文件以后,脚本会自动读取文件内容,然后作为一个 *Data URL* 赋值给 \<*img*> 元素的 *src* 属性,从而把图片展示出来。 + + + +## 综合实例 + +最后,我们通过一个综合实例来贯穿上面所学的内容。 + +*HTML* + +```html + +``` + +*CSS* + +```css +.uploadImg { + width: 150px; + height: 150px; + border: 1px dashed skyblue; + border-radius: 30px; + position: relative; + cursor: pointer; +} + +.cross { + width: 30px; + height: 30px; + position: absolute; + left: calc(50% - 15px); + top: calc(50% - 15px); +} + +.cross::before { + content: ""; + width: 30px; + height: 2px; + background-color: skyblue; + position: absolute; + top: calc(50% - 1px); +} + +.cross::after { + content: ""; + width: 30px; + height: 2px; + background-color: skyblue; + position: absolute; + left: calc(50% - 15px); + top: calc(50% - 1px); + transform: rotate(90deg); +} + +input[type="file"] { + display: none; +} +``` + +*JS* + +```js +var file = document.querySelector("#file"); +var div = document.querySelector(".uploadImg"); +var cross = document.querySelector(".cross"); +console.log(div.firstChild); +file.onchange = function () { + // 创建 filereader 用来读取文件 + var reader = new FileReader(); + // 获取到文件内容 + var content = file.files[0]; + if (content) { + reader.readAsDataURL(content); + } + reader.onload = function () { + // 设置 div 背景图像从而实现预览效果 + div.style.background = `url(${reader.result}) center/cover no-repeat` + cross.style.opacity = 0; + } +} +``` + + + +## *File System Access API* + +看上去上面的 *File API* 还不错,能够读取到本地的文件,但是它和离线存储有啥关系? + +我们要的是离线存储功能,能够将数据存储到本地。 + +嗯,确实 *File API* 只能够做读取的工作,但是有一套新的 *API* 规范又推出来了,叫做 *File System Access API*。 + +是的,你没有听错,这是**两套规范**,千万没弄混淆了。 + +- *File API* 规范:*https://w3c.github.io/FileAPI/* + +- *File System Access API* 规范:*https://wicg.github.io/file-system-access/* + +关于 *File System Access API*,这套方案应该是未来的主角。它提供了比较稳妥的本地文件交互模式,即保证了实用价值,又保障了用户的数据安全。 + +这个 *API* 对前端来说意义不小。有了这个功能,*Web* 可以提供更完整的功能链路,从打开、到编辑、到保存,一套到底。不过遗憾的是目前只有 *Chrome* 支持。 + +image-20211202102203944 + +(图为该 *API* 目前在各大浏览器的支持情况,可以看到全线飙红) + +目前针对该 *API* 的相关资料,无论是中文还是英文都比较少,如果对该 API 感兴趣的同学,下面给出两个扩展阅读资料(英文) + +- *MDN*:*https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API* + +- *web.dev*:*https://web.dev/file-system-access/* + +------- + +-*EOF*- \ No newline at end of file diff --git a/04. 浏览器的离线存储/File API课堂代码/.vscode/settings.json b/04. 浏览器的离线存储/File API课堂代码/.vscode/settings.json new file mode 100644 index 0000000..5165b7c --- /dev/null +++ b/04. 浏览器的离线存储/File API课堂代码/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "eggHelper.serverPort": 35684 +} \ No newline at end of file diff --git a/04. 浏览器的离线存储/File API课堂代码/index.html b/04. 浏览器的离线存储/File API课堂代码/index.html new file mode 100644 index 0000000..049dfc9 --- /dev/null +++ b/04. 浏览器的离线存储/File API课堂代码/index.html @@ -0,0 +1,72 @@ + + + + + + + + Document + + + + + + + + + \ No newline at end of file diff --git a/04. 浏览器的离线存储/File API课堂代码/test.txt b/04. 浏览器的离线存储/File API课堂代码/test.txt new file mode 100644 index 0000000..a8a9406 --- /dev/null +++ b/04. 浏览器的离线存储/File API课堂代码/test.txt @@ -0,0 +1 @@ +this is a test \ No newline at end of file diff --git a/04. 浏览器的离线存储/IndexedDB.md b/04. 浏览器的离线存储/IndexedDB.md new file mode 100644 index 0000000..32866f8 --- /dev/null +++ b/04. 浏览器的离线存储/IndexedDB.md @@ -0,0 +1,884 @@ +# *IndexedDB* + +本文主要包含以下内容: + +- *IndexedDB* 简介 +- *IndexedDB* 重要概念 +- *IndexedDB* 实操 + - 操作数据库 + - 插入数据 + - 读取数据 + - 更新数据 + - 删除数据 + + + +## *IndexedDB* 简介 + +随着浏览器的功能不断增强,越来越多的网站开始考虑,将大量数据储存在客户端,这样可以减少从服务器获取数据,直接从本地获取数据。 + +现有的浏览器数据储存方案,都不适合储存大量数据:*Cookie* 的大小不超过 *4KB*,且每次请求都会发送回服务器;*LocalStorage* 在 *2.5MB* 到 *10MB* 之间(各家浏览器不同),而且不提供搜索功能,不能建立自定义的索引。所以,需要一种新的解决方案,这就是 *IndexedDB* 诞生的背景。 + + + +image-20211201094954024 + + + +*MDN* 官网是这样解释 *IndexedDB* 的: +>*IndexedDB* 是一种底层 *API*,用于在客户端存储大量的结构化数据(也包括文件/二进制大型对象(*blobs*))。该 *API* 使用索引实现对数据的高性能搜索。虽然 *Web Storage* 在存储较少量的数据很有用,但对于存储更大量的结构化数据来说力不从心。而 *IndexedDB* 提供了这种场景的解决方案。 + + + +通俗地说,*IndexedDB* 就是浏览器提供的本地数据库,它可以被网页脚本创建和操作。*IndexedDB* 允许储存大量数据,提供查找接口,还能建立索引。这些都是 *LocalStorage* 所不具备的。就数据库类型而言,*IndexedDB* 不属于关系型数据库(不支持 *SQL* 查询语句),更接近 *NoSQL* 数据库。 + +下表罗列出了几种常见的客户端存储方式的对比: + +| | 会话期 Cookie | 持久性 Cookie | sessionStorage | localStorage | IndexedDB | WebSQL | +| -------- | ------------------ | ------------------------ | ---------------- | ------------------------ | -------------- | ------ | +| 存储大小 | 4kb | 4kb | 2.5~10MB | 2.5~10MB | >250MB | 已废弃 | +| 失效时间 | 浏览器关闭自动清除 | 设置过期时间,到期后清除 | 浏览器关闭后清除 | 永久保存(除非手动清除) | 手动更新或删除 | 已废弃 | + + + +*IndexedDB* 具有以下特点。 + +- 键值对储存:*IndexedDB* 内部采用对象仓库( *object store* )存放数据。所有类型的数据都可以直接存入,包括 *JavaScript* 对象。对象仓库中,数据以“键值对”的形式保存,每一个数据记录都有对应的主键,主键是独一无二的,不能有重复,否则会抛出一个错误。 + +- 异步:*IndexedDB* 操作时不会锁死浏览器,用户依然可以进行其他操作,这与 *LocalStorage* 形成对比,后者的操作是同步的。异步设计是为了防止大量数据的读写,拖慢网页的表现。 + +- 支持事务:*IndexedDB* 支持事务( *transaction* ),这意味着一系列操作步骤之中,只要有一步失败,整个事务就都取消,数据库回滚到事务发生之前的状态,不存在只改写一部分数据的情况。这和 *MySQL* 等数据库的事务类似。 + +- 同源限制:*IndexedDB* 受到同源限制,每一个数据库对应创建它的域名。网页只能访问自身域名下的数据库,而不能访问跨域的数据库。 + +- 储存空间大:这是 *IndexedDB* 最显著的特点之一。*IndexedDB* 的储存空间比 *LocalStorage* 大得多,一般来说不少于 *250MB*,甚至没有上限。 + +- 支持二进制储存:*IndexedDB* 不仅可以储存字符串,还可以储存二进制数据(*ArrayBuffer* 对象和 *Blob* 对象)。 + +*IndexedDB* 主要使用在于客户端需要存储大量的数据的场景下: + +- 数据可视化等界面,大量数据,每次请求会消耗很大性能。 + +- 即时聊天工具,大量消息需要存在本地。 + +- 其它存储方式容量不满足时,不得已使用 *IndexedDB* + + + +## *IndexedDB* 重要概念 + +在正式开始之前,我们先来介绍一下 *IndexedDB* 里面一些重要的概念。 + +*IndexedDB* 是一个比较复杂的 *API*,涉及不少概念。它把不同的实体,抽象成一个个对象接口。学习这个 *API*,就是学习它的各种对象接口。 + +- 数据库:*IDBDatabase* 对象 + +- 对象仓库:*IDBObjectStore* 对象 + +- 索引:*IDBIndex* 对象 + +- 事务:*IDBTransaction* 对象 + +- 操作请求:*IDBRequest* 对象 + +- 指针:*IDBCursor* 对象 + +- 主键集合:*IDBKeyRange* 对象 + +下面是一些主要的概念。 + +(1)数据库 + +数据库是一系列相关数据的容器。每个域名(严格的说,是协议 + 域名 + 端口)都可以新建任意多个数据库。 + +*IndexedDB* 数据库有版本的概念。同一个时刻,只能有一个版本的数据库存在。如果要修改数据库结构(新增或删除表、索引或者主键),只能通过升级数据库版本完成。 + +(2)对象仓库 + +每个数据库包含若干个对象仓库( *object store* )。它类似于关系型数据库的表格。 + +(3)数据记录 + +对象仓库保存的是数据记录。每条记录类似于关系型数据库的行,但是只有主键和数据体两部分。主键用来建立默认的索引,必须是不同的,否则会报错。主键可以是数据记录里面的一个属性,也可以指定为一个递增的整数编号。 + +```js +{ id: 1, text: 'foo' } +``` + +上面的对象中,*id* 属性可以当作主键。 + +数据体可以是任意数据类型,不限于对象。 + +(4)索引 + +为了加速数据的检索,可以在对象仓库里面,为不同的属性建立索引。 + +在关系型数据库当中也有索引的概念,我们可以给对应的表字段添加索引,以便加快查找速率。在 *IndexedDB* 中同样有索引,我们可以在创建 *store* 的时候同时创建索引,在后续对 *store* 进行查询的时候即可通过索引来筛选,给某个字段添加索引后,在后续插入数据的过成功,索引字段便不能为空。 + +(5)事务 + +数据记录的读写和删改,都要通过事务完成。事务对象提供 *error、abort* 和 *complete* 三个事件,用来监听操作结果。 + +(6)指针(游标) +游标是 *IndexedDB* 数据库新的概念,大家可以把游标想象为一个指针,比如我们要查询满足某一条件的所有数据时,就需要用到游标,我们让游标一行一行的往下走,游标走到的地方便会返回这一行数据,此时我们便可对此行数据进行判断,是否满足条件。 + +## *IndexedDB* 实操 + +*IndexedDB* 所有针对仓库的操作都是基于事务的。 + +在正式开始之前,我们先创建如下的项目结构: + +image-20211201095256757 + + + +该项目目录下存在 *2* 个文件,其中 *db.js* 是用来封装各种数据库操作的。 + +### 操作数据库 + +首先第一步是创建以及连接数据库。 + +*db.js* + +```js +/** + * 打开数据库 + * @param {object} dbName 数据库的名字 + * @param {string} storeName 仓库名称 + * @param {string} version 数据库的版本 + * @return {object} 该函数会返回一个数据库实例 + */ +function openDB(dbName, version = 1) { + return new Promise((resolve, reject) => { + var db; // 存储创建的数据库 + // 打开数据库,若没有则会创建 + const request = indexedDB.open(dbName, version); + + // 数据库打开成功回调 + request.onsuccess = function (event) { + db = event.target.result; // 存储数据库对象 + console.log("数据库打开成功"); + resolve(db); + }; + + // 数据库打开失败的回调 + request.onerror = function (event) { + console.log("数据库打开报错"); + }; + + // 数据库有更新时候的回调 + request.onupgradeneeded = function (event) { + // 数据库创建或升级的时候会触发 + console.log("onupgradeneeded"); + db = event.target.result; // 存储数据库对象 + var objectStore; + // 创建存储库 + objectStore = db.createObjectStore("stu", { + keyPath: "stuId", // 这是主键 + autoIncrement: true // 实现自增 + }); + // 创建索引,在后面查询数据的时候可以根据索引查 + objectStore.createIndex("stuId", "stuId", { unique: true }); + objectStore.createIndex("stuName", "stuName", { unique: false }); + objectStore.createIndex("stuAge", "stuAge", { unique: false }); + }; + }); +} +``` + +在上面的代码中,我们封装了一个 *openDB* 的函数,该函数调用 *indexedDB.open* 方法来尝试打开一个数据库,如果该数据库不存在,就会创建。 + +*indexedDB.open* 方法返回一个对象,我们在这个对象上面分别监听了成功、错误以及更新这三个事件。 + +这里尤其要说一下 *upgradeneeded* 更新事件。该事件会在数据库发生更新时触发,什么叫做数据库有更新时呢?就是添加或删除表,以及数据库版本号更新的时候。 + +因为一开始创建数据库时,版本是从无到有,所以也会触发这个事件。 + +*index.html* + +```html + + + + +``` + +在 *index.html* 文件中,我们引入了 *db.js*,然后调用了 *openDB* 方法,效果如下图所示。 + +![image-20211201095341185](https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2021-12-01-015341.png) + + + +使用完数据库后,建议关闭数据库,以节约资源。 + +```js +/** + * 关闭数据库 + * @param {object} db 数据库实例 + */ +function closeDB(db) { + db.close(); + console.log("数据库已关闭"); +} +``` + +如果要删除数据库,可以使用 *indexDB* 的 *deleteDatabase* 方法即可。 + +```js +/** + * 删除数据库 + * @param {object} dbName 数据库名称 + */ +function deleteDBAll(dbName) { + console.log(dbName); + let deleteRequest = window.indexedDB.deleteDatabase(dbName); + deleteRequest.onerror = function (event) { + console.log("删除失败"); + }; + deleteRequest.onsuccess = function (event) { + console.log("删除成功"); + }; +} +``` + +### 插入数据 + +接下来是插入数据,我们仍然封装一个 *addData* 方法,代码如下: + +```js +/** + * 新增数据 + * @param {object} db 数据库实例 + * @param {string} storeName 仓库名称 + * @param {string} data 数据 + */ +function addData(db, storeName, data) { + var request = db + .transaction([storeName], "readwrite") // 事务对象 指定表格名称和操作模式("只读"或"读写") + .objectStore(storeName) // 仓库对象 + .add(data); + + request.onsuccess = function (event) { + console.log("数据写入成功"); + }; + + request.onerror = function (event) { + console.log("数据写入失败"); + }; +} +``` + +*IndexedDB* 插入数据需要通过事务来进行操作,插入的方法也很简单,利用 *IndexedDB* 提供的 *add* 方法即可,这里我们同样将插入数据的操作封装成了一个函数,接收三个参数,分别如下: + +- *db*:在创建或连接数据库时,返回的 *db* 实例,需要那个时候保存下来。 +- *storeName*:仓库名称(或者表名),在创建或连接数据库时我们就已经创建好了仓库。 +- *data*:需要插入的数据,通常是一个对象。 + +接下来我们在 *index.html* 中来测试。 + +```html + + + + +``` + +效果如下: + + + +![image-20211201095402192](https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2021-12-01-015402.png) + + + +>注意:插入的数据是一个对象,而且必须包含我们声明的索引键值对。 + +### 读取数据 + +读取数据根据需求的不同有不同的读取方式。 + +### 通过主键读取数据 + +```js +/** + * 通过主键读取数据 + * @param {object} db 数据库实例 + * @param {string} storeName 仓库名称 + * @param {string} key 主键值 + */ +function getDataByKey(db, storeName, key) { + return new Promise((resolve, reject) => { + var transaction = db.transaction([storeName]); // 事务 + var objectStore = transaction.objectStore(storeName); // 仓库对象 + var request = objectStore.get(key); // 通过主键获取数据 + + request.onerror = function (event) { + console.log("事务失败"); + }; + + request.onsuccess = function (event) { + console.log("主键查询结果: ", request.result); + resolve(request.result); + }; + }); +} +``` + +在我在仓库对象上面调用 *get* 方法从而通过主键获取数据。 + +*index.html* + +```html + + + + +``` + +在 *index.html* 中进行测试,调用上面封装的 *getDataByKey* 方法,可以看到返回了主键 *stuId* 为 *2* 的学生数据。 + +仓库对象也提供了 *getAll* 方法, 能够查询整张表的数据内容。 + +*db.js* + +```js +/** + * 通过主键读取数据 + * @param {object} db 数据库实例 + * @param {string} storeName 仓库名称 + * @param {string} key 主键值 + */ +function getDataByKey(db, storeName, key) { + return new Promise((resolve, reject) => { + ... + var request = objectStore.getAll(); // 通过主键获取数据 + ... + }); +} +``` + +在 *index.html* 中调用方法时就需要再传递第三个参数作为 *key* 了。 + +```js +openDB('stuDB', 1) +.then((db) => { + addData(db, "stu", { "stuId": 1, "stuName": "谢杰", "stuAge": 18 }); + addData(db, "stu", { "stuId": 2, "stuName": "雅静", "stuAge": 20 }); + addData(db, "stu", { "stuId": 3, "stuName": "谢希之", "stuAge": 4 }); + return getDataByKey(db, "stu"); +}).then((stuInfo)=>{ + console.log(stuInfo); // 会查询到该表的所有数据 +}) +``` + +还可以通过指针来进行查询,例如: + +```js +/** + * 通过游标读取数据 + * @param {object} db 数据库实例 + * @param {string} storeName 仓库名称 + */ +function cursorGetData(db, storeName) { + return new Promise((resolve, reject) => { + let list = []; + var store = db + .transaction(storeName, "readwrite") // 事务 + .objectStore(storeName); // 仓库对象 + var request = store.openCursor(); // 指针对象 + // 游标开启成功,逐行读数据 + request.onsuccess = function (e) { + var cursor = e.target.result; + if (cursor) { + // 必须要检查 + list.push(cursor.value); + cursor.continue(); // 遍历了存储对象中的所有内容 + } else { + resolve(list) + } + }; + }) +} +``` + +在上面的代码中,我们通过仓库对象的 *openCursor* 方法开启了一个指针,这个指针会指向数据表的第一条数据,之后指针逐项进行偏移从而遍历整个数据表。 + +所以每次偏移拿到数据后,我们 *push* 到 *list* 数组里面,如果某一次没有拿到数据,说明已经读取完了所有的数据,那么我们就返回 *list* 数组。 + +*indx.html* + +```js +openDB('stuDB', 1) +.then((db) => { + addData(db, "stu", { "stuId": 1, "stuName": "谢杰", "stuAge": 18 }); + addData(db, "stu", { "stuId": 2, "stuName": "雅静", "stuAge": 20 }); + addData(db, "stu", { "stuId": 3, "stuName": "谢希之", "stuAge": 4 }); + return cursorGetData(db, "stu"); +}).then((stuInfo)=>{ + console.log(stuInfo); +}) +``` + +目前为止,我们的精准查询只能通过主键来进行查询。但是更多的场景是我们压根儿就不知道某一条数据的主键。例如我们要查询学生姓名为“张三”的学生数据,对于我们来讲,我们知道的信息只有学生姓名“张三”。 + +此时我们就可以通过索引来查询数据。 + +*db.js* + +```js +/** + * 通过索引读取数据 + * @param {object} db 数据库实例 + * @param {string} storeName 仓库名称 + * @param {string} indexName 索引名称 + * @param {string} indexValue 索引值 + */ +function getDataByIndex(db, storeName, indexName, indexValue) { + return new Promise((resolve, reject) => { + var store = db.transaction(storeName, "readwrite").objectStore(storeName); + var request = store.index(indexName).get(indexValue); + request.onerror = function () { + console.log("事务失败"); + }; + request.onsuccess = function (e) { + var result = e.target.result; + resolve(result); + }; + }) +} +``` + +在上面的方法中,我们通过仓库对象的 *index* 方法传入了索引名称,然后链式调用 *get* 方法传入索引的值来得到最终的查询结果。 + +*index.html* + +```js +openDB('stuDB', 1) +.then((db) => { + addData(db, "stu", { "stuId": 4, "stuName": "牛牛", "stuAge": 4 }); + return getDataByIndex(db, "stu", "stuAge", 4); +}).then((stuInfo) => { + console.log(stuInfo); // {stuId: 3, stuName: '谢希之', stuAge: 4} +}) +``` + +在 *index.html* 中我们新增了一条数据,年龄也为 *4*,当前的数据库表信息如下: + +![image-20211201095425944](https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2021-12-01-015426.png) + + + +但是很奇怪的是我们查询出来的数据却只有第一条符合要求的。 + +如果我们想要查询出索引中满足某些条件的所有数据,可以将索引和游标结合起来。 + +*db.js* + +```js +/** + * 通过索引和游标查询记录 + * @param {object} db 数据库实例 + * @param {string} storeName 仓库名称 + * @param {string} indexName 索引名称 + * @param {string} indexValue 索引值 + */ +function cursorGetDataByIndex(db, storeName, indexName, indexValue) { + return new Promise((resolve, reject) => { + let list = []; + var store = db.transaction(storeName, "readwrite").objectStore(storeName); // 仓库对象 + var request = store + .index(indexName) // 索引对象 + .openCursor(IDBKeyRange.only(indexValue)); // 指针对象 + request.onsuccess = function (e) { + var cursor = e.target.result; + if (cursor) { + // 必须要检查 + list.push(cursor.value); + cursor.continue(); // 遍历了存储对象中的所有内容 + } else { + resolve(list) + } + }; + request.onerror = function (e) { }; + }) +} +``` + +在上面的方法中,我们仍然是使用仓库对象的 *index* 方法进行索引查询,但是之后链式调用的时候不再是使用 *get* 方法传入索引值,而是调用了 *openCursor* 来打开一个指针,并且让指针指向满足索引值的数据,之后和前面一样,符合要求的数据推入到 *list* 数组,最后返回 *list* 数组。 + +当然,你可能很好奇 *IDBKeyRange* 的 *only* 方法是什么意思,除了 *only* 方法还有其他方法么? + +*IDBKeyRange* 对象代表数据仓库(*object store*)里面的一组主键。根据这组主键,可以获取数据仓库或索引里面的一组记录。 + +*IDBKeyRange* 可以只包含一个值,也可以指定上限和下限。它有四个静态方法,用来指定主键的范围。 + +- *IDBKeyRange.lowerBound( )*:指定下限。 + +- *IDBKeyRange.upperBound( )*:指定上限。 + +- *IDBKeyRange.bound( )*:同时指定上下限。 + +- *IDBKeyRange.only( )*:指定只包含一个值。 + +下面是一些代码实例。 + +```js +// All keys ≤ x +var r1 = IDBKeyRange.upperBound(x); + +// All keys < x +var r2 = IDBKeyRange.upperBound(x, true); + +// All keys ≥ y +var r3 = IDBKeyRange.lowerBound(y); + +// All keys > y +var r4 = IDBKeyRange.lowerBound(y, true); + +// All keys ≥ x && ≤ y +var r5 = IDBKeyRange.bound(x, y); + +// All keys > x &&< y +var r6 = IDBKeyRange.bound(x, y, true, true); + +// All keys > x && ≤ y +var r7 = IDBKeyRange.bound(x, y, true, false); + +// All keys ≥ x &&< y +var r8 = IDBKeyRange.bound(x, y, false, true); + +// The key = z +var r9 = IDBKeyRange.only(z); +``` + +例如我们来查询年龄大于 *4* 岁的学生,其代码片段如下: + +```js +function cursorGetDataByIndex(db, storeName, indexName, indexValue) { + return new Promise((resolve, reject) => { + ... + var request = store + .index(indexName) // 索引对象 + .openCursor(IDBKeyRange.lowerBound(indexValue, true)); // 指针对象 + ... + }) + +} +``` + +利用索引和游标结合查询,我们可以查询出索引值满足我们传入函数值的所有数据对象,而不是只查询出一条数据或者所有数据。 + +*IndexedDB* 分页查询不像 *MySQL* 分页查询那么简单,没有提供现成的 *API*,如 *limit* 等,所以需要我们自己实现分页。 + +```js +/** + * 通过索引和游标分页查询记录 + * @param {object} db 数据库实例 + * @param {string} storeName 仓库名称 + * @param {string} indexName 索引名称 + * @param {string} indexValue 索引值 + * @param {number} page 页码 + * @param {number} pageSize 查询条数 + */ +function cursorGetDataByIndexAndPage( + db, + storeName, + indexName, + indexValue, + page, + pageSize +) { + return new Promise((resolve, reject) => { + var list = []; + var counter = 0; // 计数器 + var advanced = true; // 是否跳过多少条查询 + var store = db.transaction(storeName, "readwrite").objectStore(storeName); // 仓库对象 + var request = store + // .index(indexName) // 索引对象 + // .openCursor(IDBKeyRange.only(indexValue)); // 按照指定值分页查询(配合索引) + .openCursor(); // 指针对象 + request.onsuccess = function (e) { + var cursor = e.target.result; + if (page > 1 && advanced) { + advanced = false; + cursor.advance((page - 1) * pageSize); // 跳过多少条 + return; + } + if (cursor) { + // 必须要检查 + list.push(cursor.value); + counter++; + if (counter < pageSize) { + cursor.continue(); // 遍历了存储对象中的所有内容 + } else { + cursor = null; + resolve(list); + } + } else { + resolve(list); + } + }; + request.onerror = function (e) { }; + }) +} +``` + +这里用到了 *IndexedDB* 的一个 *API*:*advance*。 + +该函数可以让我们的游标跳过多少条开始查询。假如我们的额分页是每页 *5* 条数据,现在需要查询第 *2* 页,那么我们就需要跳过前面 *5* 条数据,从第 *6* 条数据开始查询,直到计数器等于 *5*,那么我们就关闭游标,结束查询。 + +下面在 *index.html* 中进行测试如下: + +```html + + + + +``` + +在上面的代码中,我们为了实现分页效果,添加了一些数据。然后查询第 *3* 页的内容。 + +![image-20211201095452722](https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2021-12-01-015453.png) + +查询结果如下: + +image-20211201095509714 + +### 更新数据 + +*IndexedDB* 更新数据较为简单,直接使用 *put* 方法,值得注意的是如果数据库中没有该条数据,则会默认增加该条数据,否则更新。 + +有些小伙伴喜欢更新和新增都是用 *put* 方法,这也是可行的。 + +*db.js* + +```js +/** + * 更新数据 + * @param {object} db 数据库实例 + * @param {string} storeName 仓库名称 + * @param {object} data 数据 + */ +function updateDB(db, storeName, data) { + return new Promise((resolve, reject) => { + var request = db + .transaction([storeName], "readwrite") // 事务对象 + .objectStore(storeName) // 仓库对象 + .put(data); + + request.onsuccess = function () { + resolve({ + status: true, + message: "更新数据成功" + }) + }; + + request.onerror = function () { + reject({ + status: false, + message: "更新数据失败" + }) + }; + }) +} +``` + +在上面的方法中,我们使用仓库对象的 *put* 方法来修改数据,所以在调用该方法时,需要传入整条数据对象,特别是主键。因为是通过主键来查询到要修改的数据。 + +如果传入的数据没有主键,则是一个新增数据的效果。 + +*index.html* + +```js +openDB('stuDB', 1) + .then((db) => { + return updateDB(db, "stu", {stuId: 1, stuName: '谢杰2', stuAge: 19}); + }).then(({message}) => { + console.log(message); + }) +``` + +效果如下: + +![image-20211201095532213](https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2021-12-01-015532.png) + + + +### 删除数据 + +删除数据这里记录 *2* 种方式,一个是通过主键来进行删除。 + +*db.js* + +```js +/** + * 通过主键删除数据 + * @param {object} db 数据库实例 + * @param {string} storeName 仓库名称 + * @param {object} id 主键值 + */ +function deleteDB(db, storeName, id) { + return new Promise((resolve, reject) => { + var request = db + .transaction([storeName], "readwrite") + .objectStore(storeName) + .delete(id); + + request.onsuccess = function () { + resolve({ + status: true, + message: "删除数据成功" + }) + }; + + request.onerror = function () { + reject({ + status: true, + message: "删除数据失败" + }) + }; + }) +} +``` + +*index.html* + +```js +openDB('stuDB', 1) + .then((db) => { + return deleteDB(db, "stu", 1) + }).then(({message}) => { + console.log(message); + }) +``` + +执行上面的代码后 *stuId* 为 *1* 的学生被删除掉。 + +有时候我们拿不到主键值,只能只能通过索引值来删除。通过这种方式,我们可以删除一条数据(索引值唯一)或者所有满足条件的数据(索引值不唯一)。 + +*db.js* + +```js +/** + * 通过索引和游标删除指定的数据 + * @param {object} db 数据库实例 + * @param {string} storeName 仓库名称 + * @param {string} indexName 索引名 + * @param {object} indexValue 索引值 + */ +function cursorDelete(db, storeName, indexName, indexValue) { + return new Promise((resolve, reject) => { + var store = db.transaction(storeName, "readwrite").objectStore(storeName); + var request = store + .index(indexName) // 索引对象 + .openCursor(IDBKeyRange.only(indexValue)); // 指针对象 + request.onsuccess = function (e) { + var cursor = e.target.result; + var deleteRequest; + if (cursor) { + deleteRequest = cursor.delete(); // 请求删除当前项 + deleteRequest.onsuccess = function () { + console.log("游标删除该记录成功"); + resolve({ + status: true, + message: "游标删除该记录成功" + }) + }; + deleteRequest.onerror = function () { + reject({ + status: false, + message: "游标删除该记录失败" + }) + }; + cursor.continue(); + } + }; + request.onerror = function (e) { }; + }) +} +``` + +*index.html* + +```js +openDB('stuDB', 1) + .then((db) => { + return cursorDelete(db, "stu", "stuName", "雅静") + }).then(({ message }) => { + console.log(message); + }) +``` + +在上面的示例中,我们就删除了所有 *stuName* 值为 “雅静” 的同学。 + + +------- + +以上,就是关于 *IndexedDB* 的基本操作。 + +可以看到,在了解了它的几个基本概念后,上手还是比较容易的。 + +另外由于 *IndexedDB* 所提供的原生 *API* 比较复杂,所以现在也出现了基于 *IndexedDB* 封装的库。例如 *Dexie.js*。 + +![image-20211201095555138](https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2021-12-01-015556.png) + +(图为 *Dexie.js* 官网部分截图) + + + +*Dexie.js* 官网:*https://dexie.org/* + + + +该库和 *IndexedDB* 之间的关系,就类似于 *jQuery* 和 *JavaScript* 之间的关系。有兴趣的同学可以自行进行研究,这里就不再做过多的赘述。 + + + +如果想了解 *IndexedDB* 相关的更多 *API*,可以扩展阅读:*https://www.wangdoc.com/javascript/bom/indexeddb.html* + + +------- + + + +-*EOF*- \ No newline at end of file diff --git a/04. 浏览器的离线存储/IndexedDB课堂代码/.vscode/settings.json b/04. 浏览器的离线存储/IndexedDB课堂代码/.vscode/settings.json new file mode 100644 index 0000000..5165b7c --- /dev/null +++ b/04. 浏览器的离线存储/IndexedDB课堂代码/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "eggHelper.serverPort": 35684 +} \ No newline at end of file diff --git a/04. 浏览器的离线存储/IndexedDB课堂代码/db.js b/04. 浏览器的离线存储/IndexedDB课堂代码/db.js new file mode 100644 index 0000000..aaafcdf --- /dev/null +++ b/04. 浏览器的离线存储/IndexedDB课堂代码/db.js @@ -0,0 +1,330 @@ + +/** + * + * @param {*} dbName 数据库名称 + * @param {*} version 数据库的版本 + */ +function openDB(dbName, version = 1) { + return new Promise((resolve, reject) => { + var db; // 存储数据库对象 + // 打开数据库,如果没有就是创建操作 + var request = indexedDB.open(dbName, version); + + // 数据库打开或者创建成功的时候 + request.onsuccess = function (event) { + db = event.target.result; + console.log("数据库打开成功"); + resolve(db); + } + + // 打开失败 + request.onerror = function () { + console.log("数据库打开失败"); + } + + // 数据库发生更新的时候 + // 1. 版本号更新 2. 添加或者删除了表(对象仓库)的时候 + // 当我们第一次调用 open 方法时,会触发这个事件 + // 我们在这里来初始化我们的表 + request.onupgradeneeded = function (event) { + console.log("数据库版本更新"); + db = event.target.result; + // 创建数据仓库(表) + var objectStore = db.createObjectStore("stu", { + keyPath: "stuId", // 这是主键 + autoIncrement: true // 实现自增 + }); + // 创建索引,有了索引之后,查询速度大大增快(类比新华字典) + objectStore.createIndex("stuId", "stuId", { unique: true }); + objectStore.createIndex("stuName", "stuName", { unique: false }); + objectStore.createIndex("stuAge", "stuAge", { unique: false }); + } + }) +} + +/** + * 关闭数据库 + * @param {object} db 数据库实例 + */ +function closeDB(db) { + db.close(); + console.log("数据库已关闭"); +} + +/** +* 删除数据库 +* @param {object} dbName 数据库名称 +*/ +function deleteDBAll(dbName) { + console.log(dbName); + let deleteRequest = window.indexedDB.deleteDatabase(dbName); + deleteRequest.onerror = function (event) { + console.log("删除失败"); + }; + deleteRequest.onsuccess = function (event) { + console.log("删除成功"); + }; +} + + +/** + * + * @param {*} db 数据库实例 + * @param {*} storeName 数据仓库实例(表) + * @param {*} data 要添加的数据 + */ +function addData(db, storeName, data) { + var request = db.transaction([storeName], "readwrite") + .objectStore(storeName) + .add(data); + + request.onsuccess = function () { + console.log("数据写入成功"); + } + + request.onerror = function () { + console.log("数据写入失败") + } +} + +/** + * 通过主键来读取数据 + * @param {*} db 数据库实例对象 + * @param {*} storeName 数据仓库(表)实例对象 + * @param {*} key 主键 + */ +function getDataByKey(db, storeName, key) { + return new Promise((resolve, reject) => { + var request = db.transaction([storeName]) + .objectStore(storeName) + .get(key); + + request.onsuccess = function () { + resolve(request.result) + } + + request.onerror = function () { + console.log("数据查询失败"); + } + }) +} + +/** + * 通过主键来读取数据 + * @param {*} db 数据库实例对象 + * @param {*} storeName 数据仓库(表)实例对象 + */ +function getAllData(db, storeName) { + return new Promise((resolve, reject) => { + var request = db.transaction([storeName]) + .objectStore(storeName) + .getAll(); + + request.onsuccess = function () { + resolve(request.result) + } + + request.onerror = function () { + console.log("数据查询失败"); + } + }) +} + +/** + * 通过游标(指针)来查询所有的数据 + * @param {*} db + * @param {*} storeName + */ +function cursorGetData(db, storeName) { + return new Promise((resolve, reject) => { + var list = []; // 用于存放所有的数据 + var request = db.transaction([storeName], "readwrite") + .objectStore(storeName) + .openCursor(); // 创建一个指针(游标) + + request.onsuccess = function (event) { + var cursor = event.target.result; + // 查看游标(指针)有没有返回一条数据 + if (cursor) { + list.push(cursor.value); + cursor.continue(); // 移动到下一条数据 + } else { + resolve(list) + } + } + }) +} + +/** + * 根据索引来查询数据(只会返回一条) + * @param {*} db + * @param {*} storeName + * @param {*} indexName 索引名称 + * @param {*} indexValue 索引值 + */ +function getDataByIndex(db, storeName, indexName, indexValue) { + return new Promise((resolve, reject) => { + var request = db.transaction([storeName], "readwrite") + .objectStore(storeName) + .index(indexName) + .get(indexValue); + + request.onsuccess = function (event) { + resolve(event.target.result); + } + }) +} + +/** + * 根据索引和游标来查询数据 + * @param {*} db + * @param {*} storeName + * @param {*} indexName 索引名称 + * @param {*} indexValue 索引值 + */ +function getDataByIndex(db, storeName, indexName, indexValue) { + return new Promise((resolve, reject) => { + var list = []; // 存储所有满足条件的数据 + var request = db.transaction([storeName], "readwrite") + .objectStore(storeName) + .index(indexName) + .openCursor(IDBKeyRange.lowerBound(indexValue)); + + request.onsuccess = function (event) { + var cursor = event.target.result; + if (cursor) { + list.push(cursor.value); + cursor.continue(); + } else { + resolve(list); + } + } + }) +} + +/** + * 通过索引和游标分页查询记录 + * @param {object} db 数据库实例 + * @param {string} storeName 仓库名称 + * @param {string} indexName 索引名称 + * @param {string} indexValue 索引值 + * @param {number} page 页码 + * @param {number} pageSize 查询条数 + */ +function cursorGetDataByIndexAndPage( + db, + storeName, + indexName, + indexValue, + page, + pageSize +) { + return new Promise((resolve, reject) => { + var list = []; // 用于存储当前页的分页数据 + var counter = 0; // 创建一个计数器 + var isPass = true; // 是否要跳过数据 + var request = db.transaction([storeName], "readwrite") + .objectStore(storeName) + .openCursor(); // 创建一个指针(游标)对象(目前是指向第一条数据) + request.onsuccess = function (event) { + var cursor = event.target.result; + // 接下来有一个很重要的判断,判断是否要跳过一些数据 + if (page > 1 && isPass) { + isPass = false; + cursor.advance((page - 1) * pageSize); // 跳过数据 + return; + } + if (cursor) { + list.push(cursor.value); + counter++; + if (counter < pageSize) { + cursor.continue(); + } else { + cursor = null; + resolve(list); + } + } else { + resolve(list) + } + } + }) +} + +/** + * 更新数据 + * @param {object} db 数据库实例 + * @param {string} storeName 仓库名称 + * @param {object} data 数据 + */ +function updateDB(db, storeName, data) { + return new Promise((resolve, reject) => { + var request = db.transaction([storeName], "readwrite") + .objectStore(storeName) + .put(data); + request.onsuccess = function () { + resolve({ + status: true, + message: "更新数据成功" + }) + } + }) +} + +/** +* 通过主键删除数据 +* @param {object} db 数据库实例 +* @param {string} storeName 仓库名称 +* @param {object} id 主键值 +*/ +function deleteDB(db, storeName, id) { + return new Promise((resolve, reject) => { + var request = db + .transaction([storeName], "readwrite") + .objectStore(storeName) + .delete(id); + + request.onsuccess = function () { + resolve({ + status: true, + message: "删除数据成功" + }) + }; + + request.onerror = function () { + reject({ + status: true, + message: "删除数据失败" + }) + }; + }) +} + + +/** + * 通过索引和游标删除指定的数据 + * @param {object} db 数据库实例 + * @param {string} storeName 仓库名称 + * @param {string} indexName 索引名 + * @param {object} indexValue 索引值 + */ + function cursorDelete(db, storeName, indexName, indexValue) { + return new Promise((resolve, reject)=>{ + var request = db.transaction([storeName], "readwrite") + .objectStore(storeName) + .index(indexName) + .openCursor(IDBKeyRange.only(indexValue)); + request.onsuccess = function(event){ + var cursor = event.target.result; + if(cursor){ + var deleteRequest = cursor.delete(); + deleteRequest.onsuccess = function(){ + resolve({ + status: true, + message: "删除数据成功" + }) + } + cursor.continue(); + } + } + }) + } \ No newline at end of file diff --git a/04. 浏览器的离线存储/IndexedDB课堂代码/index.html b/04. 浏览器的离线存储/IndexedDB课堂代码/index.html new file mode 100644 index 0000000..b0a6e39 --- /dev/null +++ b/04. 浏览器的离线存储/IndexedDB课堂代码/index.html @@ -0,0 +1,63 @@ + + + + + + + + Document + + + + + + + + \ No newline at end of file diff --git a/04. 浏览器的离线存储/WebSQL.md b/04. 浏览器的离线存储/WebSQL.md new file mode 100644 index 0000000..8a171a4 --- /dev/null +++ b/04. 浏览器的离线存储/WebSQL.md @@ -0,0 +1,223 @@ +# *WebSQL* + +*WebSQL* 数据库 *API* 并不是 *HTML5* 规范的一部分,但是它是一个独立的规范,引入了一组使用 *SQL* 操作客户端数据库的 *APIs*。 + +如果你之前接触过诸如像 *MySQL* 这样的关系型数据库,学习过 *SQL* 语言,那么 *WebSQL* 对于你来讲没有任何的难度。 + +最新版的 *Safari, Chrome* 和 *Opera* 浏览器都支持 *WebSQL*。 + + + +image-20211130142613099 + + + +在 *WebSQL* 中,有 *3* 个核心方法: + +- *openDatabase*:这个方法使用现有的数据库或者新建的数据库创建一个数据库对象。 + +- *transaction*:这个方法让我们能够控制一个事务,以及基于这种情况执行提交或者回滚。 + +- *executeSql*:这个方法用于执行实际的 *SQL* 查询。 + + + +## 打开数据库 + +我们可以使用 *openDatabase( )* 方法来打开已存在的数据库,如果数据库不存在,则会创建一个新的数据库,使用代码如下: + +```js +var db = openDatabase('mydb', '1.0', 'Test DB', 2 * 1024 * 1024); +``` + +在上面的代码中,我们尝试打开一个名为 *mydb* 的数据库,因为第一次不存在此数据库,所以会创建该数据库,版本号为 *1.0*,大小为 *2M*。 + + + +image-20211130142639596 + +*openDatabase( )* 方法对应的 *5* 个参数: + +- 数据库名称 + +- 版本号 + +- 描述文本 + +- 数据库大小 + +- 创建回调 + +第 *5* 个参数,创建回调会在创建数据库后被调用。 + + + +## 执行操作 + +执行操作使用 *database.transaction( )* 函数: + +```js +var db = openDatabase('mydb', '1.0', 'Test DB', 2 * 1024 * 1024); +db.transaction(function (tx) { + tx.executeSql('CREATE TABLE IF NOT EXISTS LOGS (id unique, log)'); +}); +``` + +上面的语句执行后会在 '*mydb*' 数据库中创建一个名为 *LOGS* 的表。 + +该表存在 *2* 个字段 *id* 和 *log*,其中 *id* 是唯一的。 + +image-20211130142711069 + + + +## 插入数据 + +在执行上面的创建表语句后,我们可以插入一些数据: + +```js +var db = openDatabase('mydb', '1.0', 'Test DB', 2 * 1024 * 1024); +db.transaction(function (tx) { + tx.executeSql('CREATE TABLE IF NOT EXISTS STU (id unique, name, age)'); + tx.executeSql('INSERT INTO STU (id, name, age) VALUES (1, "张三", 18)'); + tx.executeSql('INSERT INTO STU (id, name, age) VALUES (2, "李四", 20)'); +}); +``` + +在上面的代码中,我们创建了一张名为 *STU* 的表,该表存在 *3* 个字段 *id,name* 和 *age*。 + +之后我们往这张表中插入了 *2* 条数据。 + +image-20211130142729393 + + + +我们也可以使用动态值来插入数据: + +```js +var stuName = "谢杰"; +var stuAge = 18; +var db = openDatabase('mydb', '1.0', 'Test DB', 2 * 1024 * 1024); +db.transaction(function (tx) { + tx.executeSql('CREATE TABLE IF NOT EXISTS STU (id unique, name, age)'); + // tx.executeSql('INSERT INTO STU (id, name, age) VALUES (1, "张三", 18)'); + // tx.executeSql('INSERT INTO STU (id, name, age) VALUES (2, "李四", 20)'); + tx.executeSql('INSERT INTO STU (id, name, age) VALUES (3, ?, ?)', [stuName, stuAge]); +}); +``` + +在上面的代码中,我们使用动态值的方式插入了一条数据,实例中的 *stuName* 和 *stuAge* 是外部变量,*executeSql* 会映射数组参数中的每个条目给 "?"。 + +>注意:由于上一次操作已经插入了 *id* 为 *1* 和 *2* 的数据,所以这一次插入内容时,要将前面两次插入语句注释调,否则插入操作不会成功。因为这里是一个事务,前面失败了会导致后面也失败。 + + + +## 读取数据 + +以下实例演示了如何读取数据库中已经存在的数据: + +```html +
+``` + +```js +var stuName = "谢杰"; +var stuAge = 18; +// 打开数据库 +var db = openDatabase('mydb', '1.0', 'Test DB', 2 * 1024 * 1024); +// 插入数据 +db.transaction(function (tx) { + tx.executeSql('CREATE TABLE IF NOT EXISTS STU (id unique, name, age)'); + tx.executeSql('INSERT INTO STU (id, name, age) VALUES (1, "张三", 18)'); + tx.executeSql('INSERT INTO STU (id, name, age) VALUES (2, "李四", 20)'); + tx.executeSql('INSERT INTO STU (id, name, age) VALUES (3, ?, ?)', [stuName, stuAge]); +}); + +// 读取操作 +db.transaction(function (tx) { + tx.executeSql('SELECT * FROM STU', [], function (tx, results) { + var len = results.rows.length, i; + msg = "

查询记录条数: " + len + "

"; + document.querySelector('#status').innerHTML += msg; + + for (i = 0; i < len; i++) { + msg = "

" + results.rows.item(i).name + ":" + results.rows.item(i).age + "

"; + document.querySelector('#status').innerHTML += msg; + } + + }, null); +}); +``` + +在上面的代码中,第二个部分是读取数据的操作。这里我们仍然是使用的 *executeSql( )* 方法来执行的 *SQL* 命令,但是用法又不一样了。是时候来看一下完整的 *executeSql( )* 方法是什么样了。 + +```js +executeSql(sqlStatement, arguments, callback, errorCallback) +``` + +该方法完整的语法实际上是接收 *4* 个参数,分别是: + +- *SQL* 语句 +- 参数 +- 执行 *SQL* 语句后的回调 +- 错误回调 + +因此在上面的示例中,我们 *executeSql( )* 的第三个参数就是执行了 *SQL* 语句后的回调。我们在回调中可以通过 *results.rows* 拿到该表中的数据,之后对数据进行业务需求的操作即可。 + +image-20211130142755739 + +## 删除数据 + +删除数据也是使用 *SQL* 中的语法,同样也支持动态指定的方式。 + +```js +var stuID = 2; +// 删除操作 +db.transaction(function (tx) { + tx.executeSql('DELETE FROM STU WHERE id=1'); + tx.executeSql('DELETE FROM STU WHERE id=?', [stuID]); +}); +``` + +在上面的代码中,我们删除了 *id* 为 *1* 和 *2* 的两条数据,其中第二条是动态指定的。 + + + +## 修改数据 + +要修改数据也是使用 *SQL* 中的语法,同样也支持动态指定的方式。 + +```js +var stuID = 3; +// 更新操作 +db.transaction(function (tx) { + tx.executeSql('UPDATE STU SET name=\'王羲之\' WHERE id=3'); + tx.executeSql('UPDATE STU SET age=21 WHERE id=?', [stuID]); +}); +``` + +在上面的代码中,我们修改了 *id* 为 *3* 的学生,名字修改为“王羲之”,年龄修改为 *21*。 + + + +## 总结 + +目前来看,*WebSQL* 已经不再是 *W3C* 推荐规范,官方也已经不再维护了。原因说的很清楚,当前的 *SQL* 规范采用 *SQLite* 的 *SQL* 方言,而作为一个标准,这是不可接受的。 + +另外,*WebSQL* 使用 *SQL* 语言来进行操作,更像是一个关系型数据库,而 *IndexedDB* 则更像是一个 *NoSQL* 数据库, 这也是目前 *W3C* 强推的浏览端数据库解决方案。 + +所以本文不再对 *WebSQL* 做过多的介绍。 + +如果有兴趣的同学,可以参阅下面的资料进行扩展阅读: + +- *View Web SQL data*:*https://developer.chrome.com/docs/devtools/storage/websql/?utm_source=devtools#run*(需要搭梯子) +- *CSDN WebSQL* 最全详解:*https://blog.csdn.net/weixin_45389633/article/details/107308968* + + + + +------- + + + +-*EOF*- \ No newline at end of file diff --git a/04. 浏览器的离线存储/浏览器离线存储概述.md b/04. 浏览器的离线存储/浏览器离线存储概述.md new file mode 100644 index 0000000..a73d019 --- /dev/null +++ b/04. 浏览器的离线存储/浏览器离线存储概述.md @@ -0,0 +1,53 @@ +# 浏览器离线存储概述 + + + +在前面的章节中,我们已经为大家介绍了整个浏览器的组成部分: + + + +image-20211130141852946 + + + +大致分为: + + + +- 用户界面(*user interface*) + +- 浏览器引擎(*browser engine*) + +- 渲染引擎(*rendering engine*) + +- 网络(*networking*) + +- *JS* 解释器(*JavaScript interpreter*) + +- 用户界面后端(*UI backend*) + +- 数据存储(*data storage*) + + + +而本章节我们就一起来看一下 *Data Persistence/storage* 这个部分,翻译成中文叫做浏览器离线存储或者本地存储。顾名思义,就是内容存储在浏览器这一边。 + + + +目前常见的浏览器离线存储的方式如下: + + + +- *Cookie* +- *Web Storage* +- *WebSQL* +- *IndexedDB* +- *File System* + + + +------ + + + +-*EOF*- \ No newline at end of file diff --git a/05. 浏览器的缓存/浏览器缓存.md b/05. 浏览器的缓存/浏览器缓存.md new file mode 100644 index 0000000..2647e00 --- /dev/null +++ b/05. 浏览器的缓存/浏览器缓存.md @@ -0,0 +1,483 @@ +# 浏览器缓存 + + + +本文主要包含以下内容: + +- 什么是浏览器缓存 +- 按照缓存位置分类 + - *Service Worker* + - *Memory Cache* + - *Disk Cache* + - *Push Cache* +- 按照缓存类型分类 + - 强制缓存 + - 协商缓存 +- 缓存读取规则 +- 浏览器行为 +- 实操案例 +- 缓存的最佳实践 + +## 什么是浏览器缓存 + +在正式开始讲解浏览器缓存之前,我们先来回顾一下整个 *Web* 应用的流程。 + +![image-20211203143550954](https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2021-12-03-063551.png) + + + +上图展示了一个 *Web* 应用最最简单的结构。客户端向服务器端发送 *HTTP* 请求,服务器端从数据库获取数据,然后进行计算处理,之后向客户端返回 *HTTP* 响应。 + +那么上面整个流程中,哪些地方比较耗费时间呢?总结起来有如下两个方面: + +- 发送请求的时候 + +- 涉及到大量计算的时候 + +一般来讲,上面两个阶段比较耗费时间。 + +首先是发送请求的时候。这里所说的请求,不仅仅是 *HTTP* 请求,也包括服务器向数据库发起查询数据的请求。 + +其次是大量计算的时候。一般涉及到大量计算,主要是在服务器端和数据库端,服务器端要进行计算这个很好理解,数据库要根据服务器发送过来的查询命令查询到对应的数据,这也是比较耗时的一项工作。 + +因此,单论缓存的话,我们其实在很多地方都可以做缓存。例如: + +- 数据库缓存 +- *CDN* 缓存 +- 代理服务器缓存 +- 浏览器缓存 +- 应用层缓存 + +针对各个地方做出适当的缓存,都能够很大程度的优化整个 *Web* 应用的性能。但是要逐一讨论的话,是一个非常大的工程量,所以本文我们主要来看一下浏览器缓存,这也是和我们前端开发息息相关的。 + +整个浏览器的缓存过程如下: + +image-20211203143612695 + + + +从上图我们可以看到,整个浏览器端的缓存其实没有想象的那么复杂。其最基本的原理就是: + +- 浏览器每次发起请求,都会先在浏览器缓存中查找该请求的结果以及缓存标识 + +- 浏览器每次拿到返回的请求结果都会将该结果和缓存标识存入浏览器缓存中 + +以上两点结论就是浏览器缓存机制的关键,它确保了每个请求的缓存存入与读取,只要我们再理解浏览器缓存的使用规则,那么所有的问题就迎刃而解了。 + +接下来,我将从两个维度来介绍浏览器缓存: + +- 缓存的存储位置 + +- 缓存的类型 + +## 按照缓存位置分类 + +从缓存位置上来说分为四种,并且各自有优先级,当依次查找缓存且都没有命中的时候,才会去请求网络。这四种依次为: + +- *Service Worker* + +- *Memory Cache* + +- *Disk Cache* + +- *Push Cache* + +### *Service Worker* + +*Service Worker* 是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。 + +使用 *Service Worker* 的话,传输协议必须为 *HTTPS*。因为 *Service Worker* 中涉及到请求拦截,所以必须使用 *HTTPS* 协议来保障安全。 + +*Service Worker* 的缓存与浏览器其他内建的缓存机制不同,它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。 + +*Service Worker* 实现缓存功能一般分为三个步骤:首先需要先注册 *Service Worker*,然后监听到 *install* 事件以后就可以缓存需要的文件,那么在下次用户访问的时候就可以通过拦截请求的方式查询是否存在缓存,存在缓存的话就可以直接读取缓存文件,否则就去请求数据。. + +当 *Service Worker* 没有命中缓存的时候,我们需要去调用 *fetch* 函数获取数据。也就是说,如果我们没有在 *Service Worker* 命中缓存的话,会根据缓存查找优先级去查找数据。 + + + +![image-20211203143635717](https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2021-12-03-063636.png) + + + +但是不管我们是从 *Memory Cache* 中还是从网络请求中获取的数据,浏览器都会显示我们是从 *Service Worker* 中获取的内容。 + + + +### *Memory Cache* + +*Memory Cache* 也就是内存中的缓存,主要包含的是当前中页面中已经抓取到的资源,例如页面上已经下载的样式、脚本、图片等。 + +读取内存中的数据肯定比磁盘快,内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放。一旦我们关闭 *Tab* 页面,内存中的缓存也就被释放了。 + +那么既然内存缓存这么高效,我们是不是能让数据都存放在内存中呢? + +这是不可能的。计算机中的内存一定比硬盘容量小得多,操作系统需要精打细算内存的使用,所以能让我们使用的内存必然不多。 + +当我们访问过页面以后,再次刷新页面,可以发现很多数据都来自于内存缓存。 + +![image-20211203143700033](https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2021-12-03-063700.png) + + + +*Memory Cache* 机制保证了一个页面中如果有两个相同的请求。 + +例如两个 *src* 相同的 \<*img*>,两个 *href* 相同的 \<*link*>,都实际只会被请求最多一次,避免浪费。 + +### *Disk Cache* + +*Disk Cache* 也就是存储在硬盘中的缓存,读取速度慢点,但是什么都能存储到磁盘中,比之 *Memory Cache* 胜在容量和存储时效性上。 + +在所有浏览器缓存中,*Disk Cache* 覆盖面基本是最大的。它会根据 *HTTP Herder* 中的字段判断哪些资源需要缓存,哪些资源可以不请求直接使用,哪些资源已经过期需要重新请求。 + +并且即使在跨站点的情况下,相同地址的资源一旦被硬盘缓存下来,就不会再次去请求数据。绝大部分的缓存都来自 *Disk Cache*。 + +凡是持久性存储都会面临容量增长的问题,*Disk Cache* 也不例外。 + +在浏览器自动清理时,会有特殊的算法去把“最老的”或者“最可能过时的”资源删除,因此是一个一个删除的。不过每个浏览器识别“最老的”和“最可能过时的”资源的算法不尽相同,这也可以看作是各个浏览器差异性的体现。 + +### *Push Cache* + +*Push Cache* 翻译成中文叫做“推送缓存”,是属于 *HTTP/2* 中新增的内容。 + +当以上三种缓存都没有命中时,它才会被使用。它只在会话(*Session*)中存在,一旦会话结束就被释放,并且缓存时间也很短暂,在 *Chrome* 浏览器中只有 *5* 分钟左右,同时它也并非严格执行 *HTTP/2* 头中的缓存指令。 + +*Push Cache* 在国内能够查到的资料很少,也是因为 *HTTP2* 在国内还不够普及。 + +这里推荐阅读 *Jake Archibald* 的 [*HTTP/2 push is tougher than I thought*](https://jakearchibald.com/2017/h2-push-tougher-than-i-thought/) 这篇文章。 + +文章中的几个结论: + +- 所有的资源都能被推送,并且能够被缓存,但是 *Edge* 和 *Safari* 浏览器支持相对比较差 + +- 可以推送 *no-cache* 和 *no-store* 的资源 + +- 一旦连接被关闭,*Push Cache* 就被释放 + +- 多个页面可以使用同一个 *HTTP/2* 的连接,也就可以使用同一个 *Push Cache*。这主要还是依赖浏览器的实现而定,出于对性能的考虑,有的浏览器会对相同域名但不同的 *tab* 标签使用同一个 *HTTP* 连接。 + +- *Push Cache* 中的缓存只能被使用一次 + +- 浏览器可以拒绝接受已经存在的资源推送 + +- 你可以给其他域名推送资源 + + +------- + +如果一个请求在上述几个位置都没有找到缓存,那么浏览器会正式发送网络请求去获取内容。之后为了提升之后请求的缓存命中率,自然要把这个资源添加到缓存中去。具体来说: + +- 根据 *Service Worker* 中的 *handler* 决定是否存入 *Cache Storage* (额外的缓存位置)。*Service Worker* 是由开发者编写的额外的脚本,且缓存位置独立,出现也较晚,使用还不算太广泛。 + +- *Memory Cache* 保存一份资源的引用,以备下次使用。*Memory Cache* 是浏览器为了加快读取缓存速度而进行的自身的优化行为,不受开发者控制,也不受 *HTTP* 协议头的约束,算是一个黑盒。 + +- 根据 *HTTP* 头部的相关字段( *Cache-control、Pragma* 等 )决定是否存入 *Disk Cache*。*Disk Cache* 也是平时我们最熟悉的一种缓存机制,也叫 *HTTP Cache* (因为不像 *Memory Cache*,它遵守 *HTTP* 协议头中的字段)。平时所说的强制缓存,协商缓存,以及 *Cache-Control* 等,也都归于此类。 + +## 按照缓存类型分类 + +按照缓存类型来进行分类,可以分为**强制缓存**和**协商缓存**。需要注意的是,无论是强制缓存还是协商缓存,都是属于 *Disk Cache* 或者叫做 *HTTP Cache* 里面的一种。 + +### 强制缓存 + +强制缓存的含义是,当客户端请求后,会先访问缓存数据库看缓存是否存在。如果存在则直接返回;不存在则请求真的服务器,响应后再写入缓存数据库。 + +强制缓存直接减少请求数,是提升最大的缓存策略。如果考虑使用缓存来优化网页性能的话,强制缓存应该是首先被考虑的。 + +可以造成强制缓存的字段是 *Cache-control* 和 *Expires*。 + +#### *Expires* + +这是 *HTTP 1.0* 的字段,表示缓存到期时间,是一个绝对的时间 (当前时间+缓存时间),如: + +``` +Expires: Thu, 10 Nov 2017 08:45:11 GMT +``` + +在响应消息头中,设置这个字段之后,就可以告诉浏览器,在未过期之前不需要再次请求。 + +但是,这个字段设置时有两个缺点: + +- 由于是绝对时间,用户可能会将客户端本地的时间进行修改,而导致浏览器判断缓存失效,重新请求该资源。此外,即使不考虑自行修改的因素,时差或者误差等因素也可能造成客户端与服务端的时间不一致,致使缓存失效。 + +- 写法太复杂了。表示时间的字符串多个空格,少个字母,都会导致变为非法属性从而设置失效。 + + + +#### *Cache-control* + +已知 *Expires* 的缺点之后,在 *HTTP/1.1* 中,增加了一个字段 *Cache-control*,该字段表示资源缓存的最大有效时间,在该时间内,客户端不需要向服务器发送请求 + +这两者的区别就是前者是绝对时间,而后者是相对时间。如下: + +``` +Cache-control: max-age=2592000 +``` + +下面列举一些 *Cache-control* 字段常用的值:(完整的列表可以查看 [*MDN*](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Cache-Control)) + +- *max-age*:即最大有效时间,在上面的例子中我们可以看到 + +- *must-revalidate*:如果超过了 *max-age* 的时间,浏览器必须向服务器发送请求,验证资源是否还有效。 + +- *no-cache*:虽然字面意思是“不要缓存”,但实际上还是要求客户端缓存内容的,只是是否使用这个内容由后续的协商缓存来决定。 + +- *no-store*:真正意义上的“不要缓存”。所有内容都不走缓存,包括强制缓存和协商缓存。 + +- *public*:所有的内容都可以被缓存(包括客户端和代理服务器, 如 *CDN* ) + +- *private*:所有的内容只有客户端才可以缓存,代理服务器不能缓存。默认值。 + +这些值可以混合使用,例如 *Cache-control:public, max-age=2592000*。在混合使用时,它们的优先级如下图: + +image-20211203143733448 + + + +>*max-age=0* 和 *no-cache* 等价吗? +>从规范的字面意思来说,*max-age* 到期是 应该( *SHOULD* )重新验证,而 *no-cache* 是 必须( *MUST* )重新验证。但实际情况以浏览器实现为准,大部分情况他们俩的行为还是一致的。(如果是 *max-age=0, must-revalidate* 就和 *no-cache* 等价了) + +在 *HTTP/1.1* 之前,如果想使用 *no-cache*,通常是使用 *Pragma* 字段,如 *Pragma: no-cache*(这也是 *Pragma* 字段唯一的取值)。 + +但是这个字段只是浏览器约定俗成的实现,并没有确切规范,因此缺乏可靠性。它应该只作为一个兼容字段出现,在当前的网络环境下其实用处已经很小。 + +总结一下,自从 *HTTP/1.1* 开始,*Expires* 逐渐被 *Cache-control* 取代。 + +*Cache-control* 是一个相对时间,即使客户端时间发生改变,相对时间也不会随之改变,这样可以保持服务器和客户端的时间一致性。而且 *Cache-control* 的可配置性比较强大。*Cache-control* 的优先级高于 *Expires*。 + +为了兼容 *HTTP/1.0* 和 *HTTP/1.1*,实际项目中两个字段我们都会设置。 + + + +### 协商缓存 + +当强制缓存失效(超过规定时间)时,就需要使用协商缓存,由服务器决定缓存内容是否失效。 + +流程上说,浏览器先请求缓存数据库,返回一个缓存标识。之后浏览器拿这个标识和服务器通讯。如果缓存未失效,则返回 *HTTP* 状态码 *304* 表示继续使用,于是客户端继续使用缓存; + +image-20211203143800447 + + + +如果失效,则返回新的数据和缓存规则,浏览器响应数据后,再把规则写入到缓存数据库。 + +image-20211203143820739 + + + +协商缓存在请求数上和没有缓存是一致的,但如果是 *304* 的话,返回的仅仅是一个状态码而已,并没有实际的文件内容,因此 在响应体体积上的节省是它的优化点。 + +它的优化主要体现在“响应”上面通过减少响应体体积,来缩短网络传输时间。所以和强制缓存相比提升幅度较小,但总比没有缓存好。 + +协商缓存是可以和强制缓存一起使用的,作为在强制缓存失效后的一种后备方案。实际项目中他们也的确经常一同出现。 + +对比缓存有 *2* 组字段(不是两个): + +- *Last-Modified & If-Modified-Since* + +- *Etag & If-None-Match* + +#### *Last-Modified & If-Modified-Since* + +1. 服务器通过 *Last-Modified* 字段告知客户端,资源最后一次被修改的时间,例如: + + ``` + Last-Modified: Mon, 10 Nov 2018 09:10:11 GMT + ``` + +2. 浏览器将这个值和内容一起记录在缓存数据库中。 + + +3. 下一次请求相同资源时时,浏览器从自己的缓存中找出“不确定是否过期的”缓存。因此在请求头中将上次的 *Last-Modified* 的值写入到请求头的 *If-Modified-Since* 字段 + + +4. 服务器会将 *If-Modified-Since* 的值与 *Last-Modified* 字段进行对比。如果相等,则表示未修改,响应 *304*;反之,则表示修改了,响应 *200* 状态码,并返回数据。 + +但是他还是有一定缺陷的: + +- 如果资源更新的速度是秒以下单位,那么该缓存是不能被使用的,因为它的时间单位最低是秒。 + +- 如果文件是通过服务器动态生成的,那么该方法的更新时间永远是生成的时间,尽管文件可能没有变化,所以起不到缓存的作用。 + +因此在 *HTTP/1.1* 出现了 *ETag* 和 *If-None-Match* + + + +#### *Etag & If-None-Match* + +为了解决上述问题,出现了一组新的字段 *Etag* 和 *If-None-Match*。 + +*Etag* 存储的是文件的特殊标识(一般都是一个 *Hash* 值),服务器存储着文件的 *Etag* 字段。 + +之后的流程和 *Last-Modified* 一致,只是 *Last-Modified* 字段和它所表示的更新时间改变成了 *Etag* 字段和它所表示的文件 *hash*,把 *If-Modified-Since* 变成了 *If-None-Match*。 + +浏览器在下一次加载资源向服务器发送请求时,会将上一次返回的 Etag 值放到请求头里的 *If-None-Match* 里,服务器只需要比较客户端传来的 *If-None-Match* 跟自己服务器上该资源的 *ETag* 是否一致,就能很好地判断资源相对客户端而言是否被修改过了。 + +如果服务器发现 *ETag* 匹配不上,那么直接以常规 *GET 200* 回包形式将新的资源(当然也包括了新的 *ETag*)发给客户端;如果 *ETag* 是一致的,则直接返回 *304* 告诉客户端直接使用本地缓存即可。 + +image-20211203143850009 + + + +两者之间的简单对比: + +- 首先在精确度上,*Etag* 要优于 *Last-Modified*。 + + *Last-Modified* 的时间单位是秒,如果某个文件在 *1* 秒内改变了多次,那么 *Last-Modified* 其实并没有体现出来修改,但是 *Etag* 是一个 *Hash* 值,每次都会改变从而确保了精度。 + +- 第二在性能上,*Etag* 要逊于 *Last-Modified*,毕竟 *Last-Modified* 只需要记录时间,而 *Etag* 需要服务器通过算法来计算出一个 *Hash* 值。 + +- 第三在优先级上,服务器校验优先考虑 *Etag*,也就是说 *Etag* 的优先级高于 *Last-Modified*。 + + + +## 缓存读取规则 + +接下来我们来对上面所讲的缓存做一个总结。 + +当浏览器要请求资源时: + +1. 从 *Service Worker* 中获取内容( 如果设置了 *Service Worker* ) + +2. 查看 *Memory Cache* + +3. 查看 *Disk Cache*。这里又细分: + + - 如果有强制缓存且未失效,则使用强制缓存,不请求服务器。这时的状态码全部是 *200* + + - 如果有强制缓存但已失效,使用协商缓存,比较后确定 *304* 还是 *200* + +4. 发送网络请求,等待网络响应 + +5. 把响应内容存入 *Disk Cache* (如果 *HTTP* 响应头信息有相应配置的话) + +6. 把响应内容的引用存入 *Memory Cache* (无视 *HTTP* 头信息的配置) + +7. 把响应内容存入 *Service Worker* 的 *Cache Storage*( 如果设置了 *Service Worker* ) + +其中针对第 *3* 步,具体的流程图如下: + +image-20211203143918845 + + + +## 浏览器行为 + +在了解了整个缓存策略或者说缓存读取流程后,我们还需要了解一个东西,那就是用户对浏览器的不同操作,会触发不同的缓存读取策略。 + +对应主要有 *3* 种不同的浏览器行为: + +- 打开网页,地址栏输入地址:查找 *Disk Cache* 中是否有匹配。如有则使用;如没有则发送网络请求。 + +- 普通刷新 (F5):因为 TAB 并没有关闭,因此 *Memory Cache* 是可用的,会被优先使用(如果匹配的话)。其次才是 *Disk Cache*。 + +- 强制刷新 ( *Ctrl + F5* ):浏览器不使用缓存,因此发送的请求头部均带有 *Cache-control: no-cache*(为了兼容,还带了 *Pragma: no-cache* )。服务器直接返回 *200* 和最新内容。 + + + +## 实操案例 + +实践才是检验真理的唯一标准。上面已经将理论部分讲解完毕了,接下来我们就来用实际代码验证一下上面所讲的验证规则。 + +下面是使用 *Node.js* 搭建的服务器: + +```js +const http = require('http'); +const path = require('path'); +const fs = require('fs'); + +var hashStr = "A hash string."; +var hash = require("crypto").createHash('sha1').update(hashStr).digest('base64'); + +http.createServer(function (req, res) { + const url = req.url; // 获取到请求的路径 + let fullPath; // 用于拼接完整的路径 + if (req.headers['if-none-match'] == hash) { + res.writeHead(304); + res.end(); + return; + } + if (url === '/') { + // 代表请求的是主页 + fullPath = path.join(__dirname, 'static/html') + '/index.html'; + } else { + fullPath = path.join(__dirname, "static", url); + res.writeHead(200, { + 'Cache-Control': 'max-age=5', + "Etag": hash + }); + } + // 根据完整的路径 使用fs模块来进行文件内容的读取 读取内容后将内容返回 + fs.readFile(fullPath, function (err, data) { + if (err) { + res.end(err.message); + } else { + // 读取文件成功,返回读取的内容,让浏览器进行解析 + res.end(data); + } + }); +}).listen(3000, function () { + console.log("服务器已启动,监听 3000 端口..."); +}) +``` + +在上面的代码中,我们使用 *Node.js* 创建了一个服务器,根据请求头的 *if-none-match* 字段接收从客户端传递过来的 *Etag* 值,如果和当前的 *Hash* 值相同,则返回 *304* 的状态码。 + +在资源方面,我们除了主页没有设置缓存,其他静态资源我们设置了 *5* 秒的缓存,并且设置了 *Etag* 值。 + +>注:上面的代码只是服务器部分代码,完整代码请参阅本章节所对应的代码。 + +效果如下: + +![2021-12-03 14.02.26](https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2021-12-03-063950.gif) + + + +可以看到,第一次请求时因为没有缓存,所以全部都是从服务器上面获取资源,之后我们刷新页面,是从 *memory cache* 中获取的资源,但是由于我们的强缓存只设置了 *5* 秒,所以之后再次刷新页面,走的就是协商缓存,返回 *304* 状态码。 + +但是在这个示例中,如果我们修改了服务器的静态资源,客户端是没办法实时的更新的,因为静态资源是直接返回的文件,只要静态资源的文件名没变,即使该资源的内容已经发生了变化,服务器也会认为资源没有变化。 + +那怎么解决呢? + +解决办法也就是我们在做静态资源构建时,在打包完成的静态资源文件名上根据它内容 *Hash* 值添加上一串 *Hash* 码,这样在 *CSS* 或者 *JS* 文件内容没有变化时,生成的文件名也就没有变化,反映到页面上的话就是 *url* 没有变化。 + +如果你的文件内容有变化,那么对应生成的文件名后面的 *Hash* 值也会发生变化,那么嵌入到页面的文件 *url* 也就会发生变化,从而可以达到一个更新缓存的目的。这也是为什么在使用 *webpack* 等一些打包工具时,打包后的文件名后面会添加上一串 *Hash* 码的原因。 + +目前来讲,这在前端开发中比较常见的一个静态资源缓存方案。 + + + +## 缓存的最佳实践 + + + +### 频繁变动的资源 + +``` +Cache-Control: no-cache +``` + +对于频繁变动的资源,首先需要使用 *Cache-Control: no-cache* 使浏览器每次都请求服务器,然后配合 *ETag* 或者 *Last-Modified* 来验证资源是否有效。 + +这样的做法虽然不能节省请求数量,但是能显著减少响应数据大小。 + +### 不常变化的资源 + +``` +Cache-Control: max-age=31536000 +``` + +通常在处理这类资源时,给它们的 *Cache-Control* 配置一个很大的 *max-age=31536000* (一年),这样浏览器之后请求相同的 *URL* 会命中强制缓存。 + +而为了解决更新的问题,就需要在文件名(或者路径)中添加 *Hash*, 版本号等动态字符,之后更改动态字符,从而达到更改引用 *URL* 的目的,让之前的强制缓存失效 (其实并未立即失效,只是不再使用了而已)。 + +在线提供的类库(如 *jquery-3.3.1.min.js、lodash.min.js* 等)均采用这个模式。 + + + + +------- + + + +-*EOF*- + diff --git a/05. 浏览器的缓存/浏览器缓存Demo/.vscode/settings.json b/05. 浏览器的缓存/浏览器缓存Demo/.vscode/settings.json new file mode 100644 index 0000000..5165b7c --- /dev/null +++ b/05. 浏览器的缓存/浏览器缓存Demo/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "eggHelper.serverPort": 35684 +} \ No newline at end of file diff --git a/05. 浏览器的缓存/浏览器缓存Demo/server.js b/05. 浏览器的缓存/浏览器缓存Demo/server.js new file mode 100644 index 0000000..98ace68 --- /dev/null +++ b/05. 浏览器的缓存/浏览器缓存Demo/server.js @@ -0,0 +1,37 @@ +const http = require('http'); +const path = require('path'); +const fs = require('fs'); + +var hashStr = "A hash string."; +var hash = require("crypto").createHash('sha1').update(hashStr).digest('base64'); + +http.createServer(function (req, res) { + const url = req.url; // 获取到请求的路径 + let fullPath; // 用于拼接完整的路径 + if (req.headers['if-none-match'] == hash) { + res.writeHead(304); + res.end(); + return; + } + if (url === '/') { + // 代表请求的是主页 + fullPath = path.join(__dirname, 'static/html') + '/index.html'; + } else { + fullPath = path.join(__dirname, "static", url); + res.writeHead(200, { + 'Cache-Control': 'max-age=10', + "Etag": hash + }); + } + // 根据完整的路径 使用fs模块来进行文件内容的读取 读取内容后将内容返回 + fs.readFile(fullPath, function (err, data) { + if (err) { + res.end(err.message); + } else { + // 读取文件成功,返回读取的内容,让浏览器进行解析 + res.end(data); + } + }); +}).listen(3000, function () { + console.log("服务器已启动,监听 3000 端口..."); +}) \ No newline at end of file diff --git a/05. 浏览器的缓存/浏览器缓存Demo/static/css/index.css b/05. 浏览器的缓存/浏览器缓存Demo/static/css/index.css new file mode 100644 index 0000000..08daea3 --- /dev/null +++ b/05. 浏览器的缓存/浏览器缓存Demo/static/css/index.css @@ -0,0 +1,25 @@ +.container{ + width: 800px; + margin: 50px auto; + padding: 10px; + font-weight: 100; +} + +h1{ + text-align: center; + font-weight: 100; + color : red; +} + +img{ + width: 150px; +} + +img:first-of-type{ + float: left; + margin-right: 20px; +} + +img:last-of-type{ + float: right; +} \ No newline at end of file diff --git a/05. 浏览器的缓存/浏览器缓存Demo/static/html/index.html b/05. 浏览器的缓存/浏览器缓存Demo/static/html/index.html new file mode 100644 index 0000000..6a81aa2 --- /dev/null +++ b/05. 浏览器的缓存/浏览器缓存Demo/static/html/index.html @@ -0,0 +1,29 @@ + + + + + + + + Document + + + + +
+

这是一个测试页面

+ + +

Lorem ipsum dolor, sit amet consectetur adipisicing elit. Tempore quod inventore recusandae? Reiciendis, + vitae dolorem voluptatem sint itaque deleniti. Asperiores laudantium dignissimos commodi et incidunt + suscipit qui nemo? Dolorum consectetur pariatur non porro quod, delectus saepe ullam quis ea labore dolorem, + totam corporis unde! Laborum quia quibusdam sapiente ex, voluptatem suscipit ipsam voluptas beatae quos + optio culpa nulla assumenda iste dicta, tempora placeat id hic? Temporibus ducimus qui quaerat accusantium + nisi rem vel eligendi eaque cumque doloribus assumenda quod quisquam quos ipsam suscipit, labore + perspiciatis neque quis! Quam quos vero labore non porro in minima deleniti architecto sint, recusandae + assumenda?

+
+ + + + \ No newline at end of file diff --git a/05. 浏览器的缓存/浏览器缓存Demo/static/img/ok.png b/05. 浏览器的缓存/浏览器缓存Demo/static/img/ok.png new file mode 100644 index 0000000000000000000000000000000000000000..004e2e6b790bf7904406d8d126c471fcad69fe47 GIT binary patch literal 162319 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 literal 0 HcmV?d00001 diff --git a/05. 浏览器的缓存/浏览器缓存Demo/static/js/index.js b/05. 浏览器的缓存/浏览器缓存Demo/static/js/index.js new file mode 100644 index 0000000..67abdd0 --- /dev/null +++ b/05. 浏览器的缓存/浏览器缓存Demo/static/js/index.js @@ -0,0 +1,5 @@ +const h1 = document.querySelector("h1"); + +h1.onclick = function(){ + window.alert("这是一个标题"); +} \ No newline at end of file diff --git a/06. 跨标签页通信/BroadCast实现跨标签页通信/.vscode/settings.json b/06. 跨标签页通信/BroadCast实现跨标签页通信/.vscode/settings.json new file mode 100644 index 0000000..5165b7c --- /dev/null +++ b/06. 跨标签页通信/BroadCast实现跨标签页通信/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "eggHelper.serverPort": 35684 +} \ No newline at end of file diff --git a/06. 跨标签页通信/BroadCast实现跨标签页通信/index.html b/06. 跨标签页通信/BroadCast实现跨标签页通信/index.html new file mode 100644 index 0000000..50c47f0 --- /dev/null +++ b/06. 跨标签页通信/BroadCast实现跨标签页通信/index.html @@ -0,0 +1,26 @@ + + + + + + + 页面一 + + + + + + + \ No newline at end of file diff --git a/06. 跨标签页通信/BroadCast实现跨标签页通信/index2.html b/06. 跨标签页通信/BroadCast实现跨标签页通信/index2.html new file mode 100644 index 0000000..20b0405 --- /dev/null +++ b/06. 跨标签页通信/BroadCast实现跨标签页通信/index2.html @@ -0,0 +1,22 @@ + + + + + + + + 页面二 + + + + + + + \ No newline at end of file diff --git a/06. 跨标签页通信/IndexedDB实现跨标签页通信/.vscode/settings.json b/06. 跨标签页通信/IndexedDB实现跨标签页通信/.vscode/settings.json new file mode 100644 index 0000000..5165b7c --- /dev/null +++ b/06. 跨标签页通信/IndexedDB实现跨标签页通信/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "eggHelper.serverPort": 35684 +} \ No newline at end of file diff --git a/06. 跨标签页通信/IndexedDB实现跨标签页通信/db.js b/06. 跨标签页通信/IndexedDB实现跨标签页通信/db.js new file mode 100644 index 0000000..a9c3251 --- /dev/null +++ b/06. 跨标签页通信/IndexedDB实现跨标签页通信/db.js @@ -0,0 +1,87 @@ +/** + * 打开数据库 + * @param {object} dbName 数据库的名字 + * @param {string} storeName 仓库名称 + * @param {string} version 数据库的版本 + * @return {object} 该函数会返回一个数据库实例 + */ +function openDB(dbName, version = 1) { + return new Promise((resolve, reject) => { + var db; // 存储创建的数据库 + // 打开数据库,若没有则会创建 + const request = indexedDB.open(dbName, version); + + // 数据库打开成功回调 + request.onsuccess = function (event) { + db = event.target.result; // 存储数据库对象 + console.log("数据库打开成功"); + resolve(db); + }; + + // 数据库打开失败的回调 + request.onerror = function (event) { + console.log("数据库打开报错"); + }; + + // 数据库有更新时候的回调 + request.onupgradeneeded = function (event) { + // 数据库创建或升级的时候会触发 + console.log("onupgradeneeded"); + db = event.target.result; // 存储数据库对象 + var objectStore; + // 创建存储库 + objectStore = db.createObjectStore("stu", { + keyPath: "stuId", // 这是主键 + autoIncrement: true // 实现自增 + }); + // 创建索引,在后面查询数据的时候可以根据索引查 + objectStore.createIndex("stuId", "stuId", { unique: true }); + objectStore.createIndex("stuName", "stuName", { unique: false }); + objectStore.createIndex("stuAge", "stuAge", { unique: false }); + }; + }); +} + +/** + * 新增数据 + * @param {object} db 数据库实例 + * @param {string} storeName 仓库名称 + * @param {string} data 数据 + */ +function addData(db, storeName, data) { + var request = db + .transaction([storeName], "readwrite") // 事务对象 指定表格名称和操作模式("只读"或"读写") + .objectStore(storeName) // 仓库对象 + .add(data); + + request.onsuccess = function (event) { + console.log("数据写入成功"); + }; + + request.onerror = function (event) { + console.log("数据写入失败"); + }; +} + +/** + * 通过主键读取数据 + * @param {object} db 数据库实例 + * @param {string} storeName 仓库名称 + * @param {string} key 主键值 + */ +function getDataByKey(db, storeName, key) { + return new Promise((resolve, reject) => { + var transaction = db.transaction([storeName]); // 事务 + var objectStore = transaction.objectStore(storeName); // 仓库对象 + var request = objectStore.getAll(); // 通过主键获取数据 + + request.onerror = function (event) { + console.log("事务失败"); + }; + + request.onsuccess = function (event) { + // console.log("主键查询结果: ", request.result); + resolve(request.result); + }; + }); +} \ No newline at end of file diff --git a/06. 跨标签页通信/IndexedDB实现跨标签页通信/index.html b/06. 跨标签页通信/IndexedDB实现跨标签页通信/index.html new file mode 100644 index 0000000..0eb0725 --- /dev/null +++ b/06. 跨标签页通信/IndexedDB实现跨标签页通信/index.html @@ -0,0 +1,46 @@ + + + + + + + + 页面一 + + + +

新增学生

+
+ 学生学号: + +
+
+ 学生姓名: + +
+
+ 学生年龄: + +
+ + + + + + \ No newline at end of file diff --git a/06. 跨标签页通信/IndexedDB实现跨标签页通信/index2.html b/06. 跨标签页通信/IndexedDB实现跨标签页通信/index2.html new file mode 100644 index 0000000..294bfbf --- /dev/null +++ b/06. 跨标签页通信/IndexedDB实现跨标签页通信/index2.html @@ -0,0 +1,80 @@ + + + + + + + + 页面二 + + + + +

学生表

+ + +
+ + + + + \ No newline at end of file diff --git a/06. 跨标签页通信/ServiceWorker实现跨标签页通信/.vscode/settings.json b/06. 跨标签页通信/ServiceWorker实现跨标签页通信/.vscode/settings.json new file mode 100644 index 0000000..5165b7c --- /dev/null +++ b/06. 跨标签页通信/ServiceWorker实现跨标签页通信/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "eggHelper.serverPort": 35684 +} \ No newline at end of file diff --git a/06. 跨标签页通信/ServiceWorker实现跨标签页通信/index.html b/06. 跨标签页通信/ServiceWorker实现跨标签页通信/index.html new file mode 100644 index 0000000..733f01c --- /dev/null +++ b/06. 跨标签页通信/ServiceWorker实现跨标签页通信/index.html @@ -0,0 +1,31 @@ + + + + + + + + 页面一 + + + + + + + + + \ No newline at end of file diff --git a/06. 跨标签页通信/ServiceWorker实现跨标签页通信/index2.html b/06. 跨标签页通信/ServiceWorker实现跨标签页通信/index2.html new file mode 100644 index 0000000..3e93383 --- /dev/null +++ b/06. 跨标签页通信/ServiceWorker实现跨标签页通信/index2.html @@ -0,0 +1,21 @@ + + + + + + + 页面二 + + + + + \ No newline at end of file diff --git a/06. 跨标签页通信/ServiceWorker实现跨标签页通信/sw.js b/06. 跨标签页通信/ServiceWorker实现跨标签页通信/sw.js new file mode 100644 index 0000000..ee39b1d --- /dev/null +++ b/06. 跨标签页通信/ServiceWorker实现跨标签页通信/sw.js @@ -0,0 +1,8 @@ +// 消息就会到达这里 +self.addEventListener("message", async event=>{ + // 首先获取到所有注册了 service worker 的客户端 + const clients = await self.clients.matchAll(); + clients.forEach(function(client){ + client.postMessage(event.data.value); + }) +}) \ No newline at end of file diff --git a/06. 跨标签页通信/SharedWorker实现跨标签页通信/.vscode/settings.json b/06. 跨标签页通信/SharedWorker实现跨标签页通信/.vscode/settings.json new file mode 100644 index 0000000..5165b7c --- /dev/null +++ b/06. 跨标签页通信/SharedWorker实现跨标签页通信/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "eggHelper.serverPort": 35684 +} \ No newline at end of file diff --git a/06. 跨标签页通信/SharedWorker实现跨标签页通信/index.html b/06. 跨标签页通信/SharedWorker实现跨标签页通信/index.html new file mode 100644 index 0000000..9cb0afb --- /dev/null +++ b/06. 跨标签页通信/SharedWorker实现跨标签页通信/index.html @@ -0,0 +1,27 @@ + + + + + + + + 页面一 + + + + + + + + + \ No newline at end of file diff --git a/06. 跨标签页通信/SharedWorker实现跨标签页通信/index2.html b/06. 跨标签页通信/SharedWorker实现跨标签页通信/index2.html new file mode 100644 index 0000000..e275f93 --- /dev/null +++ b/06. 跨标签页通信/SharedWorker实现跨标签页通信/index2.html @@ -0,0 +1,26 @@ + + + + + + + + 页面二 + + + + + + + \ No newline at end of file diff --git a/06. 跨标签页通信/SharedWorker实现跨标签页通信/worker.js b/06. 跨标签页通信/SharedWorker实现跨标签页通信/worker.js new file mode 100644 index 0000000..f0751a0 --- /dev/null +++ b/06. 跨标签页通信/SharedWorker实现跨标签页通信/worker.js @@ -0,0 +1,15 @@ +var data = ""; // 存储用户发送过来的信息 +onconnect = function (e) { + var port = e.ports[0]; + + port.onmessage = function (e) { + // 说明要将接收到数据返回给客户端 + if(e.data === "get"){ + port.postMessage(data); + data = ""; + } else { + data = e.data; + } + } + +} \ No newline at end of file diff --git a/06. 跨标签页通信/Websocket实现跨标签页通信/.vscode/settings.json b/06. 跨标签页通信/Websocket实现跨标签页通信/.vscode/settings.json new file mode 100644 index 0000000..5165b7c --- /dev/null +++ b/06. 跨标签页通信/Websocket实现跨标签页通信/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "eggHelper.serverPort": 35684 +} \ No newline at end of file diff --git a/06. 跨标签页通信/Websocket实现跨标签页通信/index.html b/06. 跨标签页通信/Websocket实现跨标签页通信/index.html new file mode 100644 index 0000000..84926f2 --- /dev/null +++ b/06. 跨标签页通信/Websocket实现跨标签页通信/index.html @@ -0,0 +1,29 @@ + + + + + + + 页面一 + + + + + + + \ No newline at end of file diff --git a/06. 跨标签页通信/Websocket实现跨标签页通信/index2.html b/06. 跨标签页通信/Websocket实现跨标签页通信/index2.html new file mode 100644 index 0000000..6711a58 --- /dev/null +++ b/06. 跨标签页通信/Websocket实现跨标签页通信/index2.html @@ -0,0 +1,32 @@ + + + + + + + + Document + + + + + + + \ No newline at end of file diff --git a/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/.package-lock.json b/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/.package-lock.json new file mode 100644 index 0000000..e786d33 --- /dev/null +++ b/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/.package-lock.json @@ -0,0 +1,28 @@ +{ + "name": "demo", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "node_modules/ws": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.3.0.tgz", + "integrity": "sha512-Gs5EZtpqZzLvmIM59w4igITU57lrtYVFneaa434VROv4thzJyV6UjIL3D42lslWlI+D4KzLYnxSwtfuiO79sNw==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/LICENSE b/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/LICENSE new file mode 100644 index 0000000..65ff176 --- /dev/null +++ b/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2011 Einar Otto Stangvik + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/README.md b/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/README.md new file mode 100644 index 0000000..82ca8db --- /dev/null +++ b/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/README.md @@ -0,0 +1,493 @@ +# ws: a Node.js WebSocket library + +[![Version npm](https://img.shields.io/npm/v/ws.svg?logo=npm)](https://www.npmjs.com/package/ws) +[![CI](https://img.shields.io/github/workflow/status/websockets/ws/CI/master?label=CI&logo=github)](https://github.com/websockets/ws/actions?query=workflow%3ACI+branch%3Amaster) +[![Coverage Status](https://img.shields.io/coveralls/websockets/ws/master.svg?logo=coveralls)](https://coveralls.io/github/websockets/ws) + +ws is a simple to use, blazing fast, and thoroughly tested WebSocket client and +server implementation. + +Passes the quite extensive Autobahn test suite: [server][server-report], +[client][client-report]. + +**Note**: This module does not work in the browser. The client in the docs is a +reference to a back end with the role of a client in the WebSocket +communication. Browser clients must use the native +[`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) +object. To make the same code work seamlessly on Node.js and the browser, you +can use one of the many wrappers available on npm, like +[isomorphic-ws](https://github.com/heineiuo/isomorphic-ws). + +## Table of Contents + +- [Protocol support](#protocol-support) +- [Installing](#installing) + - [Opt-in for performance](#opt-in-for-performance) +- [API docs](#api-docs) +- [WebSocket compression](#websocket-compression) +- [Usage examples](#usage-examples) + - [Sending and receiving text data](#sending-and-receiving-text-data) + - [Sending binary data](#sending-binary-data) + - [Simple server](#simple-server) + - [External HTTP/S server](#external-https-server) + - [Multiple servers sharing a single HTTP/S server](#multiple-servers-sharing-a-single-https-server) + - [Client authentication](#client-authentication) + - [Server broadcast](#server-broadcast) + - [echo.websocket.org demo](#echowebsocketorg-demo) + - [Use the Node.js streams API](#use-the-nodejs-streams-api) + - [Other examples](#other-examples) +- [FAQ](#faq) + - [How to get the IP address of the client?](#how-to-get-the-ip-address-of-the-client) + - [How to detect and close broken connections?](#how-to-detect-and-close-broken-connections) + - [How to connect via a proxy?](#how-to-connect-via-a-proxy) +- [Changelog](#changelog) +- [License](#license) + +## Protocol support + +- **HyBi drafts 07-12** (Use the option `protocolVersion: 8`) +- **HyBi drafts 13-17** (Current default, alternatively option + `protocolVersion: 13`) + +## Installing + +``` +npm install ws +``` + +### Opt-in for performance + +There are 2 optional modules that can be installed along side with the ws +module. These modules are binary addons which improve certain operations. +Prebuilt binaries are available for the most popular platforms so you don't +necessarily need to have a C++ compiler installed on your machine. + +- `npm install --save-optional bufferutil`: Allows to efficiently perform + operations such as masking and unmasking the data payload of the WebSocket + frames. +- `npm install --save-optional utf-8-validate`: Allows to efficiently check if a + message contains valid UTF-8. + +## API docs + +See [`/doc/ws.md`](./doc/ws.md) for Node.js-like documentation of ws classes and +utility functions. + +## WebSocket compression + +ws supports the [permessage-deflate extension][permessage-deflate] which enables +the client and server to negotiate a compression algorithm and its parameters, +and then selectively apply it to the data payloads of each WebSocket message. + +The extension is disabled by default on the server and enabled by default on the +client. It adds a significant overhead in terms of performance and memory +consumption so we suggest to enable it only if it is really needed. + +Note that Node.js has a variety of issues with high-performance compression, +where increased concurrency, especially on Linux, can lead to [catastrophic +memory fragmentation][node-zlib-bug] and slow performance. If you intend to use +permessage-deflate in production, it is worthwhile to set up a test +representative of your workload and ensure Node.js/zlib will handle it with +acceptable performance and memory usage. + +Tuning of permessage-deflate can be done via the options defined below. You can +also use `zlibDeflateOptions` and `zlibInflateOptions`, which is passed directly +into the creation of [raw deflate/inflate streams][node-zlib-deflaterawdocs]. + +See [the docs][ws-server-options] for more options. + +```js +import WebSocket, { WebSocketServer } from 'ws'; + +const wss = new WebSocketServer({ + port: 8080, + perMessageDeflate: { + zlibDeflateOptions: { + // See zlib defaults. + chunkSize: 1024, + memLevel: 7, + level: 3 + }, + zlibInflateOptions: { + chunkSize: 10 * 1024 + }, + // Other options settable: + clientNoContextTakeover: true, // Defaults to negotiated value. + serverNoContextTakeover: true, // Defaults to negotiated value. + serverMaxWindowBits: 10, // Defaults to negotiated value. + // Below options specified as default values. + concurrencyLimit: 10, // Limits zlib concurrency for perf. + threshold: 1024 // Size (in bytes) below which messages + // should not be compressed if context takeover is disabled. + } +}); +``` + +The client will only use the extension if it is supported and enabled on the +server. To always disable the extension on the client set the +`perMessageDeflate` option to `false`. + +```js +import WebSocket from 'ws'; + +const ws = new WebSocket('ws://www.host.com/path', { + perMessageDeflate: false +}); +``` + +## Usage examples + +### Sending and receiving text data + +```js +import WebSocket from 'ws'; + +const ws = new WebSocket('ws://www.host.com/path'); + +ws.on('open', function open() { + ws.send('something'); +}); + +ws.on('message', function message(data) { + console.log('received: %s', data); +}); +``` + +### Sending binary data + +```js +import WebSocket from 'ws'; + +const ws = new WebSocket('ws://www.host.com/path'); + +ws.on('open', function open() { + const array = new Float32Array(5); + + for (var i = 0; i < array.length; ++i) { + array[i] = i / 2; + } + + ws.send(array); +}); +``` + +### Simple server + +```js +import { WebSocketServer } from 'ws'; + +const wss = new WebSocketServer({ port: 8080 }); + +wss.on('connection', function connection(ws) { + ws.on('message', function message(data) { + console.log('received: %s', data); + }); + + ws.send('something'); +}); +``` + +### External HTTP/S server + +```js +import { createServer } from 'https'; +import { readFileSync } from 'fs'; +import { WebSocketServer } from 'ws'; + +const server = createServer({ + cert: readFileSync('/path/to/cert.pem'), + key: readFileSync('/path/to/key.pem') +}); +const wss = new WebSocketServer({ server }); + +wss.on('connection', function connection(ws) { + ws.on('message', function message(data) { + console.log('received: %s', data); + }); + + ws.send('something'); +}); + +server.listen(8080); +``` + +### Multiple servers sharing a single HTTP/S server + +```js +import { createServer } from 'http'; +import { parse } from 'url'; +import { WebSocketServer } from 'ws'; + +const server = createServer(); +const wss1 = new WebSocketServer({ noServer: true }); +const wss2 = new WebSocketServer({ noServer: true }); + +wss1.on('connection', function connection(ws) { + // ... +}); + +wss2.on('connection', function connection(ws) { + // ... +}); + +server.on('upgrade', function upgrade(request, socket, head) { + const { pathname } = parse(request.url); + + if (pathname === '/foo') { + wss1.handleUpgrade(request, socket, head, function done(ws) { + wss1.emit('connection', ws, request); + }); + } else if (pathname === '/bar') { + wss2.handleUpgrade(request, socket, head, function done(ws) { + wss2.emit('connection', ws, request); + }); + } else { + socket.destroy(); + } +}); + +server.listen(8080); +``` + +### Client authentication + +```js +import WebSocket from 'ws'; +import { createServer } from 'http'; + +const server = createServer(); +const wss = new WebSocketServer({ noServer: true }); + +wss.on('connection', function connection(ws, request, client) { + ws.on('message', function message(data) { + console.log(`Received message ${data} from user ${client}`); + }); +}); + +server.on('upgrade', function upgrade(request, socket, head) { + // This function is not defined on purpose. Implement it with your own logic. + authenticate(request, function next(err, client) { + if (err || !client) { + socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n'); + socket.destroy(); + return; + } + + wss.handleUpgrade(request, socket, head, function done(ws) { + wss.emit('connection', ws, request, client); + }); + }); +}); + +server.listen(8080); +``` + +Also see the provided [example][session-parse-example] using `express-session`. + +### Server broadcast + +A client WebSocket broadcasting to all connected WebSocket clients, including +itself. + +```js +import WebSocket, { WebSocketServer } from 'ws'; + +const wss = new WebSocketServer({ port: 8080 }); + +wss.on('connection', function connection(ws) { + ws.on('message', function message(data, isBinary) { + wss.clients.forEach(function each(client) { + if (client.readyState === WebSocket.OPEN) { + client.send(data, { binary: isBinary }); + } + }); + }); +}); +``` + +A client WebSocket broadcasting to every other connected WebSocket clients, +excluding itself. + +```js +import WebSocket, { WebSocketServer } from 'ws'; + +const wss = new WebSocketServer({ port: 8080 }); + +wss.on('connection', function connection(ws) { + ws.on('message', function message(data, isBinary) { + wss.clients.forEach(function each(client) { + if (client !== ws && client.readyState === WebSocket.OPEN) { + client.send(data, { binary: isBinary }); + } + }); + }); +}); +``` + +### echo.websocket.org demo + +```js +import WebSocket from 'ws'; + +const ws = new WebSocket('wss://echo.websocket.org/', { + origin: 'https://websocket.org' +}); + +ws.on('open', function open() { + console.log('connected'); + ws.send(Date.now()); +}); + +ws.on('close', function close() { + console.log('disconnected'); +}); + +ws.on('message', function message(data) { + console.log(`Roundtrip time: ${Date.now() - data} ms`); + + setTimeout(function timeout() { + ws.send(Date.now()); + }, 500); +}); +``` + +### Use the Node.js streams API + +```js +import WebSocket, { createWebSocketStream } from 'ws'; + +const ws = new WebSocket('wss://echo.websocket.org/', { + origin: 'https://websocket.org' +}); + +const duplex = createWebSocketStream(ws, { encoding: 'utf8' }); + +duplex.pipe(process.stdout); +process.stdin.pipe(duplex); +``` + +### Other examples + +For a full example with a browser client communicating with a ws server, see the +examples folder. + +Otherwise, see the test cases. + +## FAQ + +### How to get the IP address of the client? + +The remote IP address can be obtained from the raw socket. + +```js +import { WebSocketServer } from 'ws'; + +const wss = new WebSocketServer({ port: 8080 }); + +wss.on('connection', function connection(ws, req) { + const ip = req.socket.remoteAddress; +}); +``` + +When the server runs behind a proxy like NGINX, the de-facto standard is to use +the `X-Forwarded-For` header. + +```js +wss.on('connection', function connection(ws, req) { + const ip = req.headers['x-forwarded-for'].split(',')[0].trim(); +}); +``` + +### How to detect and close broken connections? + +Sometimes the link between the server and the client can be interrupted in a way +that keeps both the server and the client unaware of the broken state of the +connection (e.g. when pulling the cord). + +In these cases ping messages can be used as a means to verify that the remote +endpoint is still responsive. + +```js +import { WebSocketServer } from 'ws'; + +function heartbeat() { + this.isAlive = true; +} + +const wss = new WebSocketServer({ port: 8080 }); + +wss.on('connection', function connection(ws) { + ws.isAlive = true; + ws.on('pong', heartbeat); +}); + +const interval = setInterval(function ping() { + wss.clients.forEach(function each(ws) { + if (ws.isAlive === false) return ws.terminate(); + + ws.isAlive = false; + ws.ping(); + }); +}, 30000); + +wss.on('close', function close() { + clearInterval(interval); +}); +``` + +Pong messages are automatically sent in response to ping messages as required by +the spec. + +Just like the server example above your clients might as well lose connection +without knowing it. You might want to add a ping listener on your clients to +prevent that. A simple implementation would be: + +```js +import WebSocket from 'ws'; + +function heartbeat() { + clearTimeout(this.pingTimeout); + + // Use `WebSocket#terminate()`, which immediately destroys the connection, + // instead of `WebSocket#close()`, which waits for the close timer. + // Delay should be equal to the interval at which your server + // sends out pings plus a conservative assumption of the latency. + this.pingTimeout = setTimeout(() => { + this.terminate(); + }, 30000 + 1000); +} + +const client = new WebSocket('wss://echo.websocket.org/'); + +client.on('open', heartbeat); +client.on('ping', heartbeat); +client.on('close', function clear() { + clearTimeout(this.pingTimeout); +}); +``` + +### How to connect via a proxy? + +Use a custom `http.Agent` implementation like [https-proxy-agent][] or +[socks-proxy-agent][]. + +## Changelog + +We're using the GitHub [releases][changelog] for changelog entries. + +## License + +[MIT](LICENSE) + +[changelog]: https://github.com/websockets/ws/releases +[client-report]: http://websockets.github.io/ws/autobahn/clients/ +[https-proxy-agent]: https://github.com/TooTallNate/node-https-proxy-agent +[node-zlib-bug]: https://github.com/nodejs/node/issues/8871 +[node-zlib-deflaterawdocs]: + https://nodejs.org/api/zlib.html#zlib_zlib_createdeflateraw_options +[permessage-deflate]: https://tools.ietf.org/html/rfc7692 +[server-report]: http://websockets.github.io/ws/autobahn/servers/ +[session-parse-example]: ./examples/express-session-parse +[socks-proxy-agent]: https://github.com/TooTallNate/node-socks-proxy-agent +[ws-server-options]: + https://github.com/websockets/ws/blob/master/doc/ws.md#new-websocketserveroptions-callback diff --git a/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/browser.js b/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/browser.js new file mode 100644 index 0000000..ca4f628 --- /dev/null +++ b/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/browser.js @@ -0,0 +1,8 @@ +'use strict'; + +module.exports = function () { + throw new Error( + 'ws does not work in the browser. Browser clients must use the native ' + + 'WebSocket object' + ); +}; diff --git a/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/index.js b/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/index.js new file mode 100644 index 0000000..41edb3b --- /dev/null +++ b/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/index.js @@ -0,0 +1,13 @@ +'use strict'; + +const WebSocket = require('./lib/websocket'); + +WebSocket.createWebSocketStream = require('./lib/stream'); +WebSocket.Server = require('./lib/websocket-server'); +WebSocket.Receiver = require('./lib/receiver'); +WebSocket.Sender = require('./lib/sender'); + +WebSocket.WebSocket = WebSocket; +WebSocket.WebSocketServer = WebSocket.Server; + +module.exports = WebSocket; diff --git a/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/lib/buffer-util.js b/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/lib/buffer-util.js new file mode 100644 index 0000000..1ba1d1b --- /dev/null +++ b/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/lib/buffer-util.js @@ -0,0 +1,126 @@ +'use strict'; + +const { EMPTY_BUFFER } = require('./constants'); + +/** + * Merges an array of buffers into a new buffer. + * + * @param {Buffer[]} list The array of buffers to concat + * @param {Number} totalLength The total length of buffers in the list + * @return {Buffer} The resulting buffer + * @public + */ +function concat(list, totalLength) { + if (list.length === 0) return EMPTY_BUFFER; + if (list.length === 1) return list[0]; + + const target = Buffer.allocUnsafe(totalLength); + let offset = 0; + + for (let i = 0; i < list.length; i++) { + const buf = list[i]; + target.set(buf, offset); + offset += buf.length; + } + + if (offset < totalLength) return target.slice(0, offset); + + return target; +} + +/** + * Masks a buffer using the given mask. + * + * @param {Buffer} source The buffer to mask + * @param {Buffer} mask The mask to use + * @param {Buffer} output The buffer where to store the result + * @param {Number} offset The offset at which to start writing + * @param {Number} length The number of bytes to mask. + * @public + */ +function _mask(source, mask, output, offset, length) { + for (let i = 0; i < length; i++) { + output[offset + i] = source[i] ^ mask[i & 3]; + } +} + +/** + * Unmasks a buffer using the given mask. + * + * @param {Buffer} buffer The buffer to unmask + * @param {Buffer} mask The mask to use + * @public + */ +function _unmask(buffer, mask) { + for (let i = 0; i < buffer.length; i++) { + buffer[i] ^= mask[i & 3]; + } +} + +/** + * Converts a buffer to an `ArrayBuffer`. + * + * @param {Buffer} buf The buffer to convert + * @return {ArrayBuffer} Converted buffer + * @public + */ +function toArrayBuffer(buf) { + if (buf.byteLength === buf.buffer.byteLength) { + return buf.buffer; + } + + return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); +} + +/** + * Converts `data` to a `Buffer`. + * + * @param {*} data The data to convert + * @return {Buffer} The buffer + * @throws {TypeError} + * @public + */ +function toBuffer(data) { + toBuffer.readOnly = true; + + if (Buffer.isBuffer(data)) return data; + + let buf; + + if (data instanceof ArrayBuffer) { + buf = Buffer.from(data); + } else if (ArrayBuffer.isView(data)) { + buf = Buffer.from(data.buffer, data.byteOffset, data.byteLength); + } else { + buf = Buffer.from(data); + toBuffer.readOnly = false; + } + + return buf; +} + +try { + const bufferUtil = require('bufferutil'); + + module.exports = { + concat, + mask(source, mask, output, offset, length) { + if (length < 48) _mask(source, mask, output, offset, length); + else bufferUtil.mask(source, mask, output, offset, length); + }, + toArrayBuffer, + toBuffer, + unmask(buffer, mask) { + if (buffer.length < 32) _unmask(buffer, mask); + else bufferUtil.unmask(buffer, mask); + } + }; +} catch (e) /* istanbul ignore next */ { + module.exports = { + concat, + mask: _mask, + toArrayBuffer, + toBuffer, + unmask: _unmask + }; +} diff --git a/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/lib/constants.js b/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/lib/constants.js new file mode 100644 index 0000000..d691b30 --- /dev/null +++ b/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/lib/constants.js @@ -0,0 +1,12 @@ +'use strict'; + +module.exports = { + BINARY_TYPES: ['nodebuffer', 'arraybuffer', 'fragments'], + EMPTY_BUFFER: Buffer.alloc(0), + GUID: '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', + kForOnEventAttribute: Symbol('kIsForOnEventAttribute'), + kListener: Symbol('kListener'), + kStatusCode: Symbol('status-code'), + kWebSocket: Symbol('websocket'), + NOOP: () => {} +}; diff --git a/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/lib/event-target.js b/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/lib/event-target.js new file mode 100644 index 0000000..d5abd83 --- /dev/null +++ b/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/lib/event-target.js @@ -0,0 +1,266 @@ +'use strict'; + +const { kForOnEventAttribute, kListener } = require('./constants'); + +const kCode = Symbol('kCode'); +const kData = Symbol('kData'); +const kError = Symbol('kError'); +const kMessage = Symbol('kMessage'); +const kReason = Symbol('kReason'); +const kTarget = Symbol('kTarget'); +const kType = Symbol('kType'); +const kWasClean = Symbol('kWasClean'); + +/** + * Class representing an event. + */ +class Event { + /** + * Create a new `Event`. + * + * @param {String} type The name of the event + * @throws {TypeError} If the `type` argument is not specified + */ + constructor(type) { + this[kTarget] = null; + this[kType] = type; + } + + /** + * @type {*} + */ + get target() { + return this[kTarget]; + } + + /** + * @type {String} + */ + get type() { + return this[kType]; + } +} + +Object.defineProperty(Event.prototype, 'target', { enumerable: true }); +Object.defineProperty(Event.prototype, 'type', { enumerable: true }); + +/** + * Class representing a close event. + * + * @extends Event + */ +class CloseEvent extends Event { + /** + * Create a new `CloseEvent`. + * + * @param {String} type The name of the event + * @param {Object} [options] A dictionary object that allows for setting + * attributes via object members of the same name + * @param {Number} [options.code=0] The status code explaining why the + * connection was closed + * @param {String} [options.reason=''] A human-readable string explaining why + * the connection was closed + * @param {Boolean} [options.wasClean=false] Indicates whether or not the + * connection was cleanly closed + */ + constructor(type, options = {}) { + super(type); + + this[kCode] = options.code === undefined ? 0 : options.code; + this[kReason] = options.reason === undefined ? '' : options.reason; + this[kWasClean] = options.wasClean === undefined ? false : options.wasClean; + } + + /** + * @type {Number} + */ + get code() { + return this[kCode]; + } + + /** + * @type {String} + */ + get reason() { + return this[kReason]; + } + + /** + * @type {Boolean} + */ + get wasClean() { + return this[kWasClean]; + } +} + +Object.defineProperty(CloseEvent.prototype, 'code', { enumerable: true }); +Object.defineProperty(CloseEvent.prototype, 'reason', { enumerable: true }); +Object.defineProperty(CloseEvent.prototype, 'wasClean', { enumerable: true }); + +/** + * Class representing an error event. + * + * @extends Event + */ +class ErrorEvent extends Event { + /** + * Create a new `ErrorEvent`. + * + * @param {String} type The name of the event + * @param {Object} [options] A dictionary object that allows for setting + * attributes via object members of the same name + * @param {*} [options.error=null] The error that generated this event + * @param {String} [options.message=''] The error message + */ + constructor(type, options = {}) { + super(type); + + this[kError] = options.error === undefined ? null : options.error; + this[kMessage] = options.message === undefined ? '' : options.message; + } + + /** + * @type {*} + */ + get error() { + return this[kError]; + } + + /** + * @type {String} + */ + get message() { + return this[kMessage]; + } +} + +Object.defineProperty(ErrorEvent.prototype, 'error', { enumerable: true }); +Object.defineProperty(ErrorEvent.prototype, 'message', { enumerable: true }); + +/** + * Class representing a message event. + * + * @extends Event + */ +class MessageEvent extends Event { + /** + * Create a new `MessageEvent`. + * + * @param {String} type The name of the event + * @param {Object} [options] A dictionary object that allows for setting + * attributes via object members of the same name + * @param {*} [options.data=null] The message content + */ + constructor(type, options = {}) { + super(type); + + this[kData] = options.data === undefined ? null : options.data; + } + + /** + * @type {*} + */ + get data() { + return this[kData]; + } +} + +Object.defineProperty(MessageEvent.prototype, 'data', { enumerable: true }); + +/** + * This provides methods for emulating the `EventTarget` interface. It's not + * meant to be used directly. + * + * @mixin + */ +const EventTarget = { + /** + * Register an event listener. + * + * @param {String} type A string representing the event type to listen for + * @param {Function} listener The listener to add + * @param {Object} [options] An options object specifies characteristics about + * the event listener + * @param {Boolean} [options.once=false] A `Boolean` indicating that the + * listener should be invoked at most once after being added. If `true`, + * the listener would be automatically removed when invoked. + * @public + */ + addEventListener(type, listener, options = {}) { + let wrapper; + + if (type === 'message') { + wrapper = function onMessage(data, isBinary) { + const event = new MessageEvent('message', { + data: isBinary ? data : data.toString() + }); + + event[kTarget] = this; + listener.call(this, event); + }; + } else if (type === 'close') { + wrapper = function onClose(code, message) { + const event = new CloseEvent('close', { + code, + reason: message.toString(), + wasClean: this._closeFrameReceived && this._closeFrameSent + }); + + event[kTarget] = this; + listener.call(this, event); + }; + } else if (type === 'error') { + wrapper = function onError(error) { + const event = new ErrorEvent('error', { + error, + message: error.message + }); + + event[kTarget] = this; + listener.call(this, event); + }; + } else if (type === 'open') { + wrapper = function onOpen() { + const event = new Event('open'); + + event[kTarget] = this; + listener.call(this, event); + }; + } else { + return; + } + + wrapper[kForOnEventAttribute] = !!options[kForOnEventAttribute]; + wrapper[kListener] = listener; + + if (options.once) { + this.once(type, wrapper); + } else { + this.on(type, wrapper); + } + }, + + /** + * Remove an event listener. + * + * @param {String} type A string representing the event type to remove + * @param {Function} handler The listener to remove + * @public + */ + removeEventListener(type, handler) { + for (const listener of this.listeners(type)) { + if (listener[kListener] === handler && !listener[kForOnEventAttribute]) { + this.removeListener(type, listener); + break; + } + } + } +}; + +module.exports = { + CloseEvent, + ErrorEvent, + Event, + EventTarget, + MessageEvent +}; diff --git a/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/lib/extension.js b/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/lib/extension.js new file mode 100644 index 0000000..3d7895c --- /dev/null +++ b/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/lib/extension.js @@ -0,0 +1,203 @@ +'use strict'; + +const { tokenChars } = require('./validation'); + +/** + * Adds an offer to the map of extension offers or a parameter to the map of + * parameters. + * + * @param {Object} dest The map of extension offers or parameters + * @param {String} name The extension or parameter name + * @param {(Object|Boolean|String)} elem The extension parameters or the + * parameter value + * @private + */ +function push(dest, name, elem) { + if (dest[name] === undefined) dest[name] = [elem]; + else dest[name].push(elem); +} + +/** + * Parses the `Sec-WebSocket-Extensions` header into an object. + * + * @param {String} header The field value of the header + * @return {Object} The parsed object + * @public + */ +function parse(header) { + const offers = Object.create(null); + let params = Object.create(null); + let mustUnescape = false; + let isEscaping = false; + let inQuotes = false; + let extensionName; + let paramName; + let start = -1; + let code = -1; + let end = -1; + let i = 0; + + for (; i < header.length; i++) { + code = header.charCodeAt(i); + + if (extensionName === undefined) { + if (end === -1 && tokenChars[code] === 1) { + if (start === -1) start = i; + } else if ( + i !== 0 && + (code === 0x20 /* ' ' */ || code === 0x09) /* '\t' */ + ) { + if (end === -1 && start !== -1) end = i; + } else if (code === 0x3b /* ';' */ || code === 0x2c /* ',' */) { + if (start === -1) { + throw new SyntaxError(`Unexpected character at index ${i}`); + } + + if (end === -1) end = i; + const name = header.slice(start, end); + if (code === 0x2c) { + push(offers, name, params); + params = Object.create(null); + } else { + extensionName = name; + } + + start = end = -1; + } else { + throw new SyntaxError(`Unexpected character at index ${i}`); + } + } else if (paramName === undefined) { + if (end === -1 && tokenChars[code] === 1) { + if (start === -1) start = i; + } else if (code === 0x20 || code === 0x09) { + if (end === -1 && start !== -1) end = i; + } else if (code === 0x3b || code === 0x2c) { + if (start === -1) { + throw new SyntaxError(`Unexpected character at index ${i}`); + } + + if (end === -1) end = i; + push(params, header.slice(start, end), true); + if (code === 0x2c) { + push(offers, extensionName, params); + params = Object.create(null); + extensionName = undefined; + } + + start = end = -1; + } else if (code === 0x3d /* '=' */ && start !== -1 && end === -1) { + paramName = header.slice(start, i); + start = end = -1; + } else { + throw new SyntaxError(`Unexpected character at index ${i}`); + } + } else { + // + // The value of a quoted-string after unescaping must conform to the + // token ABNF, so only token characters are valid. + // Ref: https://tools.ietf.org/html/rfc6455#section-9.1 + // + if (isEscaping) { + if (tokenChars[code] !== 1) { + throw new SyntaxError(`Unexpected character at index ${i}`); + } + if (start === -1) start = i; + else if (!mustUnescape) mustUnescape = true; + isEscaping = false; + } else if (inQuotes) { + if (tokenChars[code] === 1) { + if (start === -1) start = i; + } else if (code === 0x22 /* '"' */ && start !== -1) { + inQuotes = false; + end = i; + } else if (code === 0x5c /* '\' */) { + isEscaping = true; + } else { + throw new SyntaxError(`Unexpected character at index ${i}`); + } + } else if (code === 0x22 && header.charCodeAt(i - 1) === 0x3d) { + inQuotes = true; + } else if (end === -1 && tokenChars[code] === 1) { + if (start === -1) start = i; + } else if (start !== -1 && (code === 0x20 || code === 0x09)) { + if (end === -1) end = i; + } else if (code === 0x3b || code === 0x2c) { + if (start === -1) { + throw new SyntaxError(`Unexpected character at index ${i}`); + } + + if (end === -1) end = i; + let value = header.slice(start, end); + if (mustUnescape) { + value = value.replace(/\\/g, ''); + mustUnescape = false; + } + push(params, paramName, value); + if (code === 0x2c) { + push(offers, extensionName, params); + params = Object.create(null); + extensionName = undefined; + } + + paramName = undefined; + start = end = -1; + } else { + throw new SyntaxError(`Unexpected character at index ${i}`); + } + } + } + + if (start === -1 || inQuotes || code === 0x20 || code === 0x09) { + throw new SyntaxError('Unexpected end of input'); + } + + if (end === -1) end = i; + const token = header.slice(start, end); + if (extensionName === undefined) { + push(offers, token, params); + } else { + if (paramName === undefined) { + push(params, token, true); + } else if (mustUnescape) { + push(params, paramName, token.replace(/\\/g, '')); + } else { + push(params, paramName, token); + } + push(offers, extensionName, params); + } + + return offers; +} + +/** + * Builds the `Sec-WebSocket-Extensions` header field value. + * + * @param {Object} extensions The map of extensions and parameters to format + * @return {String} A string representing the given object + * @public + */ +function format(extensions) { + return Object.keys(extensions) + .map((extension) => { + let configurations = extensions[extension]; + if (!Array.isArray(configurations)) configurations = [configurations]; + return configurations + .map((params) => { + return [extension] + .concat( + Object.keys(params).map((k) => { + let values = params[k]; + if (!Array.isArray(values)) values = [values]; + return values + .map((v) => (v === true ? k : `${k}=${v}`)) + .join('; '); + }) + ) + .join('; '); + }) + .join(', '); + }) + .join(', '); +} + +module.exports = { format, parse }; diff --git a/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/lib/limiter.js b/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/lib/limiter.js new file mode 100644 index 0000000..3fd3578 --- /dev/null +++ b/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/lib/limiter.js @@ -0,0 +1,55 @@ +'use strict'; + +const kDone = Symbol('kDone'); +const kRun = Symbol('kRun'); + +/** + * A very simple job queue with adjustable concurrency. Adapted from + * https://github.com/STRML/async-limiter + */ +class Limiter { + /** + * Creates a new `Limiter`. + * + * @param {Number} [concurrency=Infinity] The maximum number of jobs allowed + * to run concurrently + */ + constructor(concurrency) { + this[kDone] = () => { + this.pending--; + this[kRun](); + }; + this.concurrency = concurrency || Infinity; + this.jobs = []; + this.pending = 0; + } + + /** + * Adds a job to the queue. + * + * @param {Function} job The job to run + * @public + */ + add(job) { + this.jobs.push(job); + this[kRun](); + } + + /** + * Removes a job from the queue and runs it if possible. + * + * @private + */ + [kRun]() { + if (this.pending === this.concurrency) return; + + if (this.jobs.length) { + const job = this.jobs.shift(); + + this.pending++; + job(this[kDone]); + } + } +} + +module.exports = Limiter; diff --git a/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/lib/permessage-deflate.js b/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/lib/permessage-deflate.js new file mode 100644 index 0000000..5040697 --- /dev/null +++ b/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/lib/permessage-deflate.js @@ -0,0 +1,511 @@ +'use strict'; + +const zlib = require('zlib'); + +const bufferUtil = require('./buffer-util'); +const Limiter = require('./limiter'); +const { kStatusCode } = require('./constants'); + +const TRAILER = Buffer.from([0x00, 0x00, 0xff, 0xff]); +const kPerMessageDeflate = Symbol('permessage-deflate'); +const kTotalLength = Symbol('total-length'); +const kCallback = Symbol('callback'); +const kBuffers = Symbol('buffers'); +const kError = Symbol('error'); + +// +// We limit zlib concurrency, which prevents severe memory fragmentation +// as documented in https://github.com/nodejs/node/issues/8871#issuecomment-250915913 +// and https://github.com/websockets/ws/issues/1202 +// +// Intentionally global; it's the global thread pool that's an issue. +// +let zlibLimiter; + +/** + * permessage-deflate implementation. + */ +class PerMessageDeflate { + /** + * Creates a PerMessageDeflate instance. + * + * @param {Object} [options] Configuration options + * @param {(Boolean|Number)} [options.clientMaxWindowBits] Advertise support + * for, or request, a custom client window size + * @param {Boolean} [options.clientNoContextTakeover=false] Advertise/ + * acknowledge disabling of client context takeover + * @param {Number} [options.concurrencyLimit=10] The number of concurrent + * calls to zlib + * @param {(Boolean|Number)} [options.serverMaxWindowBits] Request/confirm the + * use of a custom server window size + * @param {Boolean} [options.serverNoContextTakeover=false] Request/accept + * disabling of server context takeover + * @param {Number} [options.threshold=1024] Size (in bytes) below which + * messages should not be compressed if context takeover is disabled + * @param {Object} [options.zlibDeflateOptions] Options to pass to zlib on + * deflate + * @param {Object} [options.zlibInflateOptions] Options to pass to zlib on + * inflate + * @param {Boolean} [isServer=false] Create the instance in either server or + * client mode + * @param {Number} [maxPayload=0] The maximum allowed message length + */ + constructor(options, isServer, maxPayload) { + this._maxPayload = maxPayload | 0; + this._options = options || {}; + this._threshold = + this._options.threshold !== undefined ? this._options.threshold : 1024; + this._isServer = !!isServer; + this._deflate = null; + this._inflate = null; + + this.params = null; + + if (!zlibLimiter) { + const concurrency = + this._options.concurrencyLimit !== undefined + ? this._options.concurrencyLimit + : 10; + zlibLimiter = new Limiter(concurrency); + } + } + + /** + * @type {String} + */ + static get extensionName() { + return 'permessage-deflate'; + } + + /** + * Create an extension negotiation offer. + * + * @return {Object} Extension parameters + * @public + */ + offer() { + const params = {}; + + if (this._options.serverNoContextTakeover) { + params.server_no_context_takeover = true; + } + if (this._options.clientNoContextTakeover) { + params.client_no_context_takeover = true; + } + if (this._options.serverMaxWindowBits) { + params.server_max_window_bits = this._options.serverMaxWindowBits; + } + if (this._options.clientMaxWindowBits) { + params.client_max_window_bits = this._options.clientMaxWindowBits; + } else if (this._options.clientMaxWindowBits == null) { + params.client_max_window_bits = true; + } + + return params; + } + + /** + * Accept an extension negotiation offer/response. + * + * @param {Array} configurations The extension negotiation offers/reponse + * @return {Object} Accepted configuration + * @public + */ + accept(configurations) { + configurations = this.normalizeParams(configurations); + + this.params = this._isServer + ? this.acceptAsServer(configurations) + : this.acceptAsClient(configurations); + + return this.params; + } + + /** + * Releases all resources used by the extension. + * + * @public + */ + cleanup() { + if (this._inflate) { + this._inflate.close(); + this._inflate = null; + } + + if (this._deflate) { + const callback = this._deflate[kCallback]; + + this._deflate.close(); + this._deflate = null; + + if (callback) { + callback( + new Error( + 'The deflate stream was closed while data was being processed' + ) + ); + } + } + } + + /** + * Accept an extension negotiation offer. + * + * @param {Array} offers The extension negotiation offers + * @return {Object} Accepted configuration + * @private + */ + acceptAsServer(offers) { + const opts = this._options; + const accepted = offers.find((params) => { + if ( + (opts.serverNoContextTakeover === false && + params.server_no_context_takeover) || + (params.server_max_window_bits && + (opts.serverMaxWindowBits === false || + (typeof opts.serverMaxWindowBits === 'number' && + opts.serverMaxWindowBits > params.server_max_window_bits))) || + (typeof opts.clientMaxWindowBits === 'number' && + !params.client_max_window_bits) + ) { + return false; + } + + return true; + }); + + if (!accepted) { + throw new Error('None of the extension offers can be accepted'); + } + + if (opts.serverNoContextTakeover) { + accepted.server_no_context_takeover = true; + } + if (opts.clientNoContextTakeover) { + accepted.client_no_context_takeover = true; + } + if (typeof opts.serverMaxWindowBits === 'number') { + accepted.server_max_window_bits = opts.serverMaxWindowBits; + } + if (typeof opts.clientMaxWindowBits === 'number') { + accepted.client_max_window_bits = opts.clientMaxWindowBits; + } else if ( + accepted.client_max_window_bits === true || + opts.clientMaxWindowBits === false + ) { + delete accepted.client_max_window_bits; + } + + return accepted; + } + + /** + * Accept the extension negotiation response. + * + * @param {Array} response The extension negotiation response + * @return {Object} Accepted configuration + * @private + */ + acceptAsClient(response) { + const params = response[0]; + + if ( + this._options.clientNoContextTakeover === false && + params.client_no_context_takeover + ) { + throw new Error('Unexpected parameter "client_no_context_takeover"'); + } + + if (!params.client_max_window_bits) { + if (typeof this._options.clientMaxWindowBits === 'number') { + params.client_max_window_bits = this._options.clientMaxWindowBits; + } + } else if ( + this._options.clientMaxWindowBits === false || + (typeof this._options.clientMaxWindowBits === 'number' && + params.client_max_window_bits > this._options.clientMaxWindowBits) + ) { + throw new Error( + 'Unexpected or invalid parameter "client_max_window_bits"' + ); + } + + return params; + } + + /** + * Normalize parameters. + * + * @param {Array} configurations The extension negotiation offers/reponse + * @return {Array} The offers/response with normalized parameters + * @private + */ + normalizeParams(configurations) { + configurations.forEach((params) => { + Object.keys(params).forEach((key) => { + let value = params[key]; + + if (value.length > 1) { + throw new Error(`Parameter "${key}" must have only a single value`); + } + + value = value[0]; + + if (key === 'client_max_window_bits') { + if (value !== true) { + const num = +value; + if (!Number.isInteger(num) || num < 8 || num > 15) { + throw new TypeError( + `Invalid value for parameter "${key}": ${value}` + ); + } + value = num; + } else if (!this._isServer) { + throw new TypeError( + `Invalid value for parameter "${key}": ${value}` + ); + } + } else if (key === 'server_max_window_bits') { + const num = +value; + if (!Number.isInteger(num) || num < 8 || num > 15) { + throw new TypeError( + `Invalid value for parameter "${key}": ${value}` + ); + } + value = num; + } else if ( + key === 'client_no_context_takeover' || + key === 'server_no_context_takeover' + ) { + if (value !== true) { + throw new TypeError( + `Invalid value for parameter "${key}": ${value}` + ); + } + } else { + throw new Error(`Unknown parameter "${key}"`); + } + + params[key] = value; + }); + }); + + return configurations; + } + + /** + * Decompress data. Concurrency limited. + * + * @param {Buffer} data Compressed data + * @param {Boolean} fin Specifies whether or not this is the last fragment + * @param {Function} callback Callback + * @public + */ + decompress(data, fin, callback) { + zlibLimiter.add((done) => { + this._decompress(data, fin, (err, result) => { + done(); + callback(err, result); + }); + }); + } + + /** + * Compress data. Concurrency limited. + * + * @param {Buffer} data Data to compress + * @param {Boolean} fin Specifies whether or not this is the last fragment + * @param {Function} callback Callback + * @public + */ + compress(data, fin, callback) { + zlibLimiter.add((done) => { + this._compress(data, fin, (err, result) => { + done(); + callback(err, result); + }); + }); + } + + /** + * Decompress data. + * + * @param {Buffer} data Compressed data + * @param {Boolean} fin Specifies whether or not this is the last fragment + * @param {Function} callback Callback + * @private + */ + _decompress(data, fin, callback) { + const endpoint = this._isServer ? 'client' : 'server'; + + if (!this._inflate) { + const key = `${endpoint}_max_window_bits`; + const windowBits = + typeof this.params[key] !== 'number' + ? zlib.Z_DEFAULT_WINDOWBITS + : this.params[key]; + + this._inflate = zlib.createInflateRaw({ + ...this._options.zlibInflateOptions, + windowBits + }); + this._inflate[kPerMessageDeflate] = this; + this._inflate[kTotalLength] = 0; + this._inflate[kBuffers] = []; + this._inflate.on('error', inflateOnError); + this._inflate.on('data', inflateOnData); + } + + this._inflate[kCallback] = callback; + + this._inflate.write(data); + if (fin) this._inflate.write(TRAILER); + + this._inflate.flush(() => { + const err = this._inflate[kError]; + + if (err) { + this._inflate.close(); + this._inflate = null; + callback(err); + return; + } + + const data = bufferUtil.concat( + this._inflate[kBuffers], + this._inflate[kTotalLength] + ); + + if (this._inflate._readableState.endEmitted) { + this._inflate.close(); + this._inflate = null; + } else { + this._inflate[kTotalLength] = 0; + this._inflate[kBuffers] = []; + + if (fin && this.params[`${endpoint}_no_context_takeover`]) { + this._inflate.reset(); + } + } + + callback(null, data); + }); + } + + /** + * Compress data. + * + * @param {Buffer} data Data to compress + * @param {Boolean} fin Specifies whether or not this is the last fragment + * @param {Function} callback Callback + * @private + */ + _compress(data, fin, callback) { + const endpoint = this._isServer ? 'server' : 'client'; + + if (!this._deflate) { + const key = `${endpoint}_max_window_bits`; + const windowBits = + typeof this.params[key] !== 'number' + ? zlib.Z_DEFAULT_WINDOWBITS + : this.params[key]; + + this._deflate = zlib.createDeflateRaw({ + ...this._options.zlibDeflateOptions, + windowBits + }); + + this._deflate[kTotalLength] = 0; + this._deflate[kBuffers] = []; + + this._deflate.on('data', deflateOnData); + } + + this._deflate[kCallback] = callback; + + this._deflate.write(data); + this._deflate.flush(zlib.Z_SYNC_FLUSH, () => { + if (!this._deflate) { + // + // The deflate stream was closed while data was being processed. + // + return; + } + + let data = bufferUtil.concat( + this._deflate[kBuffers], + this._deflate[kTotalLength] + ); + + if (fin) data = data.slice(0, data.length - 4); + + // + // Ensure that the callback will not be called again in + // `PerMessageDeflate#cleanup()`. + // + this._deflate[kCallback] = null; + + this._deflate[kTotalLength] = 0; + this._deflate[kBuffers] = []; + + if (fin && this.params[`${endpoint}_no_context_takeover`]) { + this._deflate.reset(); + } + + callback(null, data); + }); + } +} + +module.exports = PerMessageDeflate; + +/** + * The listener of the `zlib.DeflateRaw` stream `'data'` event. + * + * @param {Buffer} chunk A chunk of data + * @private + */ +function deflateOnData(chunk) { + this[kBuffers].push(chunk); + this[kTotalLength] += chunk.length; +} + +/** + * The listener of the `zlib.InflateRaw` stream `'data'` event. + * + * @param {Buffer} chunk A chunk of data + * @private + */ +function inflateOnData(chunk) { + this[kTotalLength] += chunk.length; + + if ( + this[kPerMessageDeflate]._maxPayload < 1 || + this[kTotalLength] <= this[kPerMessageDeflate]._maxPayload + ) { + this[kBuffers].push(chunk); + return; + } + + this[kError] = new RangeError('Max payload size exceeded'); + this[kError].code = 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH'; + this[kError][kStatusCode] = 1009; + this.removeListener('data', inflateOnData); + this.reset(); +} + +/** + * The listener of the `zlib.InflateRaw` stream `'error'` event. + * + * @param {Error} err The emitted error + * @private + */ +function inflateOnError(err) { + // + // There is no need to call `Zlib#close()` as the handle is automatically + // closed when an error is emitted. + // + this[kPerMessageDeflate]._inflate = null; + err[kStatusCode] = 1007; + this[kCallback](err); +} diff --git a/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/lib/receiver.js b/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/lib/receiver.js new file mode 100644 index 0000000..e11e266 --- /dev/null +++ b/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/lib/receiver.js @@ -0,0 +1,612 @@ +'use strict'; + +const { Writable } = require('stream'); + +const PerMessageDeflate = require('./permessage-deflate'); +const { + BINARY_TYPES, + EMPTY_BUFFER, + kStatusCode, + kWebSocket +} = require('./constants'); +const { concat, toArrayBuffer, unmask } = require('./buffer-util'); +const { isValidStatusCode, isValidUTF8 } = require('./validation'); + +const GET_INFO = 0; +const GET_PAYLOAD_LENGTH_16 = 1; +const GET_PAYLOAD_LENGTH_64 = 2; +const GET_MASK = 3; +const GET_DATA = 4; +const INFLATING = 5; + +/** + * HyBi Receiver implementation. + * + * @extends Writable + */ +class Receiver extends Writable { + /** + * Creates a Receiver instance. + * + * @param {Object} [options] Options object + * @param {String} [options.binaryType=nodebuffer] The type for binary data + * @param {Object} [options.extensions] An object containing the negotiated + * extensions + * @param {Boolean} [options.isServer=false] Specifies whether to operate in + * client or server mode + * @param {Number} [options.maxPayload=0] The maximum allowed message length + * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or + * not to skip UTF-8 validation for text and close messages + */ + constructor(options = {}) { + super(); + + this._binaryType = options.binaryType || BINARY_TYPES[0]; + this._extensions = options.extensions || {}; + this._isServer = !!options.isServer; + this._maxPayload = options.maxPayload | 0; + this._skipUTF8Validation = !!options.skipUTF8Validation; + this[kWebSocket] = undefined; + + this._bufferedBytes = 0; + this._buffers = []; + + this._compressed = false; + this._payloadLength = 0; + this._mask = undefined; + this._fragmented = 0; + this._masked = false; + this._fin = false; + this._opcode = 0; + + this._totalPayloadLength = 0; + this._messageLength = 0; + this._fragments = []; + + this._state = GET_INFO; + this._loop = false; + } + + /** + * Implements `Writable.prototype._write()`. + * + * @param {Buffer} chunk The chunk of data to write + * @param {String} encoding The character encoding of `chunk` + * @param {Function} cb Callback + * @private + */ + _write(chunk, encoding, cb) { + if (this._opcode === 0x08 && this._state == GET_INFO) return cb(); + + this._bufferedBytes += chunk.length; + this._buffers.push(chunk); + this.startLoop(cb); + } + + /** + * Consumes `n` bytes from the buffered data. + * + * @param {Number} n The number of bytes to consume + * @return {Buffer} The consumed bytes + * @private + */ + consume(n) { + this._bufferedBytes -= n; + + if (n === this._buffers[0].length) return this._buffers.shift(); + + if (n < this._buffers[0].length) { + const buf = this._buffers[0]; + this._buffers[0] = buf.slice(n); + return buf.slice(0, n); + } + + const dst = Buffer.allocUnsafe(n); + + do { + const buf = this._buffers[0]; + const offset = dst.length - n; + + if (n >= buf.length) { + dst.set(this._buffers.shift(), offset); + } else { + dst.set(new Uint8Array(buf.buffer, buf.byteOffset, n), offset); + this._buffers[0] = buf.slice(n); + } + + n -= buf.length; + } while (n > 0); + + return dst; + } + + /** + * Starts the parsing loop. + * + * @param {Function} cb Callback + * @private + */ + startLoop(cb) { + let err; + this._loop = true; + + do { + switch (this._state) { + case GET_INFO: + err = this.getInfo(); + break; + case GET_PAYLOAD_LENGTH_16: + err = this.getPayloadLength16(); + break; + case GET_PAYLOAD_LENGTH_64: + err = this.getPayloadLength64(); + break; + case GET_MASK: + this.getMask(); + break; + case GET_DATA: + err = this.getData(cb); + break; + default: + // `INFLATING` + this._loop = false; + return; + } + } while (this._loop); + + cb(err); + } + + /** + * Reads the first two bytes of a frame. + * + * @return {(RangeError|undefined)} A possible error + * @private + */ + getInfo() { + if (this._bufferedBytes < 2) { + this._loop = false; + return; + } + + const buf = this.consume(2); + + if ((buf[0] & 0x30) !== 0x00) { + this._loop = false; + return error( + RangeError, + 'RSV2 and RSV3 must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_RSV_2_3' + ); + } + + const compressed = (buf[0] & 0x40) === 0x40; + + if (compressed && !this._extensions[PerMessageDeflate.extensionName]) { + this._loop = false; + return error( + RangeError, + 'RSV1 must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_RSV_1' + ); + } + + this._fin = (buf[0] & 0x80) === 0x80; + this._opcode = buf[0] & 0x0f; + this._payloadLength = buf[1] & 0x7f; + + if (this._opcode === 0x00) { + if (compressed) { + this._loop = false; + return error( + RangeError, + 'RSV1 must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_RSV_1' + ); + } + + if (!this._fragmented) { + this._loop = false; + return error( + RangeError, + 'invalid opcode 0', + true, + 1002, + 'WS_ERR_INVALID_OPCODE' + ); + } + + this._opcode = this._fragmented; + } else if (this._opcode === 0x01 || this._opcode === 0x02) { + if (this._fragmented) { + this._loop = false; + return error( + RangeError, + `invalid opcode ${this._opcode}`, + true, + 1002, + 'WS_ERR_INVALID_OPCODE' + ); + } + + this._compressed = compressed; + } else if (this._opcode > 0x07 && this._opcode < 0x0b) { + if (!this._fin) { + this._loop = false; + return error( + RangeError, + 'FIN must be set', + true, + 1002, + 'WS_ERR_EXPECTED_FIN' + ); + } + + if (compressed) { + this._loop = false; + return error( + RangeError, + 'RSV1 must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_RSV_1' + ); + } + + if (this._payloadLength > 0x7d) { + this._loop = false; + return error( + RangeError, + `invalid payload length ${this._payloadLength}`, + true, + 1002, + 'WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH' + ); + } + } else { + this._loop = false; + return error( + RangeError, + `invalid opcode ${this._opcode}`, + true, + 1002, + 'WS_ERR_INVALID_OPCODE' + ); + } + + if (!this._fin && !this._fragmented) this._fragmented = this._opcode; + this._masked = (buf[1] & 0x80) === 0x80; + + if (this._isServer) { + if (!this._masked) { + this._loop = false; + return error( + RangeError, + 'MASK must be set', + true, + 1002, + 'WS_ERR_EXPECTED_MASK' + ); + } + } else if (this._masked) { + this._loop = false; + return error( + RangeError, + 'MASK must be clear', + true, + 1002, + 'WS_ERR_UNEXPECTED_MASK' + ); + } + + if (this._payloadLength === 126) this._state = GET_PAYLOAD_LENGTH_16; + else if (this._payloadLength === 127) this._state = GET_PAYLOAD_LENGTH_64; + else return this.haveLength(); + } + + /** + * Gets extended payload length (7+16). + * + * @return {(RangeError|undefined)} A possible error + * @private + */ + getPayloadLength16() { + if (this._bufferedBytes < 2) { + this._loop = false; + return; + } + + this._payloadLength = this.consume(2).readUInt16BE(0); + return this.haveLength(); + } + + /** + * Gets extended payload length (7+64). + * + * @return {(RangeError|undefined)} A possible error + * @private + */ + getPayloadLength64() { + if (this._bufferedBytes < 8) { + this._loop = false; + return; + } + + const buf = this.consume(8); + const num = buf.readUInt32BE(0); + + // + // The maximum safe integer in JavaScript is 2^53 - 1. An error is returned + // if payload length is greater than this number. + // + if (num > Math.pow(2, 53 - 32) - 1) { + this._loop = false; + return error( + RangeError, + 'Unsupported WebSocket frame: payload length > 2^53 - 1', + false, + 1009, + 'WS_ERR_UNSUPPORTED_DATA_PAYLOAD_LENGTH' + ); + } + + this._payloadLength = num * Math.pow(2, 32) + buf.readUInt32BE(4); + return this.haveLength(); + } + + /** + * Payload length has been read. + * + * @return {(RangeError|undefined)} A possible error + * @private + */ + haveLength() { + if (this._payloadLength && this._opcode < 0x08) { + this._totalPayloadLength += this._payloadLength; + if (this._totalPayloadLength > this._maxPayload && this._maxPayload > 0) { + this._loop = false; + return error( + RangeError, + 'Max payload size exceeded', + false, + 1009, + 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH' + ); + } + } + + if (this._masked) this._state = GET_MASK; + else this._state = GET_DATA; + } + + /** + * Reads mask bytes. + * + * @private + */ + getMask() { + if (this._bufferedBytes < 4) { + this._loop = false; + return; + } + + this._mask = this.consume(4); + this._state = GET_DATA; + } + + /** + * Reads data bytes. + * + * @param {Function} cb Callback + * @return {(Error|RangeError|undefined)} A possible error + * @private + */ + getData(cb) { + let data = EMPTY_BUFFER; + + if (this._payloadLength) { + if (this._bufferedBytes < this._payloadLength) { + this._loop = false; + return; + } + + data = this.consume(this._payloadLength); + if (this._masked) unmask(data, this._mask); + } + + if (this._opcode > 0x07) return this.controlMessage(data); + + if (this._compressed) { + this._state = INFLATING; + this.decompress(data, cb); + return; + } + + if (data.length) { + // + // This message is not compressed so its length is the sum of the payload + // length of all fragments. + // + this._messageLength = this._totalPayloadLength; + this._fragments.push(data); + } + + return this.dataMessage(); + } + + /** + * Decompresses data. + * + * @param {Buffer} data Compressed data + * @param {Function} cb Callback + * @private + */ + decompress(data, cb) { + const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName]; + + perMessageDeflate.decompress(data, this._fin, (err, buf) => { + if (err) return cb(err); + + if (buf.length) { + this._messageLength += buf.length; + if (this._messageLength > this._maxPayload && this._maxPayload > 0) { + return cb( + error( + RangeError, + 'Max payload size exceeded', + false, + 1009, + 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH' + ) + ); + } + + this._fragments.push(buf); + } + + const er = this.dataMessage(); + if (er) return cb(er); + + this.startLoop(cb); + }); + } + + /** + * Handles a data message. + * + * @return {(Error|undefined)} A possible error + * @private + */ + dataMessage() { + if (this._fin) { + const messageLength = this._messageLength; + const fragments = this._fragments; + + this._totalPayloadLength = 0; + this._messageLength = 0; + this._fragmented = 0; + this._fragments = []; + + if (this._opcode === 2) { + let data; + + if (this._binaryType === 'nodebuffer') { + data = concat(fragments, messageLength); + } else if (this._binaryType === 'arraybuffer') { + data = toArrayBuffer(concat(fragments, messageLength)); + } else { + data = fragments; + } + + this.emit('message', data, true); + } else { + const buf = concat(fragments, messageLength); + + if (!this._skipUTF8Validation && !isValidUTF8(buf)) { + this._loop = false; + return error( + Error, + 'invalid UTF-8 sequence', + true, + 1007, + 'WS_ERR_INVALID_UTF8' + ); + } + + this.emit('message', buf, false); + } + } + + this._state = GET_INFO; + } + + /** + * Handles a control message. + * + * @param {Buffer} data Data to handle + * @return {(Error|RangeError|undefined)} A possible error + * @private + */ + controlMessage(data) { + if (this._opcode === 0x08) { + this._loop = false; + + if (data.length === 0) { + this.emit('conclude', 1005, EMPTY_BUFFER); + this.end(); + } else if (data.length === 1) { + return error( + RangeError, + 'invalid payload length 1', + true, + 1002, + 'WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH' + ); + } else { + const code = data.readUInt16BE(0); + + if (!isValidStatusCode(code)) { + return error( + RangeError, + `invalid status code ${code}`, + true, + 1002, + 'WS_ERR_INVALID_CLOSE_CODE' + ); + } + + const buf = data.slice(2); + + if (!this._skipUTF8Validation && !isValidUTF8(buf)) { + return error( + Error, + 'invalid UTF-8 sequence', + true, + 1007, + 'WS_ERR_INVALID_UTF8' + ); + } + + this.emit('conclude', code, buf); + this.end(); + } + } else if (this._opcode === 0x09) { + this.emit('ping', data); + } else { + this.emit('pong', data); + } + + this._state = GET_INFO; + } +} + +module.exports = Receiver; + +/** + * Builds an error object. + * + * @param {function(new:Error|RangeError)} ErrorCtor The error constructor + * @param {String} message The error message + * @param {Boolean} prefix Specifies whether or not to add a default prefix to + * `message` + * @param {Number} statusCode The status code + * @param {String} errorCode The exposed error code + * @return {(Error|RangeError)} The error + * @private + */ +function error(ErrorCtor, message, prefix, statusCode, errorCode) { + const err = new ErrorCtor( + prefix ? `Invalid WebSocket frame: ${message}` : message + ); + + Error.captureStackTrace(err, error); + err.code = errorCode; + err[kStatusCode] = statusCode; + return err; +} diff --git a/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/lib/sender.js b/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/lib/sender.js new file mode 100644 index 0000000..4490a62 --- /dev/null +++ b/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/lib/sender.js @@ -0,0 +1,422 @@ +/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^net|tls$" }] */ + +'use strict'; + +const net = require('net'); +const tls = require('tls'); +const { randomFillSync } = require('crypto'); + +const PerMessageDeflate = require('./permessage-deflate'); +const { EMPTY_BUFFER } = require('./constants'); +const { isValidStatusCode } = require('./validation'); +const { mask: applyMask, toBuffer } = require('./buffer-util'); + +const mask = Buffer.alloc(4); + +/** + * HyBi Sender implementation. + */ +class Sender { + /** + * Creates a Sender instance. + * + * @param {(net.Socket|tls.Socket)} socket The connection socket + * @param {Object} [extensions] An object containing the negotiated extensions + */ + constructor(socket, extensions) { + this._extensions = extensions || {}; + this._socket = socket; + + this._firstFragment = true; + this._compress = false; + + this._bufferedBytes = 0; + this._deflating = false; + this._queue = []; + } + + /** + * Frames a piece of data according to the HyBi WebSocket protocol. + * + * @param {Buffer} data The data to frame + * @param {Object} options Options object + * @param {Boolean} [options.fin=false] Specifies whether or not to set the + * FIN bit + * @param {Boolean} [options.mask=false] Specifies whether or not to mask + * `data` + * @param {Number} options.opcode The opcode + * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be + * modified + * @param {Boolean} [options.rsv1=false] Specifies whether or not to set the + * RSV1 bit + * @return {Buffer[]} The framed data as a list of `Buffer` instances + * @public + */ + static frame(data, options) { + const merge = options.mask && options.readOnly; + let offset = options.mask ? 6 : 2; + let payloadLength = data.length; + + if (data.length >= 65536) { + offset += 8; + payloadLength = 127; + } else if (data.length > 125) { + offset += 2; + payloadLength = 126; + } + + const target = Buffer.allocUnsafe(merge ? data.length + offset : offset); + + target[0] = options.fin ? options.opcode | 0x80 : options.opcode; + if (options.rsv1) target[0] |= 0x40; + + target[1] = payloadLength; + + if (payloadLength === 126) { + target.writeUInt16BE(data.length, 2); + } else if (payloadLength === 127) { + target[2] = target[3] = 0; + target.writeUIntBE(data.length, 4, 6); + } + + if (!options.mask) return [target, data]; + + randomFillSync(mask, 0, 4); + + target[1] |= 0x80; + target[offset - 4] = mask[0]; + target[offset - 3] = mask[1]; + target[offset - 2] = mask[2]; + target[offset - 1] = mask[3]; + + if (merge) { + applyMask(data, mask, target, offset, data.length); + return [target]; + } + + applyMask(data, mask, data, 0, data.length); + return [target, data]; + } + + /** + * Sends a close message to the other peer. + * + * @param {Number} [code] The status code component of the body + * @param {(String|Buffer)} [data] The message component of the body + * @param {Boolean} [mask=false] Specifies whether or not to mask the message + * @param {Function} [cb] Callback + * @public + */ + close(code, data, mask, cb) { + let buf; + + if (code === undefined) { + buf = EMPTY_BUFFER; + } else if (typeof code !== 'number' || !isValidStatusCode(code)) { + throw new TypeError('First argument must be a valid error code number'); + } else if (data === undefined || !data.length) { + buf = Buffer.allocUnsafe(2); + buf.writeUInt16BE(code, 0); + } else { + const length = Buffer.byteLength(data); + + if (length > 123) { + throw new RangeError('The message must not be greater than 123 bytes'); + } + + buf = Buffer.allocUnsafe(2 + length); + buf.writeUInt16BE(code, 0); + + if (typeof data === 'string') { + buf.write(data, 2); + } else { + buf.set(data, 2); + } + } + + if (this._deflating) { + this.enqueue([this.doClose, buf, mask, cb]); + } else { + this.doClose(buf, mask, cb); + } + } + + /** + * Frames and sends a close message. + * + * @param {Buffer} data The message to send + * @param {Boolean} [mask=false] Specifies whether or not to mask `data` + * @param {Function} [cb] Callback + * @private + */ + doClose(data, mask, cb) { + this.sendFrame( + Sender.frame(data, { + fin: true, + rsv1: false, + opcode: 0x08, + mask, + readOnly: false + }), + cb + ); + } + + /** + * Sends a ping message to the other peer. + * + * @param {*} data The message to send + * @param {Boolean} [mask=false] Specifies whether or not to mask `data` + * @param {Function} [cb] Callback + * @public + */ + ping(data, mask, cb) { + const buf = toBuffer(data); + + if (buf.length > 125) { + throw new RangeError('The data size must not be greater than 125 bytes'); + } + + if (this._deflating) { + this.enqueue([this.doPing, buf, mask, toBuffer.readOnly, cb]); + } else { + this.doPing(buf, mask, toBuffer.readOnly, cb); + } + } + + /** + * Frames and sends a ping message. + * + * @param {Buffer} data The message to send + * @param {Boolean} [mask=false] Specifies whether or not to mask `data` + * @param {Boolean} [readOnly=false] Specifies whether `data` can be modified + * @param {Function} [cb] Callback + * @private + */ + doPing(data, mask, readOnly, cb) { + this.sendFrame( + Sender.frame(data, { + fin: true, + rsv1: false, + opcode: 0x09, + mask, + readOnly + }), + cb + ); + } + + /** + * Sends a pong message to the other peer. + * + * @param {*} data The message to send + * @param {Boolean} [mask=false] Specifies whether or not to mask `data` + * @param {Function} [cb] Callback + * @public + */ + pong(data, mask, cb) { + const buf = toBuffer(data); + + if (buf.length > 125) { + throw new RangeError('The data size must not be greater than 125 bytes'); + } + + if (this._deflating) { + this.enqueue([this.doPong, buf, mask, toBuffer.readOnly, cb]); + } else { + this.doPong(buf, mask, toBuffer.readOnly, cb); + } + } + + /** + * Frames and sends a pong message. + * + * @param {Buffer} data The message to send + * @param {Boolean} [mask=false] Specifies whether or not to mask `data` + * @param {Boolean} [readOnly=false] Specifies whether `data` can be modified + * @param {Function} [cb] Callback + * @private + */ + doPong(data, mask, readOnly, cb) { + this.sendFrame( + Sender.frame(data, { + fin: true, + rsv1: false, + opcode: 0x0a, + mask, + readOnly + }), + cb + ); + } + + /** + * Sends a data message to the other peer. + * + * @param {*} data The message to send + * @param {Object} options Options object + * @param {Boolean} [options.binary=false] Specifies whether `data` is binary + * or text + * @param {Boolean} [options.compress=false] Specifies whether or not to + * compress `data` + * @param {Boolean} [options.fin=false] Specifies whether the fragment is the + * last one + * @param {Boolean} [options.mask=false] Specifies whether or not to mask + * `data` + * @param {Function} [cb] Callback + * @public + */ + send(data, options, cb) { + const buf = toBuffer(data); + const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName]; + let opcode = options.binary ? 2 : 1; + let rsv1 = options.compress; + + if (this._firstFragment) { + this._firstFragment = false; + if ( + rsv1 && + perMessageDeflate && + perMessageDeflate.params[ + perMessageDeflate._isServer + ? 'server_no_context_takeover' + : 'client_no_context_takeover' + ] + ) { + rsv1 = buf.length >= perMessageDeflate._threshold; + } + this._compress = rsv1; + } else { + rsv1 = false; + opcode = 0; + } + + if (options.fin) this._firstFragment = true; + + if (perMessageDeflate) { + const opts = { + fin: options.fin, + rsv1, + opcode, + mask: options.mask, + readOnly: toBuffer.readOnly + }; + + if (this._deflating) { + this.enqueue([this.dispatch, buf, this._compress, opts, cb]); + } else { + this.dispatch(buf, this._compress, opts, cb); + } + } else { + this.sendFrame( + Sender.frame(buf, { + fin: options.fin, + rsv1: false, + opcode, + mask: options.mask, + readOnly: toBuffer.readOnly + }), + cb + ); + } + } + + /** + * Dispatches a data message. + * + * @param {Buffer} data The message to send + * @param {Boolean} [compress=false] Specifies whether or not to compress + * `data` + * @param {Object} options Options object + * @param {Number} options.opcode The opcode + * @param {Boolean} [options.fin=false] Specifies whether or not to set the + * FIN bit + * @param {Boolean} [options.mask=false] Specifies whether or not to mask + * `data` + * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be + * modified + * @param {Boolean} [options.rsv1=false] Specifies whether or not to set the + * RSV1 bit + * @param {Function} [cb] Callback + * @private + */ + dispatch(data, compress, options, cb) { + if (!compress) { + this.sendFrame(Sender.frame(data, options), cb); + return; + } + + const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName]; + + this._bufferedBytes += data.length; + this._deflating = true; + perMessageDeflate.compress(data, options.fin, (_, buf) => { + if (this._socket.destroyed) { + const err = new Error( + 'The socket was closed while data was being compressed' + ); + + if (typeof cb === 'function') cb(err); + + for (let i = 0; i < this._queue.length; i++) { + const callback = this._queue[i][4]; + + if (typeof callback === 'function') callback(err); + } + + return; + } + + this._bufferedBytes -= data.length; + this._deflating = false; + options.readOnly = false; + this.sendFrame(Sender.frame(buf, options), cb); + this.dequeue(); + }); + } + + /** + * Executes queued send operations. + * + * @private + */ + dequeue() { + while (!this._deflating && this._queue.length) { + const params = this._queue.shift(); + + this._bufferedBytes -= params[1].length; + Reflect.apply(params[0], this, params.slice(1)); + } + } + + /** + * Enqueues a send operation. + * + * @param {Array} params Send operation parameters. + * @private + */ + enqueue(params) { + this._bufferedBytes += params[1].length; + this._queue.push(params); + } + + /** + * Sends a frame. + * + * @param {Buffer[]} list The frame to send + * @param {Function} [cb] Callback + * @private + */ + sendFrame(list, cb) { + if (list.length === 2) { + this._socket.cork(); + this._socket.write(list[0]); + this._socket.write(list[1], cb); + this._socket.uncork(); + } else { + this._socket.write(list[0], cb); + } + } +} + +module.exports = Sender; diff --git a/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/lib/stream.js b/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/lib/stream.js new file mode 100644 index 0000000..230734b --- /dev/null +++ b/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/lib/stream.js @@ -0,0 +1,159 @@ +'use strict'; + +const { Duplex } = require('stream'); + +/** + * Emits the `'close'` event on a stream. + * + * @param {Duplex} stream The stream. + * @private + */ +function emitClose(stream) { + stream.emit('close'); +} + +/** + * The listener of the `'end'` event. + * + * @private + */ +function duplexOnEnd() { + if (!this.destroyed && this._writableState.finished) { + this.destroy(); + } +} + +/** + * The listener of the `'error'` event. + * + * @param {Error} err The error + * @private + */ +function duplexOnError(err) { + this.removeListener('error', duplexOnError); + this.destroy(); + if (this.listenerCount('error') === 0) { + // Do not suppress the throwing behavior. + this.emit('error', err); + } +} + +/** + * Wraps a `WebSocket` in a duplex stream. + * + * @param {WebSocket} ws The `WebSocket` to wrap + * @param {Object} [options] The options for the `Duplex` constructor + * @return {Duplex} The duplex stream + * @public + */ +function createWebSocketStream(ws, options) { + let terminateOnDestroy = true; + + const duplex = new Duplex({ + ...options, + autoDestroy: false, + emitClose: false, + objectMode: false, + writableObjectMode: false + }); + + ws.on('message', function message(msg, isBinary) { + const data = + !isBinary && duplex._readableState.objectMode ? msg.toString() : msg; + + if (!duplex.push(data)) ws.pause(); + }); + + ws.once('error', function error(err) { + if (duplex.destroyed) return; + + // Prevent `ws.terminate()` from being called by `duplex._destroy()`. + // + // - If the `'error'` event is emitted before the `'open'` event, then + // `ws.terminate()` is a noop as no socket is assigned. + // - Otherwise, the error is re-emitted by the listener of the `'error'` + // event of the `Receiver` object. The listener already closes the + // connection by calling `ws.close()`. This allows a close frame to be + // sent to the other peer. If `ws.terminate()` is called right after this, + // then the close frame might not be sent. + terminateOnDestroy = false; + duplex.destroy(err); + }); + + ws.once('close', function close() { + if (duplex.destroyed) return; + + duplex.push(null); + }); + + duplex._destroy = function (err, callback) { + if (ws.readyState === ws.CLOSED) { + callback(err); + process.nextTick(emitClose, duplex); + return; + } + + let called = false; + + ws.once('error', function error(err) { + called = true; + callback(err); + }); + + ws.once('close', function close() { + if (!called) callback(err); + process.nextTick(emitClose, duplex); + }); + + if (terminateOnDestroy) ws.terminate(); + }; + + duplex._final = function (callback) { + if (ws.readyState === ws.CONNECTING) { + ws.once('open', function open() { + duplex._final(callback); + }); + return; + } + + // If the value of the `_socket` property is `null` it means that `ws` is a + // client websocket and the handshake failed. In fact, when this happens, a + // socket is never assigned to the websocket. Wait for the `'error'` event + // that will be emitted by the websocket. + if (ws._socket === null) return; + + if (ws._socket._writableState.finished) { + callback(); + if (duplex._readableState.endEmitted) duplex.destroy(); + } else { + ws._socket.once('finish', function finish() { + // `duplex` is not destroyed here because the `'end'` event will be + // emitted on `duplex` after this `'finish'` event. The EOF signaling + // `null` chunk is, in fact, pushed when the websocket emits `'close'`. + callback(); + }); + ws.close(); + } + }; + + duplex._read = function () { + if (ws.isPaused) ws.resume(); + }; + + duplex._write = function (chunk, encoding, callback) { + if (ws.readyState === ws.CONNECTING) { + ws.once('open', function open() { + duplex._write(chunk, encoding, callback); + }); + return; + } + + ws.send(chunk, callback); + }; + + duplex.on('end', duplexOnEnd); + duplex.on('error', duplexOnError); + return duplex; +} + +module.exports = createWebSocketStream; diff --git a/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/lib/subprotocol.js b/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/lib/subprotocol.js new file mode 100644 index 0000000..d4381e8 --- /dev/null +++ b/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/lib/subprotocol.js @@ -0,0 +1,62 @@ +'use strict'; + +const { tokenChars } = require('./validation'); + +/** + * Parses the `Sec-WebSocket-Protocol` header into a set of subprotocol names. + * + * @param {String} header The field value of the header + * @return {Set} The subprotocol names + * @public + */ +function parse(header) { + const protocols = new Set(); + let start = -1; + let end = -1; + let i = 0; + + for (i; i < header.length; i++) { + const code = header.charCodeAt(i); + + if (end === -1 && tokenChars[code] === 1) { + if (start === -1) start = i; + } else if ( + i !== 0 && + (code === 0x20 /* ' ' */ || code === 0x09) /* '\t' */ + ) { + if (end === -1 && start !== -1) end = i; + } else if (code === 0x2c /* ',' */) { + if (start === -1) { + throw new SyntaxError(`Unexpected character at index ${i}`); + } + + if (end === -1) end = i; + + const protocol = header.slice(start, end); + + if (protocols.has(protocol)) { + throw new SyntaxError(`The "${protocol}" subprotocol is duplicated`); + } + + protocols.add(protocol); + start = end = -1; + } else { + throw new SyntaxError(`Unexpected character at index ${i}`); + } + } + + if (start === -1 || end !== -1) { + throw new SyntaxError('Unexpected end of input'); + } + + const protocol = header.slice(start, i); + + if (protocols.has(protocol)) { + throw new SyntaxError(`The "${protocol}" subprotocol is duplicated`); + } + + protocols.add(protocol); + return protocols; +} + +module.exports = { parse }; diff --git a/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/lib/validation.js b/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/lib/validation.js new file mode 100644 index 0000000..ed98c75 --- /dev/null +++ b/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/lib/validation.js @@ -0,0 +1,124 @@ +'use strict'; + +// +// Allowed token characters: +// +// '!', '#', '$', '%', '&', ''', '*', '+', '-', +// '.', 0-9, A-Z, '^', '_', '`', a-z, '|', '~' +// +// tokenChars[32] === 0 // ' ' +// tokenChars[33] === 1 // '!' +// tokenChars[34] === 0 // '"' +// ... +// +// prettier-ignore +const tokenChars = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 - 15 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31 + 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, // 32 - 47 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48 - 63 + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64 - 79 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, // 80 - 95 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96 - 111 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0 // 112 - 127 +]; + +/** + * Checks if a status code is allowed in a close frame. + * + * @param {Number} code The status code + * @return {Boolean} `true` if the status code is valid, else `false` + * @public + */ +function isValidStatusCode(code) { + return ( + (code >= 1000 && + code <= 1014 && + code !== 1004 && + code !== 1005 && + code !== 1006) || + (code >= 3000 && code <= 4999) + ); +} + +/** + * Checks if a given buffer contains only correct UTF-8. + * Ported from https://www.cl.cam.ac.uk/%7Emgk25/ucs/utf8_check.c by + * Markus Kuhn. + * + * @param {Buffer} buf The buffer to check + * @return {Boolean} `true` if `buf` contains only correct UTF-8, else `false` + * @public + */ +function _isValidUTF8(buf) { + const len = buf.length; + let i = 0; + + while (i < len) { + if ((buf[i] & 0x80) === 0) { + // 0xxxxxxx + i++; + } else if ((buf[i] & 0xe0) === 0xc0) { + // 110xxxxx 10xxxxxx + if ( + i + 1 === len || + (buf[i + 1] & 0xc0) !== 0x80 || + (buf[i] & 0xfe) === 0xc0 // Overlong + ) { + return false; + } + + i += 2; + } else if ((buf[i] & 0xf0) === 0xe0) { + // 1110xxxx 10xxxxxx 10xxxxxx + if ( + i + 2 >= len || + (buf[i + 1] & 0xc0) !== 0x80 || + (buf[i + 2] & 0xc0) !== 0x80 || + (buf[i] === 0xe0 && (buf[i + 1] & 0xe0) === 0x80) || // Overlong + (buf[i] === 0xed && (buf[i + 1] & 0xe0) === 0xa0) // Surrogate (U+D800 - U+DFFF) + ) { + return false; + } + + i += 3; + } else if ((buf[i] & 0xf8) === 0xf0) { + // 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + if ( + i + 3 >= len || + (buf[i + 1] & 0xc0) !== 0x80 || + (buf[i + 2] & 0xc0) !== 0x80 || + (buf[i + 3] & 0xc0) !== 0x80 || + (buf[i] === 0xf0 && (buf[i + 1] & 0xf0) === 0x80) || // Overlong + (buf[i] === 0xf4 && buf[i + 1] > 0x8f) || + buf[i] > 0xf4 // > U+10FFFF + ) { + return false; + } + + i += 4; + } else { + return false; + } + } + + return true; +} + +try { + const isValidUTF8 = require('utf-8-validate'); + + module.exports = { + isValidStatusCode, + isValidUTF8(buf) { + return buf.length < 150 ? _isValidUTF8(buf) : isValidUTF8(buf); + }, + tokenChars + }; +} catch (e) /* istanbul ignore next */ { + module.exports = { + isValidStatusCode, + isValidUTF8: _isValidUTF8, + tokenChars + }; +} diff --git a/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/lib/websocket-server.js b/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/lib/websocket-server.js new file mode 100644 index 0000000..3c7939f --- /dev/null +++ b/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/lib/websocket-server.js @@ -0,0 +1,485 @@ +/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^net|tls|https$" }] */ + +'use strict'; + +const EventEmitter = require('events'); +const http = require('http'); +const https = require('https'); +const net = require('net'); +const tls = require('tls'); +const { createHash } = require('crypto'); + +const extension = require('./extension'); +const PerMessageDeflate = require('./permessage-deflate'); +const subprotocol = require('./subprotocol'); +const WebSocket = require('./websocket'); +const { GUID, kWebSocket } = require('./constants'); + +const keyRegex = /^[+/0-9A-Za-z]{22}==$/; + +const RUNNING = 0; +const CLOSING = 1; +const CLOSED = 2; + +/** + * Class representing a WebSocket server. + * + * @extends EventEmitter + */ +class WebSocketServer extends EventEmitter { + /** + * Create a `WebSocketServer` instance. + * + * @param {Object} options Configuration options + * @param {Number} [options.backlog=511] The maximum length of the queue of + * pending connections + * @param {Boolean} [options.clientTracking=true] Specifies whether or not to + * track clients + * @param {Function} [options.handleProtocols] A hook to handle protocols + * @param {String} [options.host] The hostname where to bind the server + * @param {Number} [options.maxPayload=104857600] The maximum allowed message + * size + * @param {Boolean} [options.noServer=false] Enable no server mode + * @param {String} [options.path] Accept only connections matching this path + * @param {(Boolean|Object)} [options.perMessageDeflate=false] Enable/disable + * permessage-deflate + * @param {Number} [options.port] The port where to bind the server + * @param {(http.Server|https.Server)} [options.server] A pre-created HTTP/S + * server to use + * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or + * not to skip UTF-8 validation for text and close messages + * @param {Function} [options.verifyClient] A hook to reject connections + * @param {Function} [callback] A listener for the `listening` event + */ + constructor(options, callback) { + super(); + + options = { + maxPayload: 100 * 1024 * 1024, + skipUTF8Validation: false, + perMessageDeflate: false, + handleProtocols: null, + clientTracking: true, + verifyClient: null, + noServer: false, + backlog: null, // use default (511 as implemented in net.js) + server: null, + host: null, + path: null, + port: null, + ...options + }; + + if ( + (options.port == null && !options.server && !options.noServer) || + (options.port != null && (options.server || options.noServer)) || + (options.server && options.noServer) + ) { + throw new TypeError( + 'One and only one of the "port", "server", or "noServer" options ' + + 'must be specified' + ); + } + + if (options.port != null) { + this._server = http.createServer((req, res) => { + const body = http.STATUS_CODES[426]; + + res.writeHead(426, { + 'Content-Length': body.length, + 'Content-Type': 'text/plain' + }); + res.end(body); + }); + this._server.listen( + options.port, + options.host, + options.backlog, + callback + ); + } else if (options.server) { + this._server = options.server; + } + + if (this._server) { + const emitConnection = this.emit.bind(this, 'connection'); + + this._removeListeners = addListeners(this._server, { + listening: this.emit.bind(this, 'listening'), + error: this.emit.bind(this, 'error'), + upgrade: (req, socket, head) => { + this.handleUpgrade(req, socket, head, emitConnection); + } + }); + } + + if (options.perMessageDeflate === true) options.perMessageDeflate = {}; + if (options.clientTracking) { + this.clients = new Set(); + this._shouldEmitClose = false; + } + + this.options = options; + this._state = RUNNING; + } + + /** + * Returns the bound address, the address family name, and port of the server + * as reported by the operating system if listening on an IP socket. + * If the server is listening on a pipe or UNIX domain socket, the name is + * returned as a string. + * + * @return {(Object|String|null)} The address of the server + * @public + */ + address() { + if (this.options.noServer) { + throw new Error('The server is operating in "noServer" mode'); + } + + if (!this._server) return null; + return this._server.address(); + } + + /** + * Stop the server from accepting new connections and emit the `'close'` event + * when all existing connections are closed. + * + * @param {Function} [cb] A one-time listener for the `'close'` event + * @public + */ + close(cb) { + if (this._state === CLOSED) { + if (cb) { + this.once('close', () => { + cb(new Error('The server is not running')); + }); + } + + process.nextTick(emitClose, this); + return; + } + + if (cb) this.once('close', cb); + + if (this._state === CLOSING) return; + this._state = CLOSING; + + if (this.options.noServer || this.options.server) { + if (this._server) { + this._removeListeners(); + this._removeListeners = this._server = null; + } + + if (this.clients) { + if (!this.clients.size) { + process.nextTick(emitClose, this); + } else { + this._shouldEmitClose = true; + } + } else { + process.nextTick(emitClose, this); + } + } else { + const server = this._server; + + this._removeListeners(); + this._removeListeners = this._server = null; + + // + // The HTTP/S server was created internally. Close it, and rely on its + // `'close'` event. + // + server.close(() => { + emitClose(this); + }); + } + } + + /** + * See if a given request should be handled by this server instance. + * + * @param {http.IncomingMessage} req Request object to inspect + * @return {Boolean} `true` if the request is valid, else `false` + * @public + */ + shouldHandle(req) { + if (this.options.path) { + const index = req.url.indexOf('?'); + const pathname = index !== -1 ? req.url.slice(0, index) : req.url; + + if (pathname !== this.options.path) return false; + } + + return true; + } + + /** + * Handle a HTTP Upgrade request. + * + * @param {http.IncomingMessage} req The request object + * @param {(net.Socket|tls.Socket)} socket The network socket between the + * server and client + * @param {Buffer} head The first packet of the upgraded stream + * @param {Function} cb Callback + * @public + */ + handleUpgrade(req, socket, head, cb) { + socket.on('error', socketOnError); + + const key = + req.headers['sec-websocket-key'] !== undefined + ? req.headers['sec-websocket-key'] + : false; + const version = +req.headers['sec-websocket-version']; + + if ( + req.method !== 'GET' || + req.headers.upgrade.toLowerCase() !== 'websocket' || + !key || + !keyRegex.test(key) || + (version !== 8 && version !== 13) || + !this.shouldHandle(req) + ) { + return abortHandshake(socket, 400); + } + + const secWebSocketProtocol = req.headers['sec-websocket-protocol']; + let protocols = new Set(); + + if (secWebSocketProtocol !== undefined) { + try { + protocols = subprotocol.parse(secWebSocketProtocol); + } catch (err) { + return abortHandshake(socket, 400); + } + } + + const secWebSocketExtensions = req.headers['sec-websocket-extensions']; + const extensions = {}; + + if ( + this.options.perMessageDeflate && + secWebSocketExtensions !== undefined + ) { + const perMessageDeflate = new PerMessageDeflate( + this.options.perMessageDeflate, + true, + this.options.maxPayload + ); + + try { + const offers = extension.parse(secWebSocketExtensions); + + if (offers[PerMessageDeflate.extensionName]) { + perMessageDeflate.accept(offers[PerMessageDeflate.extensionName]); + extensions[PerMessageDeflate.extensionName] = perMessageDeflate; + } + } catch (err) { + return abortHandshake(socket, 400); + } + } + + // + // Optionally call external client verification handler. + // + if (this.options.verifyClient) { + const info = { + origin: + req.headers[`${version === 8 ? 'sec-websocket-origin' : 'origin'}`], + secure: !!(req.socket.authorized || req.socket.encrypted), + req + }; + + if (this.options.verifyClient.length === 2) { + this.options.verifyClient(info, (verified, code, message, headers) => { + if (!verified) { + return abortHandshake(socket, code || 401, message, headers); + } + + this.completeUpgrade( + extensions, + key, + protocols, + req, + socket, + head, + cb + ); + }); + return; + } + + if (!this.options.verifyClient(info)) return abortHandshake(socket, 401); + } + + this.completeUpgrade(extensions, key, protocols, req, socket, head, cb); + } + + /** + * Upgrade the connection to WebSocket. + * + * @param {Object} extensions The accepted extensions + * @param {String} key The value of the `Sec-WebSocket-Key` header + * @param {Set} protocols The subprotocols + * @param {http.IncomingMessage} req The request object + * @param {(net.Socket|tls.Socket)} socket The network socket between the + * server and client + * @param {Buffer} head The first packet of the upgraded stream + * @param {Function} cb Callback + * @throws {Error} If called more than once with the same socket + * @private + */ + completeUpgrade(extensions, key, protocols, req, socket, head, cb) { + // + // Destroy the socket if the client has already sent a FIN packet. + // + if (!socket.readable || !socket.writable) return socket.destroy(); + + if (socket[kWebSocket]) { + throw new Error( + 'server.handleUpgrade() was called more than once with the same ' + + 'socket, possibly due to a misconfiguration' + ); + } + + if (this._state > RUNNING) return abortHandshake(socket, 503); + + const digest = createHash('sha1') + .update(key + GUID) + .digest('base64'); + + const headers = [ + 'HTTP/1.1 101 Switching Protocols', + 'Upgrade: websocket', + 'Connection: Upgrade', + `Sec-WebSocket-Accept: ${digest}` + ]; + + const ws = new WebSocket(null); + + if (protocols.size) { + // + // Optionally call external protocol selection handler. + // + const protocol = this.options.handleProtocols + ? this.options.handleProtocols(protocols, req) + : protocols.values().next().value; + + if (protocol) { + headers.push(`Sec-WebSocket-Protocol: ${protocol}`); + ws._protocol = protocol; + } + } + + if (extensions[PerMessageDeflate.extensionName]) { + const params = extensions[PerMessageDeflate.extensionName].params; + const value = extension.format({ + [PerMessageDeflate.extensionName]: [params] + }); + headers.push(`Sec-WebSocket-Extensions: ${value}`); + ws._extensions = extensions; + } + + // + // Allow external modification/inspection of handshake headers. + // + this.emit('headers', headers, req); + + socket.write(headers.concat('\r\n').join('\r\n')); + socket.removeListener('error', socketOnError); + + ws.setSocket(socket, head, { + maxPayload: this.options.maxPayload, + skipUTF8Validation: this.options.skipUTF8Validation + }); + + if (this.clients) { + this.clients.add(ws); + ws.on('close', () => { + this.clients.delete(ws); + + if (this._shouldEmitClose && !this.clients.size) { + process.nextTick(emitClose, this); + } + }); + } + + cb(ws, req); + } +} + +module.exports = WebSocketServer; + +/** + * Add event listeners on an `EventEmitter` using a map of + * pairs. + * + * @param {EventEmitter} server The event emitter + * @param {Object.} map The listeners to add + * @return {Function} A function that will remove the added listeners when + * called + * @private + */ +function addListeners(server, map) { + for (const event of Object.keys(map)) server.on(event, map[event]); + + return function removeListeners() { + for (const event of Object.keys(map)) { + server.removeListener(event, map[event]); + } + }; +} + +/** + * Emit a `'close'` event on an `EventEmitter`. + * + * @param {EventEmitter} server The event emitter + * @private + */ +function emitClose(server) { + server._state = CLOSED; + server.emit('close'); +} + +/** + * Handle premature socket errors. + * + * @private + */ +function socketOnError() { + this.destroy(); +} + +/** + * Close the connection when preconditions are not fulfilled. + * + * @param {(net.Socket|tls.Socket)} socket The socket of the upgrade request + * @param {Number} code The HTTP response status code + * @param {String} [message] The HTTP response body + * @param {Object} [headers] Additional HTTP response headers + * @private + */ +function abortHandshake(socket, code, message, headers) { + if (socket.writable) { + message = message || http.STATUS_CODES[code]; + headers = { + Connection: 'close', + 'Content-Type': 'text/html', + 'Content-Length': Buffer.byteLength(message), + ...headers + }; + + socket.write( + `HTTP/1.1 ${code} ${http.STATUS_CODES[code]}\r\n` + + Object.keys(headers) + .map((h) => `${h}: ${headers[h]}`) + .join('\r\n') + + '\r\n\r\n' + + message + ); + } + + socket.removeListener('error', socketOnError); + socket.destroy(); +} diff --git a/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/lib/websocket.js b/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/lib/websocket.js new file mode 100644 index 0000000..130b3dc --- /dev/null +++ b/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/lib/websocket.js @@ -0,0 +1,1220 @@ +/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^Readable$" }] */ + +'use strict'; + +const EventEmitter = require('events'); +const https = require('https'); +const http = require('http'); +const net = require('net'); +const tls = require('tls'); +const { randomBytes, createHash } = require('crypto'); +const { Readable } = require('stream'); +const { URL } = require('url'); + +const PerMessageDeflate = require('./permessage-deflate'); +const Receiver = require('./receiver'); +const Sender = require('./sender'); +const { + BINARY_TYPES, + EMPTY_BUFFER, + GUID, + kForOnEventAttribute, + kListener, + kStatusCode, + kWebSocket, + NOOP +} = require('./constants'); +const { + EventTarget: { addEventListener, removeEventListener } +} = require('./event-target'); +const { format, parse } = require('./extension'); +const { toBuffer } = require('./buffer-util'); + +const readyStates = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED']; +const subprotocolRegex = /^[!#$%&'*+\-.0-9A-Z^_`|a-z~]+$/; +const protocolVersions = [8, 13]; +const closeTimeout = 30 * 1000; + +/** + * Class representing a WebSocket. + * + * @extends EventEmitter + */ +class WebSocket extends EventEmitter { + /** + * Create a new `WebSocket`. + * + * @param {(String|URL)} address The URL to which to connect + * @param {(String|String[])} [protocols] The subprotocols + * @param {Object} [options] Connection options + */ + constructor(address, protocols, options) { + super(); + + this._binaryType = BINARY_TYPES[0]; + this._closeCode = 1006; + this._closeFrameReceived = false; + this._closeFrameSent = false; + this._closeMessage = EMPTY_BUFFER; + this._closeTimer = null; + this._extensions = {}; + this._paused = false; + this._protocol = ''; + this._readyState = WebSocket.CONNECTING; + this._receiver = null; + this._sender = null; + this._socket = null; + + if (address !== null) { + this._bufferedAmount = 0; + this._isServer = false; + this._redirects = 0; + + if (protocols === undefined) { + protocols = []; + } else if (!Array.isArray(protocols)) { + if (typeof protocols === 'object' && protocols !== null) { + options = protocols; + protocols = []; + } else { + protocols = [protocols]; + } + } + + initAsClient(this, address, protocols, options); + } else { + this._isServer = true; + } + } + + /** + * This deviates from the WHATWG interface since ws doesn't support the + * required default "blob" type (instead we define a custom "nodebuffer" + * type). + * + * @type {String} + */ + get binaryType() { + return this._binaryType; + } + + set binaryType(type) { + if (!BINARY_TYPES.includes(type)) return; + + this._binaryType = type; + + // + // Allow to change `binaryType` on the fly. + // + if (this._receiver) this._receiver._binaryType = type; + } + + /** + * @type {Number} + */ + get bufferedAmount() { + if (!this._socket) return this._bufferedAmount; + + return this._socket._writableState.length + this._sender._bufferedBytes; + } + + /** + * @type {String} + */ + get extensions() { + return Object.keys(this._extensions).join(); + } + + /** + * @type {Boolean} + */ + get isPaused() { + return this._paused; + } + + /** + * @type {Function} + */ + /* istanbul ignore next */ + get onclose() { + return null; + } + + /** + * @type {Function} + */ + /* istanbul ignore next */ + get onerror() { + return null; + } + + /** + * @type {Function} + */ + /* istanbul ignore next */ + get onopen() { + return null; + } + + /** + * @type {Function} + */ + /* istanbul ignore next */ + get onmessage() { + return null; + } + + /** + * @type {String} + */ + get protocol() { + return this._protocol; + } + + /** + * @type {Number} + */ + get readyState() { + return this._readyState; + } + + /** + * @type {String} + */ + get url() { + return this._url; + } + + /** + * Set up the socket and the internal resources. + * + * @param {(net.Socket|tls.Socket)} socket The network socket between the + * server and client + * @param {Buffer} head The first packet of the upgraded stream + * @param {Object} options Options object + * @param {Number} [options.maxPayload=0] The maximum allowed message size + * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or + * not to skip UTF-8 validation for text and close messages + * @private + */ + setSocket(socket, head, options) { + const receiver = new Receiver({ + binaryType: this.binaryType, + extensions: this._extensions, + isServer: this._isServer, + maxPayload: options.maxPayload, + skipUTF8Validation: options.skipUTF8Validation + }); + + this._sender = new Sender(socket, this._extensions); + this._receiver = receiver; + this._socket = socket; + + receiver[kWebSocket] = this; + socket[kWebSocket] = this; + + receiver.on('conclude', receiverOnConclude); + receiver.on('drain', receiverOnDrain); + receiver.on('error', receiverOnError); + receiver.on('message', receiverOnMessage); + receiver.on('ping', receiverOnPing); + receiver.on('pong', receiverOnPong); + + socket.setTimeout(0); + socket.setNoDelay(); + + if (head.length > 0) socket.unshift(head); + + socket.on('close', socketOnClose); + socket.on('data', socketOnData); + socket.on('end', socketOnEnd); + socket.on('error', socketOnError); + + this._readyState = WebSocket.OPEN; + this.emit('open'); + } + + /** + * Emit the `'close'` event. + * + * @private + */ + emitClose() { + if (!this._socket) { + this._readyState = WebSocket.CLOSED; + this.emit('close', this._closeCode, this._closeMessage); + return; + } + + if (this._extensions[PerMessageDeflate.extensionName]) { + this._extensions[PerMessageDeflate.extensionName].cleanup(); + } + + this._receiver.removeAllListeners(); + this._readyState = WebSocket.CLOSED; + this.emit('close', this._closeCode, this._closeMessage); + } + + /** + * Start a closing handshake. + * + * +----------+ +-----------+ +----------+ + * - - -|ws.close()|-->|close frame|-->|ws.close()|- - - + * | +----------+ +-----------+ +----------+ | + * +----------+ +-----------+ | + * CLOSING |ws.close()|<--|close frame|<--+-----+ CLOSING + * +----------+ +-----------+ | + * | | | +---+ | + * +------------------------+-->|fin| - - - - + * | +---+ | +---+ + * - - - - -|fin|<---------------------+ + * +---+ + * + * @param {Number} [code] Status code explaining why the connection is closing + * @param {(String|Buffer)} [data] The reason why the connection is + * closing + * @public + */ + close(code, data) { + if (this.readyState === WebSocket.CLOSED) return; + if (this.readyState === WebSocket.CONNECTING) { + const msg = 'WebSocket was closed before the connection was established'; + return abortHandshake(this, this._req, msg); + } + + if (this.readyState === WebSocket.CLOSING) { + if ( + this._closeFrameSent && + (this._closeFrameReceived || this._receiver._writableState.errorEmitted) + ) { + this._socket.end(); + } + + return; + } + + this._readyState = WebSocket.CLOSING; + this._sender.close(code, data, !this._isServer, (err) => { + // + // This error is handled by the `'error'` listener on the socket. We only + // want to know if the close frame has been sent here. + // + if (err) return; + + this._closeFrameSent = true; + + if ( + this._closeFrameReceived || + this._receiver._writableState.errorEmitted + ) { + this._socket.end(); + } + }); + + // + // Specify a timeout for the closing handshake to complete. + // + this._closeTimer = setTimeout( + this._socket.destroy.bind(this._socket), + closeTimeout + ); + } + + /** + * Pause the socket. + * + * @public + */ + pause() { + if ( + this.readyState === WebSocket.CONNECTING || + this.readyState === WebSocket.CLOSED + ) { + return; + } + + this._paused = true; + this._socket.pause(); + } + + /** + * Send a ping. + * + * @param {*} [data] The data to send + * @param {Boolean} [mask] Indicates whether or not to mask `data` + * @param {Function} [cb] Callback which is executed when the ping is sent + * @public + */ + ping(data, mask, cb) { + if (this.readyState === WebSocket.CONNECTING) { + throw new Error('WebSocket is not open: readyState 0 (CONNECTING)'); + } + + if (typeof data === 'function') { + cb = data; + data = mask = undefined; + } else if (typeof mask === 'function') { + cb = mask; + mask = undefined; + } + + if (typeof data === 'number') data = data.toString(); + + if (this.readyState !== WebSocket.OPEN) { + sendAfterClose(this, data, cb); + return; + } + + if (mask === undefined) mask = !this._isServer; + this._sender.ping(data || EMPTY_BUFFER, mask, cb); + } + + /** + * Send a pong. + * + * @param {*} [data] The data to send + * @param {Boolean} [mask] Indicates whether or not to mask `data` + * @param {Function} [cb] Callback which is executed when the pong is sent + * @public + */ + pong(data, mask, cb) { + if (this.readyState === WebSocket.CONNECTING) { + throw new Error('WebSocket is not open: readyState 0 (CONNECTING)'); + } + + if (typeof data === 'function') { + cb = data; + data = mask = undefined; + } else if (typeof mask === 'function') { + cb = mask; + mask = undefined; + } + + if (typeof data === 'number') data = data.toString(); + + if (this.readyState !== WebSocket.OPEN) { + sendAfterClose(this, data, cb); + return; + } + + if (mask === undefined) mask = !this._isServer; + this._sender.pong(data || EMPTY_BUFFER, mask, cb); + } + + /** + * Resume the socket. + * + * @public + */ + resume() { + if ( + this.readyState === WebSocket.CONNECTING || + this.readyState === WebSocket.CLOSED + ) { + return; + } + + this._paused = false; + if (!this._receiver._writableState.needDrain) this._socket.resume(); + } + + /** + * Send a data message. + * + * @param {*} data The message to send + * @param {Object} [options] Options object + * @param {Boolean} [options.binary] Specifies whether `data` is binary or + * text + * @param {Boolean} [options.compress] Specifies whether or not to compress + * `data` + * @param {Boolean} [options.fin=true] Specifies whether the fragment is the + * last one + * @param {Boolean} [options.mask] Specifies whether or not to mask `data` + * @param {Function} [cb] Callback which is executed when data is written out + * @public + */ + send(data, options, cb) { + if (this.readyState === WebSocket.CONNECTING) { + throw new Error('WebSocket is not open: readyState 0 (CONNECTING)'); + } + + if (typeof options === 'function') { + cb = options; + options = {}; + } + + if (typeof data === 'number') data = data.toString(); + + if (this.readyState !== WebSocket.OPEN) { + sendAfterClose(this, data, cb); + return; + } + + const opts = { + binary: typeof data !== 'string', + mask: !this._isServer, + compress: true, + fin: true, + ...options + }; + + if (!this._extensions[PerMessageDeflate.extensionName]) { + opts.compress = false; + } + + this._sender.send(data || EMPTY_BUFFER, opts, cb); + } + + /** + * Forcibly close the connection. + * + * @public + */ + terminate() { + if (this.readyState === WebSocket.CLOSED) return; + if (this.readyState === WebSocket.CONNECTING) { + const msg = 'WebSocket was closed before the connection was established'; + return abortHandshake(this, this._req, msg); + } + + if (this._socket) { + this._readyState = WebSocket.CLOSING; + this._socket.destroy(); + } + } +} + +/** + * @constant {Number} CONNECTING + * @memberof WebSocket + */ +Object.defineProperty(WebSocket, 'CONNECTING', { + enumerable: true, + value: readyStates.indexOf('CONNECTING') +}); + +/** + * @constant {Number} CONNECTING + * @memberof WebSocket.prototype + */ +Object.defineProperty(WebSocket.prototype, 'CONNECTING', { + enumerable: true, + value: readyStates.indexOf('CONNECTING') +}); + +/** + * @constant {Number} OPEN + * @memberof WebSocket + */ +Object.defineProperty(WebSocket, 'OPEN', { + enumerable: true, + value: readyStates.indexOf('OPEN') +}); + +/** + * @constant {Number} OPEN + * @memberof WebSocket.prototype + */ +Object.defineProperty(WebSocket.prototype, 'OPEN', { + enumerable: true, + value: readyStates.indexOf('OPEN') +}); + +/** + * @constant {Number} CLOSING + * @memberof WebSocket + */ +Object.defineProperty(WebSocket, 'CLOSING', { + enumerable: true, + value: readyStates.indexOf('CLOSING') +}); + +/** + * @constant {Number} CLOSING + * @memberof WebSocket.prototype + */ +Object.defineProperty(WebSocket.prototype, 'CLOSING', { + enumerable: true, + value: readyStates.indexOf('CLOSING') +}); + +/** + * @constant {Number} CLOSED + * @memberof WebSocket + */ +Object.defineProperty(WebSocket, 'CLOSED', { + enumerable: true, + value: readyStates.indexOf('CLOSED') +}); + +/** + * @constant {Number} CLOSED + * @memberof WebSocket.prototype + */ +Object.defineProperty(WebSocket.prototype, 'CLOSED', { + enumerable: true, + value: readyStates.indexOf('CLOSED') +}); + +[ + 'binaryType', + 'bufferedAmount', + 'extensions', + 'isPaused', + 'protocol', + 'readyState', + 'url' +].forEach((property) => { + Object.defineProperty(WebSocket.prototype, property, { enumerable: true }); +}); + +// +// Add the `onopen`, `onerror`, `onclose`, and `onmessage` attributes. +// See https://html.spec.whatwg.org/multipage/comms.html#the-websocket-interface +// +['open', 'error', 'close', 'message'].forEach((method) => { + Object.defineProperty(WebSocket.prototype, `on${method}`, { + enumerable: true, + get() { + for (const listener of this.listeners(method)) { + if (listener[kForOnEventAttribute]) return listener[kListener]; + } + + return null; + }, + set(handler) { + for (const listener of this.listeners(method)) { + if (listener[kForOnEventAttribute]) { + this.removeListener(method, listener); + break; + } + } + + if (typeof handler !== 'function') return; + + this.addEventListener(method, handler, { + [kForOnEventAttribute]: true + }); + } + }); +}); + +WebSocket.prototype.addEventListener = addEventListener; +WebSocket.prototype.removeEventListener = removeEventListener; + +module.exports = WebSocket; + +/** + * Initialize a WebSocket client. + * + * @param {WebSocket} websocket The client to initialize + * @param {(String|URL)} address The URL to which to connect + * @param {Array} protocols The subprotocols + * @param {Object} [options] Connection options + * @param {Boolean} [options.followRedirects=false] Whether or not to follow + * redirects + * @param {Number} [options.handshakeTimeout] Timeout in milliseconds for the + * handshake request + * @param {Number} [options.maxPayload=104857600] The maximum allowed message + * size + * @param {Number} [options.maxRedirects=10] The maximum number of redirects + * allowed + * @param {String} [options.origin] Value of the `Origin` or + * `Sec-WebSocket-Origin` header + * @param {(Boolean|Object)} [options.perMessageDeflate=true] Enable/disable + * permessage-deflate + * @param {Number} [options.protocolVersion=13] Value of the + * `Sec-WebSocket-Version` header + * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or + * not to skip UTF-8 validation for text and close messages + * @private + */ +function initAsClient(websocket, address, protocols, options) { + const opts = { + protocolVersion: protocolVersions[1], + maxPayload: 100 * 1024 * 1024, + skipUTF8Validation: false, + perMessageDeflate: true, + followRedirects: false, + maxRedirects: 10, + ...options, + createConnection: undefined, + socketPath: undefined, + hostname: undefined, + protocol: undefined, + timeout: undefined, + method: undefined, + host: undefined, + path: undefined, + port: undefined + }; + + if (!protocolVersions.includes(opts.protocolVersion)) { + throw new RangeError( + `Unsupported protocol version: ${opts.protocolVersion} ` + + `(supported versions: ${protocolVersions.join(', ')})` + ); + } + + let parsedUrl; + + if (address instanceof URL) { + parsedUrl = address; + websocket._url = address.href; + } else { + try { + parsedUrl = new URL(address); + } catch (e) { + throw new SyntaxError(`Invalid URL: ${address}`); + } + + websocket._url = address; + } + + const isSecure = parsedUrl.protocol === 'wss:'; + const isUnixSocket = parsedUrl.protocol === 'ws+unix:'; + let invalidURLMessage; + + if (parsedUrl.protocol !== 'ws:' && !isSecure && !isUnixSocket) { + invalidURLMessage = + 'The URL\'s protocol must be one of "ws:", "wss:", or "ws+unix:"'; + } else if (isUnixSocket && !parsedUrl.pathname) { + invalidURLMessage = "The URL's pathname is empty"; + } else if (parsedUrl.hash) { + invalidURLMessage = 'The URL contains a fragment identifier'; + } + + if (invalidURLMessage) { + const err = new SyntaxError(invalidURLMessage); + + if (websocket._redirects === 0) { + throw err; + } else { + emitErrorAndClose(websocket, err); + return; + } + } + + const defaultPort = isSecure ? 443 : 80; + const key = randomBytes(16).toString('base64'); + const get = isSecure ? https.get : http.get; + const protocolSet = new Set(); + let perMessageDeflate; + + opts.createConnection = isSecure ? tlsConnect : netConnect; + opts.defaultPort = opts.defaultPort || defaultPort; + opts.port = parsedUrl.port || defaultPort; + opts.host = parsedUrl.hostname.startsWith('[') + ? parsedUrl.hostname.slice(1, -1) + : parsedUrl.hostname; + opts.headers = { + 'Sec-WebSocket-Version': opts.protocolVersion, + 'Sec-WebSocket-Key': key, + Connection: 'Upgrade', + Upgrade: 'websocket', + ...opts.headers + }; + opts.path = parsedUrl.pathname + parsedUrl.search; + opts.timeout = opts.handshakeTimeout; + + if (opts.perMessageDeflate) { + perMessageDeflate = new PerMessageDeflate( + opts.perMessageDeflate !== true ? opts.perMessageDeflate : {}, + false, + opts.maxPayload + ); + opts.headers['Sec-WebSocket-Extensions'] = format({ + [PerMessageDeflate.extensionName]: perMessageDeflate.offer() + }); + } + if (protocols.length) { + for (const protocol of protocols) { + if ( + typeof protocol !== 'string' || + !subprotocolRegex.test(protocol) || + protocolSet.has(protocol) + ) { + throw new SyntaxError( + 'An invalid or duplicated subprotocol was specified' + ); + } + + protocolSet.add(protocol); + } + + opts.headers['Sec-WebSocket-Protocol'] = protocols.join(','); + } + if (opts.origin) { + if (opts.protocolVersion < 13) { + opts.headers['Sec-WebSocket-Origin'] = opts.origin; + } else { + opts.headers.Origin = opts.origin; + } + } + if (parsedUrl.username || parsedUrl.password) { + opts.auth = `${parsedUrl.username}:${parsedUrl.password}`; + } + + if (isUnixSocket) { + const parts = opts.path.split(':'); + + opts.socketPath = parts[0]; + opts.path = parts[1]; + } + + let req = (websocket._req = get(opts)); + + if (opts.timeout) { + req.on('timeout', () => { + abortHandshake(websocket, req, 'Opening handshake has timed out'); + }); + } + + req.on('error', (err) => { + if (req === null || req.aborted) return; + + req = websocket._req = null; + emitErrorAndClose(websocket, err); + }); + + req.on('response', (res) => { + const location = res.headers.location; + const statusCode = res.statusCode; + + if ( + location && + opts.followRedirects && + statusCode >= 300 && + statusCode < 400 + ) { + if (++websocket._redirects > opts.maxRedirects) { + abortHandshake(websocket, req, 'Maximum redirects exceeded'); + return; + } + + req.abort(); + + let addr; + + try { + addr = new URL(location, address); + } catch (e) { + const err = new SyntaxError(`Invalid URL: ${location}`); + emitErrorAndClose(websocket, err); + return; + } + + initAsClient(websocket, addr, protocols, options); + } else if (!websocket.emit('unexpected-response', req, res)) { + abortHandshake( + websocket, + req, + `Unexpected server response: ${res.statusCode}` + ); + } + }); + + req.on('upgrade', (res, socket, head) => { + websocket.emit('upgrade', res); + + // + // The user may have closed the connection from a listener of the `upgrade` + // event. + // + if (websocket.readyState !== WebSocket.CONNECTING) return; + + req = websocket._req = null; + + const digest = createHash('sha1') + .update(key + GUID) + .digest('base64'); + + if (res.headers['sec-websocket-accept'] !== digest) { + abortHandshake(websocket, socket, 'Invalid Sec-WebSocket-Accept header'); + return; + } + + const serverProt = res.headers['sec-websocket-protocol']; + let protError; + + if (serverProt !== undefined) { + if (!protocolSet.size) { + protError = 'Server sent a subprotocol but none was requested'; + } else if (!protocolSet.has(serverProt)) { + protError = 'Server sent an invalid subprotocol'; + } + } else if (protocolSet.size) { + protError = 'Server sent no subprotocol'; + } + + if (protError) { + abortHandshake(websocket, socket, protError); + return; + } + + if (serverProt) websocket._protocol = serverProt; + + const secWebSocketExtensions = res.headers['sec-websocket-extensions']; + + if (secWebSocketExtensions !== undefined) { + if (!perMessageDeflate) { + const message = + 'Server sent a Sec-WebSocket-Extensions header but no extension ' + + 'was requested'; + abortHandshake(websocket, socket, message); + return; + } + + let extensions; + + try { + extensions = parse(secWebSocketExtensions); + } catch (err) { + const message = 'Invalid Sec-WebSocket-Extensions header'; + abortHandshake(websocket, socket, message); + return; + } + + const extensionNames = Object.keys(extensions); + + if ( + extensionNames.length !== 1 || + extensionNames[0] !== PerMessageDeflate.extensionName + ) { + const message = 'Server indicated an extension that was not requested'; + abortHandshake(websocket, socket, message); + return; + } + + try { + perMessageDeflate.accept(extensions[PerMessageDeflate.extensionName]); + } catch (err) { + const message = 'Invalid Sec-WebSocket-Extensions header'; + abortHandshake(websocket, socket, message); + return; + } + + websocket._extensions[PerMessageDeflate.extensionName] = + perMessageDeflate; + } + + websocket.setSocket(socket, head, { + maxPayload: opts.maxPayload, + skipUTF8Validation: opts.skipUTF8Validation + }); + }); +} + +/** + * Emit the `'error'` and `'close'` event. + * + * @param {WebSocket} websocket The WebSocket instance + * @param {Error} The error to emit + * @private + */ +function emitErrorAndClose(websocket, err) { + websocket._readyState = WebSocket.CLOSING; + websocket.emit('error', err); + websocket.emitClose(); +} + +/** + * Create a `net.Socket` and initiate a connection. + * + * @param {Object} options Connection options + * @return {net.Socket} The newly created socket used to start the connection + * @private + */ +function netConnect(options) { + options.path = options.socketPath; + return net.connect(options); +} + +/** + * Create a `tls.TLSSocket` and initiate a connection. + * + * @param {Object} options Connection options + * @return {tls.TLSSocket} The newly created socket used to start the connection + * @private + */ +function tlsConnect(options) { + options.path = undefined; + + if (!options.servername && options.servername !== '') { + options.servername = net.isIP(options.host) ? '' : options.host; + } + + return tls.connect(options); +} + +/** + * Abort the handshake and emit an error. + * + * @param {WebSocket} websocket The WebSocket instance + * @param {(http.ClientRequest|net.Socket|tls.Socket)} stream The request to + * abort or the socket to destroy + * @param {String} message The error message + * @private + */ +function abortHandshake(websocket, stream, message) { + websocket._readyState = WebSocket.CLOSING; + + const err = new Error(message); + Error.captureStackTrace(err, abortHandshake); + + if (stream.setHeader) { + stream.abort(); + + if (stream.socket && !stream.socket.destroyed) { + // + // On Node.js >= 14.3.0 `request.abort()` does not destroy the socket if + // called after the request completed. See + // https://github.com/websockets/ws/issues/1869. + // + stream.socket.destroy(); + } + + stream.once('abort', websocket.emitClose.bind(websocket)); + websocket.emit('error', err); + } else { + stream.destroy(err); + stream.once('error', websocket.emit.bind(websocket, 'error')); + stream.once('close', websocket.emitClose.bind(websocket)); + } +} + +/** + * Handle cases where the `ping()`, `pong()`, or `send()` methods are called + * when the `readyState` attribute is `CLOSING` or `CLOSED`. + * + * @param {WebSocket} websocket The WebSocket instance + * @param {*} [data] The data to send + * @param {Function} [cb] Callback + * @private + */ +function sendAfterClose(websocket, data, cb) { + if (data) { + const length = toBuffer(data).length; + + // + // The `_bufferedAmount` property is used only when the peer is a client and + // the opening handshake fails. Under these circumstances, in fact, the + // `setSocket()` method is not called, so the `_socket` and `_sender` + // properties are set to `null`. + // + if (websocket._socket) websocket._sender._bufferedBytes += length; + else websocket._bufferedAmount += length; + } + + if (cb) { + const err = new Error( + `WebSocket is not open: readyState ${websocket.readyState} ` + + `(${readyStates[websocket.readyState]})` + ); + cb(err); + } +} + +/** + * The listener of the `Receiver` `'conclude'` event. + * + * @param {Number} code The status code + * @param {Buffer} reason The reason for closing + * @private + */ +function receiverOnConclude(code, reason) { + const websocket = this[kWebSocket]; + + websocket._closeFrameReceived = true; + websocket._closeMessage = reason; + websocket._closeCode = code; + + if (websocket._socket[kWebSocket] === undefined) return; + + websocket._socket.removeListener('data', socketOnData); + process.nextTick(resume, websocket._socket); + + if (code === 1005) websocket.close(); + else websocket.close(code, reason); +} + +/** + * The listener of the `Receiver` `'drain'` event. + * + * @private + */ +function receiverOnDrain() { + const websocket = this[kWebSocket]; + + if (!websocket.isPaused) websocket._socket.resume(); +} + +/** + * The listener of the `Receiver` `'error'` event. + * + * @param {(RangeError|Error)} err The emitted error + * @private + */ +function receiverOnError(err) { + const websocket = this[kWebSocket]; + + if (websocket._socket[kWebSocket] !== undefined) { + websocket._socket.removeListener('data', socketOnData); + + // + // On Node.js < 14.0.0 the `'error'` event is emitted synchronously. See + // https://github.com/websockets/ws/issues/1940. + // + process.nextTick(resume, websocket._socket); + + websocket.close(err[kStatusCode]); + } + + websocket.emit('error', err); +} + +/** + * The listener of the `Receiver` `'finish'` event. + * + * @private + */ +function receiverOnFinish() { + this[kWebSocket].emitClose(); +} + +/** + * The listener of the `Receiver` `'message'` event. + * + * @param {Buffer|ArrayBuffer|Buffer[])} data The message + * @param {Boolean} isBinary Specifies whether the message is binary or not + * @private + */ +function receiverOnMessage(data, isBinary) { + this[kWebSocket].emit('message', data, isBinary); +} + +/** + * The listener of the `Receiver` `'ping'` event. + * + * @param {Buffer} data The data included in the ping frame + * @private + */ +function receiverOnPing(data) { + const websocket = this[kWebSocket]; + + websocket.pong(data, !websocket._isServer, NOOP); + websocket.emit('ping', data); +} + +/** + * The listener of the `Receiver` `'pong'` event. + * + * @param {Buffer} data The data included in the pong frame + * @private + */ +function receiverOnPong(data) { + this[kWebSocket].emit('pong', data); +} + +/** + * Resume a readable stream + * + * @param {Readable} stream The readable stream + * @private + */ +function resume(stream) { + stream.resume(); +} + +/** + * The listener of the `net.Socket` `'close'` event. + * + * @private + */ +function socketOnClose() { + const websocket = this[kWebSocket]; + + this.removeListener('close', socketOnClose); + this.removeListener('data', socketOnData); + this.removeListener('end', socketOnEnd); + + websocket._readyState = WebSocket.CLOSING; + + let chunk; + + // + // The close frame might not have been received or the `'end'` event emitted, + // for example, if the socket was destroyed due to an error. Ensure that the + // `receiver` stream is closed after writing any remaining buffered data to + // it. If the readable side of the socket is in flowing mode then there is no + // buffered data as everything has been already written and `readable.read()` + // will return `null`. If instead, the socket is paused, any possible buffered + // data will be read as a single chunk. + // + if ( + !this._readableState.endEmitted && + !websocket._closeFrameReceived && + !websocket._receiver._writableState.errorEmitted && + (chunk = websocket._socket.read()) !== null + ) { + websocket._receiver.write(chunk); + } + + websocket._receiver.end(); + + this[kWebSocket] = undefined; + + clearTimeout(websocket._closeTimer); + + if ( + websocket._receiver._writableState.finished || + websocket._receiver._writableState.errorEmitted + ) { + websocket.emitClose(); + } else { + websocket._receiver.on('error', receiverOnFinish); + websocket._receiver.on('finish', receiverOnFinish); + } +} + +/** + * The listener of the `net.Socket` `'data'` event. + * + * @param {Buffer} chunk A chunk of data + * @private + */ +function socketOnData(chunk) { + if (!this[kWebSocket]._receiver.write(chunk)) { + this.pause(); + } +} + +/** + * The listener of the `net.Socket` `'end'` event. + * + * @private + */ +function socketOnEnd() { + const websocket = this[kWebSocket]; + + websocket._readyState = WebSocket.CLOSING; + websocket._receiver.end(); + this.end(); +} + +/** + * The listener of the `net.Socket` `'error'` event. + * + * @private + */ +function socketOnError() { + const websocket = this[kWebSocket]; + + this.removeListener('error', socketOnError); + this.on('error', NOOP); + + if (websocket) { + websocket._readyState = WebSocket.CLOSING; + this.destroy(); + } +} diff --git a/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/package.json b/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/package.json new file mode 100644 index 0000000..5e3d221 --- /dev/null +++ b/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/package.json @@ -0,0 +1,61 @@ +{ + "name": "ws", + "version": "8.3.0", + "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js", + "keywords": [ + "HyBi", + "Push", + "RFC-6455", + "WebSocket", + "WebSockets", + "real-time" + ], + "homepage": "https://github.com/websockets/ws", + "bugs": "https://github.com/websockets/ws/issues", + "repository": "websockets/ws", + "author": "Einar Otto Stangvik (http://2x.io)", + "license": "MIT", + "main": "index.js", + "exports": { + "import": "./wrapper.mjs", + "require": "./index.js" + }, + "browser": "browser.js", + "engines": { + "node": ">=10.0.0" + }, + "files": [ + "browser.js", + "index.js", + "lib/*.js", + "wrapper.mjs" + ], + "scripts": { + "test": "nyc --reporter=lcov --reporter=text mocha --throw-deprecation test/*.test.js", + "integration": "mocha --throw-deprecation test/*.integration.js", + "lint": "eslint --ignore-path .gitignore . && prettier --check --ignore-path .gitignore \"**/*.{json,md,yaml,yml}\"" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + }, + "devDependencies": { + "benchmark": "^2.1.4", + "bufferutil": "^4.0.1", + "eslint": "^8.0.0", + "eslint-config-prettier": "^8.1.0", + "eslint-plugin-prettier": "^4.0.0", + "mocha": "^8.4.0", + "nyc": "^15.0.0", + "prettier": "^2.0.5", + "utf-8-validate": "^5.0.2" + } +} diff --git a/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/wrapper.mjs b/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/wrapper.mjs new file mode 100644 index 0000000..7245ad1 --- /dev/null +++ b/06. 跨标签页通信/Websocket实现跨标签页通信/node_modules/ws/wrapper.mjs @@ -0,0 +1,8 @@ +import createWebSocketStream from './lib/stream.js'; +import Receiver from './lib/receiver.js'; +import Sender from './lib/sender.js'; +import WebSocket from './lib/websocket.js'; +import WebSocketServer from './lib/websocket-server.js'; + +export { createWebSocketStream, Receiver, Sender, WebSocket, WebSocketServer }; +export default WebSocket; diff --git a/06. 跨标签页通信/Websocket实现跨标签页通信/package-lock.json b/06. 跨标签页通信/Websocket实现跨标签页通信/package-lock.json new file mode 100644 index 0000000..dbc5baa --- /dev/null +++ b/06. 跨标签页通信/Websocket实现跨标签页通信/package-lock.json @@ -0,0 +1,44 @@ +{ + "name": "demo", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "demo", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "ws": "^8.3.0" + } + }, + "node_modules/ws": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.3.0.tgz", + "integrity": "sha512-Gs5EZtpqZzLvmIM59w4igITU57lrtYVFneaa434VROv4thzJyV6UjIL3D42lslWlI+D4KzLYnxSwtfuiO79sNw==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + }, + "dependencies": { + "ws": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.3.0.tgz", + "integrity": "sha512-Gs5EZtpqZzLvmIM59w4igITU57lrtYVFneaa434VROv4thzJyV6UjIL3D42lslWlI+D4KzLYnxSwtfuiO79sNw==", + "requires": {} + } + } +} diff --git a/06. 跨标签页通信/Websocket实现跨标签页通信/package.json b/06. 跨标签页通信/Websocket实现跨标签页通信/package.json new file mode 100644 index 0000000..7b279f6 --- /dev/null +++ b/06. 跨标签页通信/Websocket实现跨标签页通信/package.json @@ -0,0 +1,15 @@ +{ + "name": "demo", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "ws": "^8.3.0" + } +} diff --git a/06. 跨标签页通信/Websocket实现跨标签页通信/server.js b/06. 跨标签页通信/Websocket实现跨标签页通信/server.js new file mode 100644 index 0000000..77dbe75 --- /dev/null +++ b/06. 跨标签页通信/Websocket实现跨标签页通信/server.js @@ -0,0 +1,38 @@ + +// 首先获取到一个 WebSocketServer 类 +var WebSocketServer = require("ws").Server; + +// 创建 WebSocket 服务器 +var wss = new WebSocketServer({ + port : 3000 +}) + +// 该数组用于保存所有的客户端连接实例 +var clients = []; + +// 当客户端连接上 WebSocket 服务器的时候 +// 就会触发 connection 事件,该客户端的实例就会传入此回调函数 +wss.on("connection", function(client){ + // 将当前客户端连接实例保存到数组里面 + clients.push(client); + console.log(`当前有${clients.length}个客户端在线...`); + + // 给传入进来的客户端连接实例绑定一个 message 事件 + client.on('message', function(msg){ + console.log("收到的消息为:" + msg); + // 接下来需要将接收到的消息推送给其他所有的客户端 + for(var c of clients){ + if(c !== client){ + c.send(msg.toString()); + } + } + }) + + client.on('close',function(){ + var index = clients.indexOf(this); + clients.splice(index, 1); + console.log(`当前有${clients.length}个客户端在线...`); + }) +}) + +console.log("Web Socket 服务器已经启动...."); \ No newline at end of file diff --git a/06. 跨标签页通信/cookie实现跨标签页通信/.vscode/settings.json b/06. 跨标签页通信/cookie实现跨标签页通信/.vscode/settings.json new file mode 100644 index 0000000..5165b7c --- /dev/null +++ b/06. 跨标签页通信/cookie实现跨标签页通信/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "eggHelper.serverPort": 35684 +} \ No newline at end of file diff --git a/06. 跨标签页通信/cookie实现跨标签页通信/index.html b/06. 跨标签页通信/cookie实现跨标签页通信/index.html new file mode 100644 index 0000000..41c576e --- /dev/null +++ b/06. 跨标签页通信/cookie实现跨标签页通信/index.html @@ -0,0 +1,19 @@ + + + + + + + + 页面一 + + + + + + + \ No newline at end of file diff --git a/06. 跨标签页通信/cookie实现跨标签页通信/index2.html b/06. 跨标签页通信/cookie实现跨标签页通信/index2.html new file mode 100644 index 0000000..a738890 --- /dev/null +++ b/06. 跨标签页通信/cookie实现跨标签页通信/index2.html @@ -0,0 +1,24 @@ + + + + + + + + 页面二 + + + + + + + \ No newline at end of file diff --git a/06. 跨标签页通信/postMessage实现跨标签页通信/.vscode/settings.json b/06. 跨标签页通信/postMessage实现跨标签页通信/.vscode/settings.json new file mode 100644 index 0000000..5165b7c --- /dev/null +++ b/06. 跨标签页通信/postMessage实现跨标签页通信/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "eggHelper.serverPort": 35684 +} \ No newline at end of file diff --git a/06. 跨标签页通信/postMessage实现跨标签页通信/index.html b/06. 跨标签页通信/postMessage实现跨标签页通信/index.html new file mode 100644 index 0000000..b7663b4 --- /dev/null +++ b/06. 跨标签页通信/postMessage实现跨标签页通信/index.html @@ -0,0 +1,34 @@ + + + + + + + + 页面一 + + + + + + + + + + \ No newline at end of file diff --git a/06. 跨标签页通信/postMessage实现跨标签页通信/index2.html b/06. 跨标签页通信/postMessage实现跨标签页通信/index2.html new file mode 100644 index 0000000..d0b2a66 --- /dev/null +++ b/06. 跨标签页通信/postMessage实现跨标签页通信/index2.html @@ -0,0 +1,20 @@ + + + + + + + + 页面二 + + + +

这是页面二

+ + + + \ No newline at end of file diff --git a/06. 跨标签页通信/storage实现跨标签页通信/.vscode/settings.json b/06. 跨标签页通信/storage实现跨标签页通信/.vscode/settings.json new file mode 100644 index 0000000..5165b7c --- /dev/null +++ b/06. 跨标签页通信/storage实现跨标签页通信/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "eggHelper.serverPort": 35684 +} \ No newline at end of file diff --git a/06. 跨标签页通信/storage实现跨标签页通信/index.html b/06. 跨标签页通信/storage实现跨标签页通信/index.html new file mode 100644 index 0000000..9eb29f6 --- /dev/null +++ b/06. 跨标签页通信/storage实现跨标签页通信/index.html @@ -0,0 +1,19 @@ + + + + + + + + 页面一 + + + + + + + \ No newline at end of file diff --git a/06. 跨标签页通信/storage实现跨标签页通信/index2.html b/06. 跨标签页通信/storage实现跨标签页通信/index2.html new file mode 100644 index 0000000..22a374b --- /dev/null +++ b/06. 跨标签页通信/storage实现跨标签页通信/index2.html @@ -0,0 +1,23 @@ + + + + + + + + 页面二 + + + + + + + \ No newline at end of file diff --git a/06. 跨标签页通信/跨标签页通信.md b/06. 跨标签页通信/跨标签页通信.md new file mode 100644 index 0000000..0218c15 --- /dev/null +++ b/06. 跨标签页通信/跨标签页通信.md @@ -0,0 +1,662 @@ +# 跨标签页通信 + + + +本文主要包含以下内容: + +- 什么是跨标签页通信 +- 跨标签页通信常见方案 + - *BroadCast Channel* + - *Service Worker* + - *LocalStorage window.onstorage* 监听 + - *Shared Worker* 定时器轮询( *setInterval* ) + - *IndexedDB* 定时器轮询( *setInterval* ) + - *cookie* 定时器轮询( *setInterval* ) + - *window.open、window.postMessage* + - *Websocket* + + + +## 什么是跨标签页通信 + +面试的时候经常会被问到的一个关于浏览器的问题: + +>浏览器中如何实现跨标签页通信? + +要回答这个问题,首先需要搞懂什么叫做跨标签通信。 + +其实这个概念也不难理解,现在几乎所有的浏览器都支持多标签页的,我们可以在一个浏览器中打开多个标签页,每个标签页访问不同的网站内容。 + +image-20211204132442156 + + + +因此,跨标签页通信也就非常好理解了,简单来讲就是一个标签页能够发送信息给另一个标签页。 + +常见的跨标签页方案如下: + +- *BroadCast Channel* + +- *Service Worker* + +- *LocalStorage window.onstorage* 监听 + +- *Shared Worker* 定时器轮询( *setInterval* ) + +- *IndexedDB* 定时器轮询( *setInterval* ) + +- *cookie* 定时器轮询( *setInterval* ) + +- *window.open、window.postMessage* + +- *Websocket* + + + +## 跨标签页通信常见方案 + +下面我们将针对每一种跨标签页通信的方案进行介绍。 + +>注:本文并不会对每一种方案的知识点本身进行详细介绍,只会介绍如何通过该方案实现跨标签页通信。关于每种方案技术点本身相关知识,可以参阅其他对应章节。 + +### *BroadCast Channel* + +*BroadCast Channel* 可以帮我们创建一个用于广播的通信频道。当所有页面都监听同一频道的消息时,其中某一个页面通过它发送的消息就会被其他所有页面收到。但是前提是同源页面。 + +*index.html* + +```html + + + + + +``` + +*index2.html* + +```html + + + +``` + +在上面的代码中,我们在页面一注册了一个名为 *load1* 的 *BroadcastChannel* 对象,之后所有的页面也创建同名的 *BroadcastChannel* 对象,然后就可以通过 *postMessage* 和 *onmessage* 方法进行相互通信了。 + + + +### *Service Worker* + +*Service Worker* 实际上是浏览器和服务器之间的代理服务器,它最大的特点是在页面中注册并安装成功后,运行于浏览器后台,不受页面刷新的影响,可以监听和截拦作用域范围内所有页面的 *HTTP* 请求。 + +*Service Worker* 的目的在于离线缓存,转发请求和网络代理。 + +*index.html* + +```html + +

页面一

+ + + +``` + +*index2.html* + +```html + +

页面二

+ + +``` + +*sw.js* + +```js +self.addEventListener("message",async event=>{ + const clients = await self.clients.matchAll(); + clients.forEach(function(client){ + client.postMessage(event.data) + }); +}); +``` + + + +### *LocalStorage window.onstorage* 监听 + +在 *Web Storage* 中,每次将一个值存储到本地存储时,就会触发一个 *storage* 事件。 + +由事件监听器发送给回调函数的事件对象有几个自动填充的属性如下: + +- *key*:告诉我们被修改的条目的键。 + +- *newValue*:告诉我们被修改后的新值。 + +- *oldValue*:告诉我们修改前的值。 + +- *storageArea*:指向事件监听对应的 *Storage* 对象。 + +- *url*:原始触发 *storage* 事件的那个网页的地址。 + +>注意:这个事件只在同一域下的任何窗口或者标签上触发,并且只在被存储的条目改变时触发。 + +示例如下:这里我们需要打开服务器进行演示,本地文件无法触发 *storage* 事件 + +*index.html* + +```html + + + +``` + +在上面的代码中,我们在该页面下设置了两个 *localStorage* 本地数据。 + +*index2.html* + +```html + + + +``` + +在该页面中我们安装了一个 *storage* 的事件监听器,安装之后只要是同一域下面的其他 *storage* 值发生改变,该页面下面的 *storage* 事件就会被触发。 + + + +### *Shared Worker* 定时器轮询( *setInterval* ) + +下面是 *MDN* 关于 *SharedWorker* 的说明: + +>*SharedWorker* 接口代表一种特定类型的 *worker*,可以从几个浏览上下文中访问,例如几个窗口、*iframe* 或其他 *worker*。它们实现一个不同于普通 *worker* 的接口,具有不同的全局作用域,如果要使 *SharedWorker* 连接到多个不同的页面,这些页面必须是同源的(相同的协议、*host* 以及端口)。 + +*index.html* + +```html + + + + + +``` + +*index2.html* + +```html + + + +``` + +*worker.js* + +```js +var data = ''; +onconnect = function (e) { + var port = e.ports[0] + port.onmessage = function (e) { + // 如果是 get 则返回数据给客户端 + if (e.data === 'get') { + port.postMessage(data); + data = ""; + } else { + // 否则把数据保存 + data = e.data + } + } +} +``` + + + +### *IndexedDB* 定时器轮询( *setInterval* ) + +*IndexedDB* 是一种底层 *API*,用于在客户端存储大量的结构化数据(也包括文件/二进制大型对象(*blobs*))。该 *API* 使用索引实现对数据的高性能搜索。 + +通过对 *IndexedDB* 进行定时器轮询的方式,我们也能够实现跨标签页的通信。 + +*index.html* + +```html + +

新增学生

+
+ 学生学号: + +
+
+ 学生姓名: + +
+
+ 学生年龄: + +
+ + + + +``` + +*index2.html* + +```html + +

学生表

+
+ + + +``` + +*db.js* + +```js +/** + * 打开数据库 + * @param {object} dbName 数据库的名字 + * @param {string} storeName 仓库名称 + * @param {string} version 数据库的版本 + * @return {object} 该函数会返回一个数据库实例 + */ +function openDB(dbName, version = 1) { + return new Promise((resolve, reject) => { + var db; // 存储创建的数据库 + // 打开数据库,若没有则会创建 + const request = indexedDB.open(dbName, version); + + // 数据库打开成功回调 + request.onsuccess = function (event) { + db = event.target.result; // 存储数据库对象 + console.log("数据库打开成功"); + resolve(db); + }; + + // 数据库打开失败的回调 + request.onerror = function (event) { + console.log("数据库打开报错"); + }; + + // 数据库有更新时候的回调 + request.onupgradeneeded = function (event) { + // 数据库创建或升级的时候会触发 + console.log("onupgradeneeded"); + db = event.target.result; // 存储数据库对象 + var objectStore; + // 创建存储库 + objectStore = db.createObjectStore("stu", { + keyPath: "stuId", // 这是主键 + autoIncrement: true // 实现自增 + }); + // 创建索引,在后面查询数据的时候可以根据索引查 + objectStore.createIndex("stuId", "stuId", { unique: true }); + objectStore.createIndex("stuName", "stuName", { unique: false }); + objectStore.createIndex("stuAge", "stuAge", { unique: false }); + }; + }); +} + +/** + * 新增数据 + * @param {object} db 数据库实例 + * @param {string} storeName 仓库名称 + * @param {string} data 数据 + */ +function addData(db, storeName, data) { + var request = db + .transaction([storeName], "readwrite") // 事务对象 指定表格名称和操作模式("只读"或"读写") + .objectStore(storeName) // 仓库对象 + .add(data); + + request.onsuccess = function (event) { + console.log("数据写入成功"); + }; + + request.onerror = function (event) { + console.log("数据写入失败"); + }; +} + +/** + * 通过主键读取数据 + * @param {object} db 数据库实例 + * @param {string} storeName 仓库名称 + * @param {string} key 主键值 + */ +function getDataByKey(db, storeName, key) { + return new Promise((resolve, reject) => { + var transaction = db.transaction([storeName]); // 事务 + var objectStore = transaction.objectStore(storeName); // 仓库对象 + var request = objectStore.getAll(); // 通过主键获取数据 + + request.onerror = function (event) { + console.log("事务失败"); + }; + + request.onsuccess = function (event) { + // console.log("主键查询结果: ", request.result); + resolve(request.result); + }; + }); +} +``` + + + +### *cookie* 定时器轮询( *setInterval* ) + +我们同样可以通过定时器轮询的方式来监听 *Cookie* 的变化,从而达到一个多标签页通信的目的。 + +*index.html* + +```html + + + +``` + +*index2.html* + +```html + + + +``` + +在上面的代码中,我们为 *index2.html* 设置了一个定时器,之后每过一秒钟都会重新去读取本地的 *Cookie* 信息,并比较和之前获取到的 *Cookie* 信息有没有变化,如果有变化就进行更新操作。 + + + +### *window.open、window.postMessage* + +*MDN* 上是这样介绍 *window.postMessage* 的: + +>window.postMessage( ) 方法可以安全地实现跨源通信。通常,对于两个不同页面的脚本,只有当执行它们的页面位于具有相同的协议(通常为https),端口号(443为https的默认值),以及主机 (两个页面的模数 Document.domain设置为相同的值) 时,这两个脚本才能相互通信。window.postMessage( ) 方法提供了一种受控机制来规避此限制,只要正确的使用,这种方法就很安全。 + +>从广义上讲,一个窗口可以获得对另一个窗口的引用(比如 targetWindow = window.opener),然后在窗口上调用 targetWindow.postMessage( ) 方法分发一个 MessageEvent 消息。接收消息的窗口可以根据需要自由处理此事件 (en-US)。传递给 window.postMessage( ) 的参数(比如 message )将通过消息事件对象暴露给接收消息的窗口。 + +*index.html* + +```html + + + + + + +``` + +*index2.html* + +```html + +

这是弹出页面

+ + +``` + +在上面的代码中,我们在页面一通过 *open* 方法打开页面二,然后通过 *postMessage* 的方式向页面二传递信息。页面二通过监听 *message* 事件来接收信息。 + + + +### *Websocket* + +*WebSocket* 协议在 *2008* 年诞生,*2011* 年成为国际标准。所有浏览器都已经支持了。 + +它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。 + +*server.js* + +```js +// 初始化一个 node 项目 npm init -y +// 安装依赖 npm i -save ws + +// 获得 WebSocketServer 类型 +var WebSocketServer = require('ws').Server; + +// 创建 WebSocketServer 对象实例,监听指定端口 +var wss = new WebSocketServer({ + port: 8080 +}); + +// 创建保存所有已连接到服务器的客户端对象的数组 +var clients = []; + +// 为服务器添加 connection 事件监听,当有客户端连接到服务端时,立刻将客户端对象保存进数组中 +wss.on('connection', function (client) { + // 如果是首次连接 + if (clients.indexOf(client) === -1) { + // 就将当前连接保存到数组备用 + clients.push(client) + console.log("有" + clients.length + "客户端在线"); + + // 为每个 client 对象绑定 message 事件,当某个客户端发来消息时,自动触发 + client.on('message', function (msg) { + console.log(msg, typeof msg); + console.log('收到消息' + msg) + // 遍历 clients 数组中每个其他客户端对象,并发送消息给其他客户端 + for (var c of clients) { + // 排除自己这个客户端连接 + if (c !== client) { + // 把消息发给别人 + c.send(msg.toString()); + } + } + }); + + // 当客户端断开连接时触发该事件 + client.onclose = function () { + var index = clients.indexOf(this); + clients.splice(index, 1); + console.log("有" + clients.length + "客户端在线") + } + } +}); + +console.log("服务器已启动..."); +``` + +在上面的代码中,我们创建了一个 *Websocket* 服务器,监听 *8080* 端口。每一个连接到该服务器的客户端,都会触发服务器的 *connection* 事件,并且会将此客户端连接实例作为回调函数的参数传入。 + +我们将所有的客户端连接实例保存到一个数组里面。为该实例绑定了 *message* 和 *close* 事件,当某个客户端发来消息时,自动触发 *message* 事件,然后遍历 *clients* 数组中每个其他客户端对象,并发送消息给其他客户端。 + +*close* 事件在客户端断开连接时会触发,我们要做的事情就是从数组中删除该连接。 + +*index.html* + +```html + + + + + + +``` + +*index2.html* + +```html + + + +``` \ No newline at end of file diff --git a/07. Web Worker/web worker.md b/07. Web Worker/web worker.md new file mode 100644 index 0000000..ba9d5e0 --- /dev/null +++ b/07. Web Worker/web worker.md @@ -0,0 +1,256 @@ +# *web worker* + + + +在运行大型或者复杂的 *JavaScript* 脚本的时候经常会出现浏览器假死的现象,这是由于 *JavaScript* 这个语言在执行的时候是采用单线程来执行而导致的。采用同步执行的方式进行运行,如果出现阻塞,那么后面的代码将不会执行。例如: + +```js +while(true){} +``` + +那么能不能让这些代码在后台运行,或者让 *JavaScript* 函数在多个进程中同时运行呢? + +*HTML5* 所提出的 *Web Worker* 正是为了解决这个问题而出现的。 + +*HTML5* 的 *Web Worker* 可以让 *Web* 应用程序具备后台的处理能力。它支持多线程处理功能,因此可以充分利用多核 *CPU* 带来的优势,将耗时长的任务分配给 *HTML5* 的 *Web Worker* 运行,从而避免了有时页面反应迟缓,甚至假死的现象。 + +本文将分为以下几个部分来介绍 *web worker*: + +- *web worker* 概述 +- *web Worker* 使用示例 +- 使用 *web Worker* 实现跨标签页通信 + + + +## *web worker* 概述 + +在 *Web* 应用程序中,*web Worker* 是一项后台处理技术。 + +在此之前,*JavaScript* 创建的 *Web* 应用程序中,因为所有的处理都是在单线程内执行的,所以如果脚本所需运行时间太长,程序界面就会长时间处于停止状态,甚至当等待时间超出一定的限度,浏览器就会进入假死的状态。 + +为了解决这个问题,*HTML5* 新增加了一个 *web Worker API*。 + +使用这个 *API*,用户可以很容易的创建在后台运行的线程,这个线程被称之为 *Worker*。如果将可能耗费较长时间的处理交给后台来执行,则对用户在前台页面中执行的操作没有影响。 + +*web Worker* 的特点如下: + +- 通过加载一个 *JS* 文件来进行大量复杂的计算,而不挂起主进程。通过 *postMessage* 和 *onMessage* 进行通信。 + +- 可以在 *Worker* 中通过 *importScripts(url)* 方法来加载 *JavaScript* 脚本文件。 + +- 可以使用 *setTimeout( ),clearTimeout( ),setInterval( ) 和 clearInterval( )* 等方法。 + +- 可以使用 *XMLHttpRequest* 进行异步请求。 + +- 可以访问 *navigator* 的部分属性。 + +- 可以使用 *JavaScript* 核心对象。 + +除了上述的优点,*web Worker* 本身也具有一定局限性的,具体如下: + +- 不能跨域加载 *JavaScript* + +- *Worker* 内代码不能访问 *DOM* + +- 使用 *Web Worker* 加载数据没有 *JSONP* 和 *Ajax* 加载数据高效。 + +目前来看,*web Worker* 的浏览器兼容性还是很不错的,基本得到了主流浏览器的一致支持。 + +![image-20211206154023963](https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2021-12-06-074024.png) + + + +在开始使用 *web Worker* 之前,我们先来看一下使用 *Worker* 时会遇到的属性和方法,如下: + +- *self*:*self* 关键值用来表示本线程范围内的作用域。 + +- *postMessage( )*:向创建线程的源窗口发送信息。 + +- *onMessage*:获取接收消息的事件句柄。 + +- *importScripts(urls)*:*Worker* 内部如果要加载其他脚本,可以使用该方法来导入其它 *JavaScript* 脚本文件。参数为该脚本文件的 *URL* 地址,导入的脚本文件必须与使用该线程文件的页面在同一个域中,并在同一个端口中。 + +例如: + +```js +// 导入了 3 个 JavaScript 脚本 +importScripts("worker1.js","worker2.js","worker3.js"); +``` + + + +## *web Worker* 使用示例 + +接下来我们来看一下 *web Worker* 的具体使用方式。 + +*web Worker* 的使用方法非常简单,只需要创建一个 *web Worker* 对象,并传入希望执行的 *JavaScript* 文件即可。 + +之后在页面中设置一个事件监听器,用来监听由 *web Worker* 对象发来的消息。 + +如果想要在页面与 *web Worker* 之间建立通信,数据需要通过 *postMessage( )* 方法来进行传递。 + +创建 *web Worker*。步骤十分简单,只要在 *Worker* 类的构造器中,将需要在后台线程中执行的脚本文件的 *URL* 地址作为参数传入,就可以创建 *Worker* 对象,如下: + +```js +var worker = new Worker("./worker.js"); +``` + +>注意:在后台线程中是不能访问页面或者窗口对象的,此时如果在后台线程的脚本文件中使用 *window* 或者 *document* 对象,则会引发错误。 + +这里传入的 *JavaScript* 的 *URL* 可以是相对或者绝对路径,只要是相同的协议,主机和端口即可。 + +如果想获取 *Worker* 进程的返回值,可以通过 *onmessage* 属性来绑定一个事件处理程序。如下: + +```js +var worker = new Worker("./worker.js"); +worker.onmessage = function(){ + console.log("the message is back!"); +} +``` + +这里第一行代码用来创建和运行 *Worker* 进程,第 *2* 行设置了 *Worker* 的 *message* 事件,当后台 Worker 的 *postMessage( )* 方法被调用时,该事件就会被触发。 + +使用 *Worker* 对象的 *postMessage( )* 方法可以给后台线程发送消息。发送的消息需要为文本数据,如果要发送任何 *JavaScript* 对象,需要通过 *JSON.stringify( )* 方法将其转换成文本数据。 + +```js +worker.postMessage(message); +``` + +通过获取 *Worker* 对象的 *onmessage* 事件以及 *Worker* 对象的 *postMessage( )* 方法就可以实现线程之间的消息接收和发送。 + +*Web Worker* 不能自行终止,但是能够被启用它们的页面所终止。 + +调用 *terminate( )* 函数可以终止后台进程。被终止的 *Web Workers* 将不再响应任何消息或者执行任何其他运算。 + +终止之后,*Worker* 不能被重新启动,但是可以使用同样的 *URL* 创建一个新的 *Worker*。 + +下面是 *web Worker* 的一个具体使用示例。 + +*index.html* + +```html +

计数:

+ + +``` + +```js +var startBtn = document.getElementById("startBtn"); +var stopBtn = document.getElementById("stopBtn"); +var worker; // 用于存储 Worker 进程 +// 开始 Worker 的代码 +startBtn.onclick = function () { + // 第一次进来没有 Worker 进程 , 创建一个新的 Worker 进程 + worker = new Worker("worker.js"); + // 接收来自于后台的数据 + worker.onmessage = function (event) { + document.getElementById("result").innerHTML = event.data; + }; +} +// 停止 Worker 的代码 +stopBtn.onclick = function () { + worker.terminate(); + worker = undefined; +} +``` + +*worker.js* + +```js +var i = 0; +function timedCount() { + i++; + // 每次得到的结果都通过 postMessage 方法返回给前台 + postMessage(i); + setTimeout("timedCount()", 1000); +} +timedCount(); +``` + +在上面的代码中,当用户点击"开始工作"时,会创建一个 *Web Worker* 在后台进行计数。每次计的数都会通过 *postMessage( )* 方法返回给前台。 + +当用户点击"停止工作"时,则会调用 *terminate( )* 方法来终止 *Web Worker* 的运行。 + + + +## 使用 *web Worker* 实现跨标签页通信 + +*web Worker* 可分为两种类型: + +- 专用线程 *dedicated web worker* + +- 共享线程 *shared web worker* + +*Dedicated web worker* 随当前页面的关闭而结束,这意味着 *Dedicated web worker* 只能被创建它的页面访问。 + +与之相对应的 *Shared web worker* 共享线程可以同时有多个页面的线程链接。 + +前面我们示例 *web Worker* 时,实例化的是一个 Worker 类,这就代表是一个 *Dedicated web worker*,而要创建 *SharedWorker* 则需要实例化 *SharedWorker* 类。 + +```js +var worker = new SharedWorker("sharedworker.js"); +``` + +下面我们就使用 *Shared web worker* 共享线程来实现跨标签页通信。 + +*index.html* + +```html + + + + + +``` + +*index2.html* + +```html + + + +``` + +*worker.js* + +```js +var data = ''; +onconnect = function (e) { + var port = e.ports[0] + port.onmessage = function (e) { + // 如果是 get 则返回数据给客户端 + if (e.data === 'get') { + port.postMessage(data); + data = ""; + } else { + // 否则把数据保存 + data = e.data + } + } +} +``` + +------- + +-*EOF*- \ No newline at end of file diff --git a/07. Web Worker/web worker课堂代码/.vscode/settings.json b/07. Web Worker/web worker课堂代码/.vscode/settings.json new file mode 100644 index 0000000..5165b7c --- /dev/null +++ b/07. Web Worker/web worker课堂代码/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "eggHelper.serverPort": 35684 +} \ No newline at end of file diff --git a/07. Web Worker/web worker课堂代码/index.html b/07. Web Worker/web worker课堂代码/index.html new file mode 100644 index 0000000..5f8e1f1 --- /dev/null +++ b/07. Web Worker/web worker课堂代码/index.html @@ -0,0 +1,32 @@ + + + + + + + + Document + + + +

计数:

+ + + + + + \ No newline at end of file diff --git a/07. Web Worker/web worker课堂代码/worker.js b/07. Web Worker/web worker课堂代码/worker.js new file mode 100644 index 0000000..1f5cc22 --- /dev/null +++ b/07. Web Worker/web worker课堂代码/worker.js @@ -0,0 +1,5 @@ +let count = 0; +setInterval(function(){ + count++; + postMessage(count); +}, 1000); \ No newline at end of file diff --git a/浏览器.dio b/浏览器.dio index c9a6694..d451049 100644 --- a/浏览器.dio +++ b/浏览器.dio @@ -4,8 +4,8 @@ - - + + @@ -28,16 +28,16 @@ - - + + - + - + @@ -52,9 +52,6 @@ - - - diff --git a/浏览器笔面试题目.txt b/浏览器笔面试题目.txt deleted file mode 100644 index 844aa0a..0000000 --- a/浏览器笔面试题目.txt +++ /dev/null @@ -1,8 +0,0 @@ -【录播】01. 浏览器渲染流程 -【录播】02. repaint 和 reflow -【录播】03. 浏览器的组成部分 -【录播】04. 浏览器的离线存储 -【录播】05. 浏览器的缓存读取规则 -【录播】06. 跨标签页通信 -【录播】07. web worker -【录播】08. 部分面试题精讲