乐趣区

关于前端:读书笔记第四版JavaScript高级程序设计第二十七章

前言

也不齐全是笔记,也做了一些本人的补充

javascript 是单线程的吗?

javascript 是单线程的,然而 javascript 能够把工作嫁接给独立的线程。同时不影响单线程模型(不能操作 DOM)。

每关上一个网页就相当于一个沙盒,每一个页面都有本人独立的内容。工作者线程相当于一个齐全独立的二级子环境。在子环境中不能与依赖单线程模型 API 交互(DOM 操作),然而能够与父环境并行执行代码。

工作者线程与线程的区别

  1. 工作者线程的底层实现原理就是线程
  2. 工作者线程能够并发
  3. 工作者线程与主线程之间能够应用 SharedArrayBuffer 实现共享内存,在 js 中能够应用 Atomics 实现并发管制。
  4. 工作者线程不共享全副的内存
  5. 工作者线程与主线程可能不在一个过程中
  6. 创立工作者线程的开销也很大(工作者线程应该用于一些长期运行的工作)

工作者线程的类型

  1. ServiceWorker 服务工作者线程
  2. SharedWorker 共享工作者线程
  3. WebWorker,Worker,专用工作者线程

WorkerGlobalScope

在工作者线程中,没有 window 对象,全局对象是 WorkerGlobalScope 的子类的实例

  • 专用工作者线程全局对象是 DedicatedWorkerGlobalScope 子类的实例
  • 共享工作者线程全局对象是 SharedWorkerGlobalScope 的实例
  • 服务工作者是 ServiceWorkerGlobalScope 的实例

专用工作者线程 Worker or WebWorker

创立专用工作者线程

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script>
        // 在本地调试,须要应用绝对路径
        const worker = new Worker('./worker.js')
        console.log(worker)
    </script>
</body>
</html>

专用工作者线程的平安限度

工作者线程的脚本文件,只能和父级同源。(然而在工作者线程中,能够应用 importScripts 加载其余源的脚本)

应用 Worker 对象

创立的 Worker 对象在工作线程终止前,是不会被垃圾回收机制回收的

  • onerror, 父上下文监听工作者线程的谬误
  • onmessage, 父上下文监听工作者线程发送的音讯
  • onmessageerror, 父上下文监听工作者线程发送音讯产生的谬误(比方音讯无奈被反序列化)
  • postMessage(),父上下文向工作者线程发送音讯
  • terminate(),终止工作者线程

DedicatedWorkerGlobalScope

工作者线程中全局作用域是 DedicatedWorkerGlobalScope 对象的实例,能够通过 self 关键字拜访全局对象

  • self.postMessage, 向父上下文发送音讯
  • self.close, 敞开线程
  • self.importScripts, 引入其余脚本

专用工作者线程的生命周期

生命周期分为初始化,流动,终止。但父上下文是无奈辨别工作者线程的状态。调用 Worker 后,尽管 worker 对象可用,然而 worker 不肯定初始化结束。可能存在提早。如果不调用 close 或者 terminate,工作者线程会始终存在,垃圾回收机制也不会回收 worker 对象。然而调用 close 和 terminate 是有一些区别的。如果工作者线程关联的网页被敞开,工作者线程也会被终止。

  1. 在工作者线程的外部,调用 close,工作者线程不会立刻完结,而且在本次宏工作执行实现后完结。
  2. 在父上下文调用 terminate,工作者线程会立刻完结。
// 专用工作者线程
self.postMessage('a')
self.close()
self.postMessage('b')

// 父上下文
const worker = new Worker('./worker.js')
worker.onmessage = ({data}) => {console.log('data:', data);
}

// consloe
// data: a
// data: b

// 工作者线程
self.onmessage = ({data}) => console.log(data);

// 父上下文
const worker = new Worker(location.href  + '/worker.js')
// 定时器期待线程初始化实现
setTimeout(() => {worker.postMessage('a')
  worker.terminate()
  worker.postMessage('b')
}, 1000);

// consloe
// a

行内创立工作者线程

专用工作者线程能够通过 Blob 对象的 URL 在行内创立,而不须要近程的 js 文件。


const workerStr = `
  self.onmessage = ({data}) => {console.log('data:', data);
  }
`;
const workerBlob = new Blob([workerStr]);
const workerBlobUrl = URL.createObjectURL(workerBlob);
const worker = new Worker(workerBlobUrl);

// data: abc
worker.postMessage('abc');

父上下文的函数,也能够传递给专用工作者线程,并且在专用工作者线程中执行。然而父上下文的函数中不能应用闭包的变量,以及全局对象。

const fn = () => '父上下文的函数';
// 将 fn 转为字符串的模式,而后自执行
const workerStr = `
  self.postMessage((${fn.toString()})())
`
const workerBlob = new Blob([workerStr]);
const workerBlobUrl = URL.createObjectURL(workerBlob);
const worker = new Worker(workerBlobUrl);
worker.onmessage = ({data}) => {
  // 父上下文的函数
  console.log(data)
}
const a = 'Hi'
// error, Uncaught ReferenceError: a is not defined
const fn = () => `${a}, 父上下文的函数 `;
const workerStr = `
  self.postMessage((${fn.toString()})())
`
const workerBlob = new Blob([workerStr]);
const workerBlobUrl = URL.createObjectURL(workerBlob);
const worker = new Worker(workerBlobUrl);
worker.onmessage = ({data}) => {
  // 父上下文的函数
  console.log(data)
}

