关于前端:读书笔记第四版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 生命周期

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理