Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ServiceWorkers那些事儿 #131

Open
FrankKai opened this issue Dec 12, 2018 · 8 comments
Open

ServiceWorkers那些事儿 #131

FrankKai opened this issue Dec 12, 2018 · 8 comments

Comments

@FrankKai
Copy link
Owner

FrankKai commented Dec 12, 2018

https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers

@FrankKai FrankKai changed the title ServiceWorker那些事儿 ServiceWorkers那些事儿 Dec 12, 2018
@FrankKai
Copy link
Owner Author

FrankKai commented Dec 12, 2018

  • 关于Service Worker的一些疑问
  • 基础架构(basic architecture)
  • 注册一个自己的worker (registering a service worker)
  • 安装和激活:填充你的缓存(Install and activate: populating your cache)
  • 请求自定义响应(Custom responses to requests)
  • 恢复错误请求(Recovering failed requests)
  • 更新service worker(Updating your service worker)
  • cache control and custom responses
  • the context fo a simple app with offline functionality

@FrankKai
Copy link
Owner Author

FrankKai commented Dec 12, 2018

Service Workers解决什么问题?

用户断网导致的离线问题。

Service Workers哪里好?

  • 细粒度的控制AppCache方式的应用
  • 更加容易地使用缓存的静态资源,从而提供一种默认的类似原生APP的用户体验

尝试service workers前的准备

chrome://flags experimental-web-platform-features

service workers的限制是什么?

  • 出于安全原因,公网环境协议必须是HTTPS。
  • 可以在支持HTTPS的GitHub上测试。
  • 由于开发需要,localhost也允许。

service worker与Promise的关系?

service worker基于Promise,这样可以做到不阻塞main thread。

service worker与XMR的关系?

XMLHttpRequest API是不推荐的API,service worker推荐使用caching和onfetch,所以最好将重心放在对Promise的理解上。

有没有service-worker的demo可以感受一下?

在线demo:https://mdn.github.io/sw-test/
github repo:https://github.com/mdn/sw-test

service worker是真正的操作系统线程吗?

Worker interface繁衍出来真正的操作系统级别的线程,所以可能会有线程并发导致的问题。
service worker 这样的web worker,可以很好地控制和其他线程的通信节点,因此不用担心线程并发导致的问题。
service worker这样的web worker,传入传出数据到线程,需要通过序列化的对象

service worker可以调用window对象的方法吗?

不完全可以。
因为service worker工作在DedicatedWorkerGlobalScope(实现了WorkerGlobalScope),window工作在WorkerGlobalScope(实现了Window和WorkerGlobalScope)。
但是可以使用很多在window环境下可以使用的技术,例如websocket,数据存储等等。可以查阅Functions and classes available to workers

service worker可以操作DOM吗?

不可以。
因为service worker这样的web worker,没有非线程安全组件和DOM的权限

如何从开发者工具查看service worker

chrome://inspect/#service-workers-当前的service-worker信息
chrome://serviceworker-internals-可以start、stop、debug service worker process
✅启用Service Workers over HTTPfirefox developer tool
Application->Service Workers->Service workers from other origins可以查看到其他origin的service worker

@FrankKai
Copy link
Owner Author

FrankKai commented Dec 12, 2018

基础架构

1.注册serviceWorkerContainer.register()抓取并注册service worker URL。
2.作用域service worker工作在ServiceWrokerGlobalScope,无DOM权限。
3.监听此时可以处理events了。
4.安装中获取到权限后,worker可以开始安装。Install事件会首先发送到service worker(此时可以填充数据到IndexedDB,缓存网站静态资源)。这与安装一个原生app一样,此时app可以离线使用了。
5.安装成功oninstall处理器完成后,service worker安装成功。
6.激活当前service worker安装成功后,service worker收到一个activate event。onactivate事件主要用来清除上一个版本的Service worker脚本的资源。
7.service worker控制页面service worker此时开始控制页面,但仅仅能控制register()后开启的页面。document在有没有service worker的情况下都可以工作,所以若service worker想控制document,需要重新加载。