工作者线程中动静执行脚本

工作者线程中,能够应用 importScripts 加载执行脚本。importScripts 加载的 js 会依照程序执行。所有导入的脚本会共享作用域,importScripts 不会同源的限度。

我通过测试,在父上下文中应用 onerror 监听谬误,是能够捕捉到 importScripts 加载的非同源脚本的谬误,并且有具体的错误信息。

// 父上下文
const worker = new Worker('http://127.0.0.1:8080/worker.js')
window.onerror = (error) => {
  // Uncaught ReferenceError: a is not defined
  console.log(error)
}

// 工作者线程
importScripts('http://127.0.0.1:8081/worker.js')

// 工作者线程中 importScripts 加载的脚本
const fn = () => {console.log(a)
}
setTimeout(() => fn(), 3000);

多个工作者线程

工作线程还能够持续创立工作者线程。然而多个工作者线程会带来额定的开销。并且 顶级工作者线程,和子工作者线程,必须和父上下文在同一个源中

工作者线程的谬误

try……catch, 无奈捕捉到线程中的谬误,然而在父上下文中,能够应用 onerror 事件捕捉到

专用工作者线程的通信

postMessage

之前曾经在 demo 中给出例子,这里不再赘述

MessageChannel

MessageChannel API 有两个端口,如果父上下文须要实现与工作线程的通信, 父上下文须要将端口传到工作者线程中

// 父上下文
const channel = new MessageChannel()
const worker = new Worker('http://127.0.0.1:8080/worker.js')

// 将端口 2 发送给工作者线程中
worker.postMessage(null, [channel.port2]);

setTimeout(() => {
  // 通过 MessageChannel 发送音讯
  channel.port1.postMessage('我是父上下文')
}, 2000)
// 工作线程
let channelPort = null

self.onmessage = ({ports}) => {if (!channelPort) {channelPort = ports[0]
    self.onmessage = null
    // 通过 channelPort 监听音讯
    channelPort.onmessage = ({data}) => {console.log('父上下文的数据:', data);
    }
  }
}

BroadcastChannel

同源脚本能够应用 BroadcastChannel 进行通信,应用 BroadcastChannel 必须留神的是,如果父上下文在工作线程初始化实现之前,就发送音讯,工作线程初始化实现后,是承受不到音讯的。音讯不会存在音讯队列中。

// 父上下文
const channel = new BroadcastChannel('worker')
const worker = new Worker('http://127.0.0.1:8080/worker.js')
// 期待工作线程初始化结束
setTimeout(() => {channel.postMessage('音讯')
}, 2000)
// 工作线程
const channel = new BroadcastChannel('worker')
channel.onmessage = ({data}) => {console.log(data)
}

Channel Messaging API

Channel Messaging API 能够用在 “ 文档主体与 iframe”,” 两个 iframe 之间 ”,” 应用 SharedWorker 的两个文档 ”,或者两个 ”worker” 之间进行通许。

专用工作者线程的数据传输

结构化克隆算法

应用 postMessage 发送数据的时候,浏览器后盾会对数据(除了 Symbol 之外的类型)进行拷贝。尽管结构化克隆算法对循环援用的问题做了兼容解决,然而对于简单对象结构化克隆算法有性能损耗。

可转移对象

将数据的所有权。由父级上下文转让给工作线程。或者由工作线程转让给父级上下文。转移后,数据就会在之前的上下文中被抹去。postMessage 的第二个参数,是可选参数,是一个数组,数组的数据须要被转让所有权的数据。


// 父上下文
const worker = new Worker('http://127.0.0.1:8080/worker.js')
const buffer = new ArrayBuffer(30)
// 30
console.log('发送之前:', buffer.byteLength)
// 期待工作线程初始化结束
setTimeout(() => {worker.postMessage(buffer, [buffer])
  // 0
  console.log('发送之后:', buffer.byteLength)
}, 2000)

// 工作线程
self.onmessage = ({data}) => {
  // 30
  console.log('工作线程承受之后', data.byteLength);
}

对于 postMessage 的第二个参数

之前能够应用 worker.postMessage(null, [channel.port2]) 发送 channel 接口的时候。工作线程的 onmessage 事件的参数,会接管 ports,然而换成其余数据是接管不到的。postMessage 应该是对 channel 的数据做了非凡的解决。

SharedArrayBuffer

SharedArrayBuffer 能够在父上下文和工作线程中共享,SharedArrayBuffer 和 ArrayBuffer 的 api 雷同,不能间接被操作须要视图。

// 父上下文
const worker = new Worker('http://127.0.0.1:8080/worker.js')
const sharedBuffer = new SharedArrayBuffer(10)
const view = new Int8Array(sharedBuffer);
view[0] = 1;
// 1
console.log('发送之前:', view[0]);
worker.postMessage(sharedBuffer)
setTimeout(() => {
  // 打印出,2
  console.log('发送之后:', view[0])
}, 2000)
// 工作者线程
self.onmessage = ({data}) => {const view = new Int8Array(data);
  view[0] = '2'
}

并行线程共享资源,会有资源征用的隐患。能够应用 Atomics 解决,Atomics 与 SharedArrayBuffer 能够查看第二十章的笔记。

线程池

开启新的工作者线程开销很大,可开启放弃固定数量的线程。线程在繁忙时不承受新工作,线程闲暇后接管新工作。这些长期开启的线程,被称为线程池。

线程池中线程的数量,能够参考电脑 cpu 的线程数,navigator.hardwareConcurrency, 将 cpu 的线程数设置线程池的下限。

上面的是数中的封装,我在 github 中也没有找到太热门的库封装的线程池可用,https://github.com/andywer/th… 曾经是 5 年前更新的了。

共享工作者线程 SharedWorker

留神:

  1. Safari 不反对 SharedWorker
  2. SharedWorke 中的 console 不肯定能打印在父上下文的控制台中。(我在 Chrome 中试验了一下,共享工作者线程中的 console,的确不会呈现在页面的控制台中)

共享工作者线程和创立,平安限度和专用工作者线程都是雷同的,共享工作者线程,能够看作是专用工作者线程的扩大。

SharedWorker 能够被多个同源的上下文(同源的网页标签)拜访。SharedWorker 的音讯接口和专用工作者线程也略有不同。

SharedWorker,没方法应用行内的 worker, 因为通过 URL.createObjectURL, 是浏览器外部的 URL, 无奈在其余标签页应用。

SharedWorker 的惟一标示

worker 每次 new 都会返回一个新的 worker 实例,SharedWorker 只会在不存在雷同标示的状况下返回新的实例。SharedWorker 的标示能够是 worker 文件的门路, 文档源。

// 只会实例化一个共享工作者线程
new SharedWorker('http://127.0.0.1:8080/worker.js')
new SharedWorker('http://127.0.0.1:8080/worker.js')

然而如果咱们给雷同源的 SharedWorker,不同的标识,浏览器会工作它们是不同的共享工作者线程

// 实例化二个共享工作者线程
new SharedWorker('http://127.0.0.1:8080/worker.js', { name: 'a'})
new SharedWorker('http://127.0.0.1:8080/worker.js', { name: 'a'})

在不同页面,只有标示雷同,创立的 SharedWorker 都是同一个链接

SharedWorker 对象的属性

  • onerror 监听共享工作者线程对象上的抛出的谬误
  • port 与共享工作者线程通信的接口,SharedWorker 会隐式的创立,用于与父上下文通信

共享工作者线程中的全局对象是 SharedWorkerGlobalScope 的实例,全局实例上的属性和办法

  • onconnect,当共享工作者线程建设链接时会触发, 参数蕴含 ports 数组,port 能够把音讯穿回给父上下文。sharedWorker.port.onmessage, sharedWorker.port.start(), 都会触发 onconnect 事件。
  • close
  • importScripts
  • name

共享工作者线程的生命周期

专用工作者线程只和一个页面绑定,而共享工作者线程只有还有一个上下文链接,它就不会被回收。共享工作者对象无奈通过 terminate 敞开,因为共享工作者线程没有 terminate 办法,浏览器会负责管理共享工作者线程的链接。

链接共享工作者线程

产生 connect 事件时,SharedWorker 构造函数会隐式的创立 MessageChannel,并把其中一个 port 转移给共享工作者线程的 ports 数组中。

???? 然而共享线程与父上下文的启动敞开不是对称的。每次关上会建设链接,connect 事件中的 ports 数组中 port 数量会加 1,然而页面被敞开,SharedWorker 无奈感知。

比方很多页面链接了 SharedWorker,当初一部分当初敞开了,SharedWorker 并不知道那些页面敞开,所以 ports 数组中,存在被敞开的页面的 port,这些死端口会净化 ports 数组。

书中给出的办法是,能够在页面销毁之前的 beforeunload 事件中,告诉 SharedWorker 革除死端口。

SharedWorke 示例

示例一

// 父页面
const worker = new SharedWorker('http://127.0.0.1:8080/worker.js')
worker.port.onmessage = ({data}) => {
    // 打印 data: 2
    console.log('data:', data);
}
worker.port.postMessage([1, 1]);
// 共享工作者线程
const connectedPorts = new Set();
self.onconnect = ({ports}) => {if (!connectedPorts.has(ports[0])) {connectedPorts.add(ports[0])
        ports[0].onmessage = ({data}) => {ports[0].postMessage(data[0] + data[1])
        }
    }  
};

示例二

分享线程生成 id,标示接口,并发送给页面。在页面 beforeunload,将 id 发送给分享工作者线程中,分享工作者线程革除死端口。

// 父页面 1
const worker = new SharedWorker('http://127.0.0.1:8080/worker.js')
let portId = null
worker.port.onmessage = ({data}) => {if (typeof data === 'string' && data.indexOf('uid:') > -1) {
        // 记录接口的 id
        portId = data.split(':')[1];
    } else {console.log('接口的数量:', data);
    }
}

window.addEventListener('beforeunload', (event) => {worker.port.postMessage(` 删除:${portId}`);
});
// 父页面 2
const worker = new SharedWorker('http://127.0.0.1:8080/worker.js')
let portId = null
worker.port.onmessage = ({data}) => {if (typeof data === 'string' && data.indexOf('uid:') > -1) {
        // 记录接口的 id
        portId = data.split(':')[1];
    } else {console.log('接口的数量:', data);
    }
}

window.addEventListener('beforeunload', (event) => {worker.port.postMessage(` 删除:${portId}`);
});
// 分享工作者线程
const uuid = () => {return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
      return v.toString(16);
    });
}
// 记录接口的 map
const connectedPortsMap = new Map();

self.onconnect = ({ports}) => {if (!connectedPortsMap.has(ports[0])) {const uid = uuid();
        connectedPortsMap.set(uid, ports[0])
        // 向页面发送接口的 id,这个 id 用于删除接口
        ports[0].postMessage(`uid:${uid}`);
        ports[0].onmessage = ({data}) => {if (typeof data === 'string' && data.indexOf('删除:') > -1) {const portId = data.split(':')[1];
                // 删除死接口
                connectedPortsMap.delete(portId);
            }
        }
    }  
};

setInterval(() => {
    // 发送接口的数量
    connectedPortsMap.forEach((value) => {value.postMessage(connectedPortsMap.size)
    })
}, 3000)

服务工作者线程

???? 感觉这个章节翻译的有点差,很多话读的很顺当,不晦涩。而且很多章节都没有给出示例代码,我很多章节都手敲了一遍例子代码,放在文章李

是浏览器中的代理服务器线程,能够拦挡申请或者缓存响应,页面能够在无网的环境下应用。与共享工作者线程相似,多个页面共享一个服务工作者线程。服务工作者线程中,服务工作者线程能够应用 Notifications API、Push API、Background Sync API。为了应用 Push API 服务工作者线程能够在浏览器或者标签页敞开后,持续期待推送的事件。

服务工作者线程,罕用于网络申请的缓存层和启用推送告诉。服务工作者线程,能够把 Web 利用的体验变为原生应用程序一样。

Notification API 根底

浏览器用于显示桌面告诉的 API, 上面是例子

// 查看是否容许发送告诉
// 如果曾经容许间接发送告诉
if (Notification.permission === "granted") {
  let notification = new Notification('西尔莎罗南', {body: '西尔莎罗南????'});
} else if (Notification.permission !== "denied") {
  // 如果还没有容许发送告诉,咱们申请用户容许
  Notification.requestPermission().then(function (permission) {
    // 如果用户承受权限,咱们就能够发动一条音讯
    if (permission === "granted") {
      let notification = new Notification('西尔莎罗南', {body: '西尔莎罗南????'});
    }
  })
}

Push API 根底

Push API 实现了 Web 承受服务器推送音讯的能力。Push API 具体的施行代码,能够看我的这个例子, 实现了一个简略的推送。

过程,客户端生成订阅信息,发送给服务端保留。服务端端能够依据须要,在适合的时候,应用订阅信息向客户端发送推送。

https://github.com/peoplesing…

Background Sync API 根底

服务工作者线程,用于定期更新数据的 API。

???? 原本想试验以一下这个 API,然而注册定时工作时,提醒“DOMException: Permission denied.”谬误,临时没有解决。

ServiceWorkerContainer

服务工作者线程没有全局的构造函数,通过 navigator.serviceWorker 创立,销毁,服务工作者线程

创立服务工作者线程

与共享工作者线程一样,在没有时创立新的链接,如果线程已存在,链接到已存在的线程上。

// 创立服务工作者线程
navigator.serviceWorker.register('http://127.0.0.1:8080/worker.js');

registerf 返回一个 Promise 对象,在同一页面首次调用 register 后,后续调用 register 没有任何返回。

navigator.serviceWorker.register('http://127.0.0.1:8080/worker.js').then(() => {console.info('注册胜利')
}).catch(() => {console.error('注册失败')
})

如果服务工作者线程用于治理缓存,服务工作线程应该在页面中提前注册。否则,服务工作者线程应该在 load 事件中实现注册。

if ('serviceWorker' in navigator) {window.addEventListener('load', () => {navigator.serviceWorker.register('http://127.0.0.1:8080/worker.js').then(() => {console.info('注册胜利')
    }).catch(() => {console.error('注册失败')
    })
  });
}

应用 ServiceWorkerContainer 对象(容器对象)

ServiceWorkerContainer 对象是浏览器对服务工作者线程的顶部封装,ServiceWorkerContainer 能够在客户端中通过 navigator.serviceWorker 拜访

  • navigator.serviceWorker.oncontrollerchange, 在注册新的服务工作者线程时触发(精确的说是在注册新版本的服务工作者线程,并接管页面时触发)
const btn = document.getElementById('btn')

btn.onclick = () => {navigator.serviceWorker.register('./sw2.js')
}

navigator.serviceWorker.oncontrollerchange = () => {console.log('触发 controllerchange 事件')
    // sw2
    console.log(navigator.serviceWorker.controller)
}