Service Worker生命周期图

image

Service Worker事件图

image

@FrankKai
Copy link
Owner Author

FrankKai commented Dec 12, 2018

注册一个worker

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw-test/sw.js', {scope: '/sw-test/'})
  .then(function(reg) {
    // registration worked
    console.log('Registration succeeded. Scope is ' + reg.scope);
  }).catch(function(error) {
    // registration failed
    console.log('Registration failed with ' + error);
  });
}
  • navigator.serviceWorker.register()本质上是navigator的serviceWorker getter生成的函数,而这个getter其实是基于ServiceWorkerContainer这个类生成的。
  • '/sw-test/sw.js'是相对window.origin的路径,window.origin在有权威域名的情况下,返回权威域名,about:blankdata:text/html,<b>love you</b>情况下返回null。
  • scope:'/sw-test/'代表我们app的所有内容都在这个目录下,service worker可以进行控制目录下内容,scope是可选的,默认值就是'/sw-test/'
  • .then()是为了确保service worker注册成功后再执行代码。
  • 注册后的service worker工作在worker context,因为工作在worker context,所以没有DOM的权限。
  • 一个service worker可以控制多个page,也就意味着每个页面没有自己单独的唯一的worker,所以在定义全局变量时一定要小心。每次scope里的页面加载时,service worker会针对该页面安装并进行操作。(这一点在跨Page/Tab通信中非常有用。无需通过window.onstorage事件做跨Page/Tab通信中。)

偶尔会注册失败会是什么原因?

  • 应用需要运行在HTTPS环境
  • service worker 文件的路径是错误的。相对origin的路径,而不是app的根路径。https://mdn.github.io/sw-test/sw.js 的path是/sw-test/sw.js,而不是/sw.js。
  • service worker被指向了一个和app不同的origin,这是不被允许的。

image

其他注意事项:

  • service worker 中从与service worker相同scope的client上获得请求。
  • service worker的最大作用域是worker的位置。
  • 如果client指定了Service-Worker-Allowed头,可以为这个worker指定一个最大作用域列表。
  • Firefox中,Service Worker API是隐藏起来的而且再private browsing mode模式下不能用。

@FrankKai
Copy link
Owner Author

FrankKai commented Nov 18, 2019

安装和激活:填充你的缓存

  • 在service worker注册成功后,浏览器会主动尝试安装并激活网页或者站点的service worker
  • 当成功安装后,会发射一个install event。install事件主要用来填充运行离线app的assets文件,也就是赋予浏览器离线缓存的能力。
  • 如何使得assets文填充到浏览器缓存呢?service worker的storage api:cache。这个api是一个service worker全局api。从而做到根据请求的key,通过response去分发assets,从而存储assets文件。
  • cache api和浏览标准cache类似,但是cache api是和域有关的。在告诉cache无效前,我们可以对他有完全的掌控。
self.addEventListener('install', (event)=>{
    event.waitUntil(
        caches.open('v1').then((cache)=>{
            return cache.addAll([
                './sw-test/',
                './sw-test/index.html',
                './sw-test/style.css',
                './sw-test/app.js',
                './sw-test/image-list.js',
                './sw-test/star-wars-logo.jpg',
                './sw-test/gallery',
                './sw-test/gallery/bountryHunters.jpg',
                './sw-test/gallery/myLittleVader.jpg',
                './sw-test/gallery/snowTroppers.jpg'
            ])
        })
    )
})
  1. 在这里我们在self上添加了一个install事件监听器,然后使用ExtendableEvent.watiUntil()方法到事件中-这样可以确保service worker直到waitUntil方法成功执行后才能install。
  2. 在waitUntil内部,我们用caches.open方法创建了一个新的cache,名字叫v1,意思这是我们网站的资源cache的第一个版本。 它返回一个带着已创建cache的promise;一旦resolve出来,我们可以调用cache的addAll方法,addAll方法接受一个包含了将要缓存的资源的相对origin的URL数组。
  3. 如果promise被reject出去,install失败,而且worker不能做任何事。这是ok的,修复bug以后可以下次注册发生后再次尝试。
  4. 一次成功的安装之后,service worker就被激活了。service worker第一次安装或者激活实际上没有太大的意义,但是在service worker更新的时候有很重大的意义。