navigator.serviceWorker.register('./worker.js')
// sw1
console.log('hehe')
// sw2
self.addEventListener('install', async () => {
    // 强制进入已激活的状态
    self.skipWaiting();})

self.addEventListener('activate', async () => {
    // 强制接管客户端
    self.clients.claim();})
  • onerror,服务工作者线程,产生谬误时触发
  • onmessage,监听服务工作者线程发送的音讯,收到音讯时触发
  • ready,返回 Promise,内容是曾经激活的 ServiceWorkerRegistration 对象
  • controller,返回以后页面的 serviceWorker 对象,如果没有激活的服务工作者线程返回 null
  • register(), 创立更新 ServiceWorkerRegistration 对象
  • getRegistration(scope), 返回 Promise, 内容与与 ServiceWorkerContainer 关联,并且与 scope(门路)匹配的 ServiceWorkerRegistration 对象。
  • getRegistrations(), 返回 Promise, 内容是与 ServiceWorkerContainer 关联的所有 ServiceWorkerRegistration 对象
  • startMessage(), 开始承受服务工作者线程通过 postMessage 派发的音讯,如果不应用 startMessage,Client.postMessage()派发的音讯会期待 DOMContentLoaded 事件之后被调度。startMessage 能够尽早的调度。(应用 ServiceWorkerContainer.onmessage 时,音讯会主动发送,不须要 startMessage)

if ('serviceWorker' in navigator) {window.addEventListener('load', () => {
    let sw1;
    let sw2;
    navigator.serviceWorker.register('http://127.0.0.1:8080/worker.js').then(sw => {console.log(sw);
      sw1 = sw;
    }) 
    navigator.serviceWorker.ready.then((sw) => {console.log(sw);
      sw2 = sw;
    })
    setTimeout(() => {
      // true
      console.log(sw1 === sw2)
    }, 1000)
  });
}

应用 ServiceWorkerRegistration 对象(注册对象)

ServiceWorkerRegistration,示意胜利注册的服务工作者线程。能够通过 register 返回的 Promise 中拜访到。在同一页面调用 register,如果 URL 雷同,返回的都是同一个 ServiceWorkerRegistration 对象。

navigator.serviceWorker.register('http://127.0.0.1:8080/worker.js').then(sw => {
  // ServiceWorkerRegistration 对象
  console.log(sw);
}) 
  • onupdatefound, 线程装置中的状态触发
  • scope, 返回服务工作者线程的门路
  • navigationPreload,返回与注册对象关联的 NavigationPreloadManager 对象
  • pushManager,返回注册对象关联的 pushManager 对象(次要用于注册音讯,不便服务端推送音讯)
  • installing,返回状态为 installing 的服务工作者线程
  • waiting,返回状态为 waiting 的服务工作者线程
  • active,如果有则返回状态 activating(激活中)或 active(流动)的服务工作者线程
  • showNotifications,显示告诉,能够配置 title 和 body
  • update,更新 ServiceWorker
  • unregister,勾销 ServiceWorker

应用 ServiceWorker 对象

如何获取 ServiceWorker 对象?有两种以下的路径

  1. ServiceWorkerRegistration 对象的 active 属性
  2. ServiceWorkerContainer 对象的 controller 属性

ServiceWorker 对象继承 Work,然而不蕴含 terminate 办法

  • onstatechange,ServiceWorker.state 变动时触发
  • scriptURL,返回注册的工作者线程的残缺 URL,相对路径会被解析为残缺的门路
  • state,返回工作者线程的状态,installing,installed,activating,activated,redundant

ServiceWorker 的平安限度

  1. 受到同源的限度
  2. 只能用于 HTTPS 的协定,127.0.0.1 或者 localhost 下能够应用 HTTP 协定。

如果在非 HTTPS 的协定下,navigator.serviceWorker 是 undefined。window.isSecureContext 能够判断以后上下文是否平安。

ServiceWorkerGlobalScope

服务工作者线程外部,全局对象是 ServiceWorkerGlobalScope 的实例。ServiceWorkerGlobalScope 继承 WorkerGlobalScope,因而领有它的属性和办法。线程外部通过 self 拜访全局的上下文。ServiceWorkerGlobalScope 的实例的做了以下的扩大。

  • caches,返回 CacheStorage 对象
  • clients,返回工作者线程的 Clients 接口
  • registration,返回服务工作者线程的注册对象 ServiceWorkerRegistration
  • skipWaiting,强制工作者线程进入流动的状态
  • fetch,在 服务工作者线程中,用于发动网络申请的办法

专用工作者线程,和共享工作者线程只有一个 onmessage 事件作为输出,但服务工作者线程能够承受多种事件

  • self.onintall, 服务工作者线程进入装置的时触发(activating 之前的状态)
  • self.onactive, 服务工作者线程进入激活状态时触发 (activating 之后的状态)
  • self.onfetch, 服务工作者线程能够拦挡页面的 fetch 申请
  • self.onmessage, 服务工作者线程接管 postMessage 发送的音讯时触发
  • self.onnotificationclick,用户点击 ServiceWorkerRegistration.showNotification(),生成的告诉触发
  • self.onnotificationclose,用户敞开 ServiceWorkerRegistration.showNotification(),生成的告诉触发
  • self.onpush,承受到服务端推送的音讯时触发

服务工作者线程作用域的限度

// worker.js 在根目录线下
navigator.serviceWorker.register('http://127.0.0.1:8080/worker.js')
// http://127.0.0.1:8080 下的所有申请都会被拦挡
fetch('http://127.0.0.1:8080/foo.js');
fetch('http://127.0.0.1:8080/foo/fooScript.js');
fetch('http://127.0.0.1:8080/baz/bazScript.js');

// worker.js 在 foo 目录下
navigator.serviceWorker.register('http://127.0.0.1:8080/foo/worker.js'})
// foo 目录下的申请会被拦挡
fetch('/foo/fooScript.js')
// 其余门路的申请不会被拦挡
fetch('/foo.js')
fetch('/baz/bazScript.js')

如果想排除某个门路下的申请,能够应用开端带斜杠的门路

// foo 门路下的申请,都不会被拦挡
navigator.serviceWorker.register(
  'http://127.0.0.1:8080/worker.js',
  {scope: '/foo/'}
)

服务工作者线程缓存

  1. 服务工作者线程的不会主动缓存任何申请
  2. 服务工作线程不会到期主动生效
  3. 缓存必须手动的更新和删除
  4. 缓存版本必须手动治理
  5. 缓存策略为 LRU,当缓存的数据超过浏览器的限度时

CacheStorage

通过 self.caches 拜访 CacheStorage 对象。CacheStorage 时字符串和 Cache 对象的映射。CacheStorage 在页面或者其余工作者线程中,都能够拜访应用。

// 拜访缓存,如果没有缓存则会创立
self.caches.open(key)

CacheStorage 也领有相似 Map 的 API,比方 has,delete,keys(),然而它们都是返回 Promise 的

match,matchAll

别离返回匹配的第一个 Response

(async () => {const request = new Request('https://www.foo.com')
    const response1 = new Response('fooResponse1')
    const response2 = new Response('fooResponse2')
    const v1 = await caches.open('v1')
    await v1.put(request, response1)
    const v2 = await caches.open('v2')
    await v2.put(request, response2)
    const matchCatch = await caches.match(request)
    const matchCatchText = await matchCatch.text()
    // true
    console.log(matchCatchText === 'fooResponse1')
})();  

Cache

CacheStorage 对象是字符串和 Cache 对象的映射。Cache 对象则是 Request 对象或者 URL 字符串,和 Response 对象之间的映射。

  • put, put(Request, Response)增加缓存,返回 Promise
  • add(request), 应用 add 发送 fetch 申请,会缓存响应
  • addAll(request[]), 会对数组中的每一项调用 add。

Cache 也领有 delete(), keys()等办法,这些办法都是返回 Promise 的

(async () => {const request1 = new Request('https://www.foo.com');
    const response1 = new Response('fooResponse');
    const cache = await caches.open('v1')
    await cache.put(request1, response1)
    const keys = await cache.keys()
    // [Request]
    console.log(keys)
})()
  • matchAll,返回匹配的 Response 数组
  • match,返回匹配的 Response 对象

(async () => {const request1 = new Request('https://www.foo.com?a=1&b=2')
    const request2 = new Request('https://www.bar.com?a=1&b=2', {method: 'GET'})
    const response1 = new Response('fooResponse')
    const response2 = new Response('barResponse')
    const v3 = await caches.open('v3')
    await v3.put(request1, response1)
    await v3.put(request2, response2)
    const matchResponse = await v3.match(new Request('https://www.foo.com'), {
        ignoreMethod: true, // 疏忽匹配 GET 或者 POST 办法
        ignoreSearch: true, // 疏忽匹配查问字符串
    });
    const matchResponseText = await matchResponse.text()
    // fooResponse
    console.log(matchResponseText)
})(); 

catch 对象的 key,value 应用的是 Request, Response 对象的 clone 办法创立的正本

(async () => {const request = new Request('https://www.foo.com');
    const response = new Response('fooResponse');
    const cache = await caches.open('v1')
    await cache.put(request, response)
    const keys = await cache.keys()
    // false
    console.log(keys[0] === request)
})();  

最大存储空间

获取存储空间,以及目前以用的空间

navigator.storage.estimate()

Client

  • id,客户端的全局惟一标示
  • type,客户端的类型
  • url,客户端的 URL
  • postMessage,向单个客户端发送音讯
  • claim,强制工作者线程管制作用域下所有的客户端。当一个 ServiceWorker 被初始注册时,页面在下次加载之前不会应用它。claim() 办法会立刻管制这些页面。

???? 对于服务工作者线程管制客户端的问题

一开始注册服务工作者时,页面将在下一次加载之前才应用它。有两种办法能够提前管制页面

// 页面
navigator.serviceWorker.register('./worker.js').then((registration) => {setTimeout(() => {fetch('/aa')
  }, 2000)
}).catch(() => {console.log('注册失败')
});
// sw
self.addEventListener('fetch', () => {
  // sw 没有管制客户端,所以无奈拦挡 fetch 申请,抛出谬误
  throw new Error('呵呵')
})

第一种解决办法, 应用 claim 强制取得控制权,然而可能会造成版本资源不统一


self.addEventListener('activate', async () => {self.clients.claim();
})

self.addEventListener('fetch', () => {
    // 能够抛出谬误
    throw new Error('呵呵')
})

第二种解决办法,刷新页面

navigator.serviceWorker.register('./worker.js').then((registration) => {setTimeout(() => {fetch('/aa')
    }, 3000)
    registration.addEventListener('updatefound', () => {
        const sw = registration.installing;
        sw.onstatechange = () => {console.log('sw.state', sw.state)
            if (sw.state === 'activated') {console.log('刷新页面')
                // 刷新页面后能够抛出谬误
                window.location.reload();}
        }     
    })
}).catch(() => {console.log('注册失败')
});

服务工作者线程的一致性

服务工作者线程,最重要的就是放弃一致性(不会存在 a 页面应用 v1 版本的服务工作者线程,b 页面应用 v2 版本的服务工作者线程)。

  1. 代码一致性,服务工作者线程在所有标签页都会同一个版本的代码
  2. 数据一致性,

    • 服务工作者线程提前失败:语法错误,资源加载失败,都会导致服务工作者线程加载是比
    • 服务工作者线程激进更新:当加载的服务工作者线程,或者服务工作者线程外部依赖的资源,有任何差别,都会导致装置新版本的服务工作者线程(新装置的工作者线程会进入 installed 态)
    • 未激活服务工作者线程消极流动,在应用 register 装置服务工作者线程后,服务工作者线程会装置,但不会被激活(除非所有受到之前版本的管制的标签页被敞开,或者调用 self.skipWaiting 办法)
    • 流动的服务工作者线程粘连,只有至多有一个客户端与关联到流动的服务工作者线程,浏览器 就会在该源的所有页面中应用它。对于新版本的服务工作者线程会始终期待。

生命周期

  1. 已解析(parsed)
  2. 装置中 (installing)
  3. 已装置(installed)
  4. 激活中(activating)
  5. 已激活(activated)
  6. 已生效(redundant)

已解析 parsed

调用 navigator.serviceWorker.register() 会进入已解析的状态,然而该状态没有事件,也没有对应的 ServiceWorker.state 的值。

装置中 installing

在客户端能够通过查看 registration.installing 是否被设置为了 ServiceWorker 实例,判断是否在装置中的状态。当服务工作者线程达到装置中的状态时,会触发 onupdatefound 事件。

navigator.serviceWorker.register('./sw1.js').then((registration) => {registration.onupdatefound = () => {console.log('我曾经达到了 installing 装置中的状态')
    }
    console.log(registration.installing)
});

在服务工作者线程的外部,能够通过监听 install 事件,确定装置中的状态。

在 install 事件中,能够用来填充缓存,能够应用 waitUntil 的办法,waitUntil 办法承受一个 Promise,只有 Promise 返回 resolve 时,服务工作者线程的状态才会向下一个状态过渡。

self.addEventListener('install', (event) => {event.waitUntil(async () => {const v1 = await caches.open('v1')
        // 缓存资源实现后,才过渡到下一个状态
        v1.addAll([
            'xxxx.js',
            'xxx.css'
        ])
    })
})

已装置 installed

在客户端能够通过查看 registration.waiting 是否被设置为了 ServiceWorker 实例,判断是否是已装置的状态。如果浏览器中没有之前版本的的 ServiceWorker,新装置的 ServiceWorker 会间接跳过这个状态,进入激活中的状态。否则将会期待。

navigator.serviceWorker.register('./worker.js').then((registration) => {console.log(registration.waiting)
});

如果有已装置的 ServiceWorker,能够应用 self.skipWaiting,强制工作者线程进入流动的状态

激活中状态 activating

如果浏览器中没有之前版本的 ServiceWorker,则新的服务工作者线程会间接进入这个状态。如果有其余服务者线程,能够通过上面的办法,使新的服务者线程进入激活中的状态

  1. self.skipWaiting(), 强制进入激活中的状态
  2. 原有的服务工作者线程客户端数量变为 0(标签页都被敞开)在下一次导航事件新工作者线程进入激活中的状态。
const btn = document.getElementById('btn');

navigator.serviceWorker.register('./sw1.js').then((registration) => {
    // 第一次加载没有流动的(之前版本)服务工作者过程, waiting 间接跳过所以为 null
    console.log('waiting:', registration.waiting);
    // 以后激活的是 sw1 的服务工作者线程
    console.log('active:', registration.active);
});

btn.onclick = () => {navigator.serviceWorker.register('./sw2.js').then((registration) => {
        // 加载新版本的服务工作者线程,触发更新加载
        // 因为之前曾经有在流动的服务工作者线程了,waiting 状态的是 sw2 的线程
        console.log('waiting:', registration.waiting);
        // 激活状态的是 sw1 的线程
        console.log('active:', registration.active);
    })
}

在客户端中能够大抵通过判断 registration.active 是否为 ServiceWorker 的实例判断。(active 为 ServiceWorker 的实例,可能是是激活状态或者激活中的状态)

在服务工作者线程中,能够通过增加 activate 事件处理来判断,该事件处理程序罕用于删除之前的缓存


const CATCH_KEY = 'v1'

self.addEventListener('activate', async (event) => {const keys = await caches.keys();
  keys.forEach((key) => {if (key !== CATCH_KEY) {caches.delete(key)
    }
  });
})

留神:activate 事件产生,并不意味着页面受控。能够应用 clients.claim()管制不受控的客户端。

已激活的状态 activated

在客户端中能够大抵通过判断 registration.active 是否为 ServiceWorker 的实例判断。(active 为 ServiceWorker 的实例,能够是激活状态或者激活中的状态)

或者能够通过查看 registration.controller 属性,controller 属性返回已激活 ServiceWorker 的实例。当新的服务工作者线程管制客户端时,会触发 navigator.serviceWorker.oncontrollerchange 事件

或者 navigator.serviceWorker.ready 返回的 Promise 为 resolve 时,工作者线程也是已激活的状态。

const btn = document.getElementById('btn');

navigator.serviceWorker.register('./sw1.js').then((registration) => {
    // 已激活的线程 sw1
    console.log('activated', navigator.serviceWorker.controller)
});

btn.onclick = () => {navigator.serviceWorker.register('./sw2.js').then((registration) => {
        // 在期待的线程 sw2
        console.log('waiting', registration.waiting)
        // 已激活的线程 sw1
        console.log('activated', navigator.serviceWorker.controller)
    })
}

已生效的状态 redundant

服务工作者会被浏览器销毁并回收资源

更新服务工作者线程

上面操作会触发更新查看:

  1. 应用 navigator.serviceWorker.register(),加载不同 URL, 会查看
  2. 产生了 push,fetch 事件。并且至多 24 小时没有更新查看。
  3. 浏览器导航到了到服务工作者线程作用域中的一个页面。

如果更新查看发现差别,浏览器会应用新脚本初始化新的工作者线程,新的工作者线程将会达到 installed 的状态。而后会期待。除非应用 self.skipWaiting(), 强制进入激活中的状态。或者原有的服务工作者线程客户端数量变为 0(标签页都被敞开)在下一次导航事件新工作者线程进入激活中的状态。

刷新页面不会让更新服务工作者线程进入激活状态并取代已有的服务工作者线程。比方,有个关上的页面,其中有一个服务工作者线程正在管制它,而一个更新服务工作者线程正在已装置状态中期待。客户端在页面刷新期间会产生重叠,即旧页面还没有卸载,新页面已加载了。因而,现有的服务工作者线程永远不会让出控制权,毕竟至多还有一个客户端在它的管制之下。为此,取代现有服务工作者线程惟一的形式就是敞开所有受控页面。

updateViaCache

应用 updateViaCache 能够管制,服务工作者线程的缓存

  • none,服务工作者线程的脚本以及 importScripts 引入都不会缓存
  • all,所有文件都会被 http 缓存
  • imports,服务工作者线程的脚本不会被缓存,importScripts 的文件会被 http 缓存

navigator.serviceWorker.register('/serviceWorker.js', {updateViaCache: 'none'});

服务工作者线程与页面通信

// 页面
navigator.serviceWorker.onmessage = ({data}) => {console.log('服务者线程发送的音讯:', data);
}

navigator.serviceWorker.register('./worker.js').then((registration) => {console.log('注册胜利')
}).catch(() => {console.log('注册失败')
});
// sw
self.addEventListener('install', async () => {self.skipWaiting();
});

self.addEventListener('activate', async () => {self.clients.claim();
    const allClients = await clients.matchAll({includeUncontrolled: true});
    let homeClient = null;
    for (const client of allClients) {const url = new URL(client.url);
        if (url.pathname === '/') {
            homeClient = client;
            break;
        }
    }
    homeClient.postMessage('Hello')
});

拦挡 fetch 申请

self.onfetch = (fetchEvent) => {fetchEvent.respondWith(fetch(fetchEvent.request));
};

fetchEvent.respondWith 承受 Promise,返回 Respose 对象,

从缓存中返回


self.onfetch = (fetchEvent) => {fetchEvent.respondWith(caches.match(fetchEvent.request));
};

从网络返回,缓存作为后备

self.onfetch = (fetchEvent) => {fetchEvent.respondWith(fetch(fetchEvent.request).catch(() => {return caches.match(fetchEvent.request)
  }));
};

从缓存返回,网络作为后备


self.onfetch = (fetchEvent) => {
  fetchEvent.respondWith(caches.match(fetchEvent.request).then((response) => {return response || fetch(fetchEvent.request).then(async (res) => {
        // 网络返回胜利后,将网络返回的资源,缓存到本地
        const catch = await catchs.open('CatchName')
        await catch.put(fetchEvent.request, res)
        return res;
      })
    })
  );
};

通用后备

在服务者线程加载时就应该缓存资源,在缓存,和网络都生效时候,返回通用的后备。

参考

  • javascript 高级程序设计第四版
  • Channel Messaging API
  • Service Worker controllerchange never fires
  • Service Worker Cookbook
  • How Push Works
  • A minimal Web Push example
  • Service Worker 生命周期
退出移动版