注意:

  • localStorage和service worker cache的工作形式是很相似的,但是localStorage是同步的,所以在service worker中是不被允许的。
  • service worker中可以使用indexDB来存储数据。

@FrankKai
Copy link
Owner Author

FrankKai commented Nov 18, 2019

请求自定义响应

  • 缓存文件通过install时间和创建出的cache,已经存储在浏览器的cache中了。
  • 如何对cache的内容做处理?使用fetch事件可以很容易就做到。
    image
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request)
  );
});
  • fetch事件,会在service worker控制的资源被fetch时被触发,包括指定作用域内的documents,以及documents内的引用的任何资源(例如index.html的image有跨域请求,这仍然要经过service worker)
  • fetch事件的respondWith方法可以hijack HTTP响应然后更新响应。然后在caches.match内可以匹配每个网络请求做一个match,会通过url和headers做匹配从而返回cache中的资源,就像HTTP request一样。

一个关于fetch和service worker很好的例子

  1. Response()构造器可以自定义响应,下面的这个返回一个普通的字符串。
new Response('Hello from your friendly neighbourhood service worker!');
  1. Response中可以自定义头,headers中写即可。
new Response('<p>Hello from your friendly neighbourhood service worker!</p>', {
  headers: { 'Content-Type': 'text/html' }
});
  1. 如果没有匹配到cache中的资源,可以fetch资源的默认网络请求。在网络好用的情况下。
fetch(event.request);
  1. 网络不可用的情况下,可以访问到本地的静态文件。
caches.match('./fallback.html');
  1. 在event的Request对象中,可以查询到详细的请求信息。
event.request.url
event.request.method
event.request.headers
event.request.body

@FrankKai
Copy link
Owner Author

FrankKai commented Nov 19, 2019

恢复错误请求

  • caches.match(event.request)在service worker cache可以被匹配时时很有用的
  • 没有match时,需要异常处理,否则会返回undefined导致promise没有resolve
  • 如果没有匹配的cache资源,会从network上请求资源并添加到cache中,event.request返回
  • response.clone()添加到cache,因为request和response streams只能read一次
  • 假如cache无匹配,网络异常,需要一个默认的返回

可以像下面这样处理异常:

self.addEventListener('fetch', (event)=>{
    event.respondWith(
        caches.match(event.request).then((response)=>{
            return response || fetch(event.request).then((response) => {
                 return caches.open('v1').then((cache) => {
                     cache.put(event.request, response.clone());
                     return response;
                });  
            });
        }).catch(()=> {
            return caches.match('./sw-test/gallery/myLittleVader.jpg');
        })
    )
})

@FrankKai
Copy link
Owner Author

更新service worker

  • 刷新或者page重新加载,新版本的worker会在background安装,但是不会activated
  • 只有在旧的service worker彻底没有用的时候,才会activate新的service worker
  • 可以更新install事件,cache v2在background安装,之前的v1 cache不会被扰乱的,v1仍然被fetch。只有page不使用v1的情况下,v2被fetch
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open('v2').then((cache) => {
      return cache.addAll([
        './sw-test/',
        './sw-test/index.html',
        './sw-test/style.css',
        './sw-test/app.js',
        './sw-test/image-list.js',
        
        

        // include other new resources for the new version...
      ]);
    })
  );
});

删除旧cache

  • 还有一个事件叫”activate“,可以用来删除旧的cache
  • 可以通过caches.keys()获得所有的cache的key,然后删除掉旧的,只保留新的
self.addEventListener('activate', (event) => {
  var cacheKeeplist = ['v2'];

  event.waitUntil(
    caches.keys().then((keyList) => {
      return Promise.all(keyList.map((key) => {
        if (cacheKeeplist.indexOf(key) === -1) {
          return caches.delete(key);
        }
      }));
    })
  );
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant