service worker 的运用与实践

Service Worker 简介

Service Workers 本质上是一种能在浏览器后台运行的独立线程,它能够在网页关闭后持续运行,能够拦截网络请求并根据网络是否可用来采取适当的动作、更新来自服务器的的资源,从而实现拦截和加工网络请求、消息推送、静默更新、事件同步等一系列功能,是 PWA 应用的核心技术之一。

与普通 JS 运行环境相比,Service Workers 有如下特点:

  • 基于web worker(JavaScript主线程的独立线程,如果执行消耗大量资源的操作也不会堵塞主线程)
  • 在web worker的基础上增加了离线缓存的能力
  • 本质上充当Web应用程序(服务器)与浏览器之间的代理服务器
  • 创建有效的离线体验(将一些不常更新的内容缓存在浏览器,提高访问体验)
  • 由事件驱动的,具有生命周期
  • 可以访问cache和indexDB
  • 支持消息推送
  • 并且可以让开发者自己控制管理缓存的内容以及版本
  • 无法直接访问 DOM,可以通过 postMessage 接口把数据传递给其他 JS 文件
  • 必须在 HTTPS 协议或本地localhost下运行。
  • 更多无限可能
使用场景

Server Worker在PWA之外也有诸多应用,基于它对HTTP请求和响应的强大管理能力,它可以作为多种依赖网络的应用的核心流程管理器。

cache.addAll(urlsToPrefetch.map(function(urlToPrefetch) {
  return new Request(urlToPrefetch, { mode: 'no-cors' });
})).then(function() {
  console.log('All resources have been fetched and cached.');
}); 

30X的HTTP状态码尚不支持离线请求重定向, 这是一个已知的issue。建议在官方支持离线重定向前,根据你的使用场景寻找其他方案,
在使用Service Worker代理HTTP的响应体时,务必记住clone response,而不要直接消费掉响应体。 原因是HTTP response是一个 流, 它的内容只能被消费一次。 只要我们仍然希望既能让浏览器正确的获得响应体中的内容,又能是它被缓存或者在Service Worker作内容检查,请不要忘记复制一个响应体。

  • 全静态站点
    如果一个网站只包含静态数据而无需服务, 我们可以缓存所有的html页面,css样式,脚本和图片等资源,来使得这个页面在初次打开后可以被完全地离线访问。
  • 预加载
    为了优化首屏渲染,页面上非必要的资源通常被延迟加载直到它们被需要。这类资源使用Server Worker来加载既可以使得在需要被加载时有良好的体验,有不会影响到首屏性能。
  • 应变响应
    有时候HTTP请求可能会因为不确定因素失败(如服务器离线,网络中断等),此时为用户提供一个应变的响应比如展示上一次成功加载的资源/数据。(例如:实时数据监测)
    Service worker可以帮助验证请求是否成功,如果失败就提供应变策略的回复。
  • 仿造响应
    仿造响应是非常有用。它可以帮助我们隔离部分特定的请求来使用给定的回复,或者我们可以用它来测试一些尚不可用,或者不能稳定重现问题的资源或者REST API.
  • 窗口缓存
    Service Worker来承担缓存数据的责任,页面可以直接使用window.cache来访问缓存。
    通过窗口缓存作为媒介可以间接实现service worker向页面的数据传递,也可以将Service Worker用作缓存的生产者而页面作为消费者。
  • 在Service Worker中使用fetch API来转发请求,请求中默认不会包含cookie等中的用户认证信息。
    如果需要为转发请求附带认证信息, 在fetch请求中添加'credentials'的参数:
fetch(url, {
  credentials: 'include'
})

跨域资源默认是不支持缓存的,需要额外参数。

  • 如果目标资源支持CORS,在构建请求需要附带参数 {mode: 'cors'} 。
  • 如果目标资源不支持CORS或者不确定, 我们可以使用 non-cors模式,
    但这会导致"不透明"的响应, 意味着Service Worker不能判断响应中的状态,不透明的结果被缓存后仍被页面消费成non-cors的响应。
生命周期

其生命周期分为首次加载更新加载
首次访问页面时候Service Worker会立即被下载下来并进行尝试安装,安装成功后就会尝试去激活等操作
更新在默认情况下Service Worker一定会每24小时被下载一次,如果下载的文件是最新文件,那么它就会被重新注册和安装但不会被激活,当不再有页面使用旧的 Service Worker 的时候,它就会被激活。

用户首次访问service worker控制的网站或页面时,service worker会立刻被下载。
之后,在以下情况将会触发更新:
一个前往作用域内页面的导航
service worker 上的一个事件被触发并且过去 24 小时没有被下载
无论它与现有service worker不同(字节对比),还是第一次在页面或网站遇到service worker,如果下载的文件是新的,安装就会尝试进行。
如果这是首次启用service worker,页面会首先尝试安装,安装成功后它会被激活。
如果现有service worker已启用,新版本会在后台安装,但不会被激活,这个时序称为worker in waiting。直到所有已加载的页面不再使用旧的service worker才会激活新的service worker。只要页面不再依赖旧的service worker,新的service worker会被激活(成为active worker)。

首次加载
  • 注册(register)
  • 安装(installing)
  • 活动(activated)或者异常(error)
  • 空闲(idle)
  • 拦截(fetch)或终止(terminated)
更新加载
  • 更新(update)
  • 安装(installing)
  • 等待活动(waiting)或者异常(error)
页面加载完成注册
if('serviceWorker' in navigator) {
    window.addEventListener('load', function() {
       navigator.serviceWorker.register('sw.js', { scope: './' })
        .then(function (reg) {
          console.log('success', reg);
        })
        .catch(function (err) {
          console.log('fail', err);
        });
  });
}
安装
self.addEventListener('install', function (event) {
    event.waitUntil(
        caches.open(CACHE_NAME)
            .then(function(cache) {
                return cache.addAll(urlsToCache);
            })
    );
});
  • 因为缓存文件需要时间所以可以通过waitUntil来防止缓存未完成就关闭serviceWorker一旦所有文件缓存成功那么serviceWorker就安装成功了,只要一个缓存失败就会导致安装失败waitUntil也会通过内部promise来获取安装事件和是否成功
激活

一旦首次安装成功后或者sw进行更新就会触发activated相对首次安装会直接进入激活状态更新触发会显得比较复杂比如
A为老的sw
B为新的sw
B进入安装更新阶段时候A还在工作状态那么B就会进waiting阶段,只有等到A被terminated后,B才能完全替换A的工作

activate阶段可以做很多有意义的事情,比如更新存储在cache中的key和value,可以清理旧缓存和旧的service worker关联的东西

self.addEventListener('activate', function(event) {
  console.log('Service Worker activate');
    event.waitUntil(
        // 遍历 caches 里所有缓存的 keys 值
        caches.keys().then(function() {
          return caches.keys().then(function (keys) {
              var all = keys.map(function (key) {
                  if (key.indexOf(CACHE_NAME) !== -1){
                      console.log('[SW]: Delete cache:' + key);
                      return caches.delete(key);
                  }
              });
              return Promise.all(all);
          });
      })
    );
});
终止
terminated终止状态一般触发条件由下面几种方式:
  • 关闭浏览器一段时间
  • 手动清除serviceworker
  • 在sw安装时直接跳过waiting阶段
拦截

fetch拦截阶段是sw最终要和关键阶段,主要用于拦截代理所有指定的请求,然后进行二次相应的处理操作通过这个阶段我们可以实现很多有意思的操作

输出缓存
self.addEventListener('fetch', function(event) {
    event.respondWith(
        caches.match(event.request)
            .then(function(response) {
                //该fetch请求已经缓存
                if (response) {
                    return response;
                }
                return fetch(event.request);
                }
            )
    );
});
输出JSON
self.addEventListener("fetch", event => {
  const data = {
    hello: "world"
  }

  const json = JSON.stringify(data, null, 2)

  return event.respondWith(
    new Response(json, {
      headers: {
        "content-type": "application/json;charset=UTF-8"
      }
    })
  )
})
输出HTML
const html = `<!DOCTYPE html>
<body>
  <h1>Hello World</h1>
  <p>This markup was generated by a Cloudflare Worker.</p>
</body>`

async function handleRequest(request) {
  return new Response(html, {
    headers: {
      "content-type": "text/html;charset=UTF-8",
    },
  })
}
addEventListener("fetch", event => {
  return event.respondWith(handleRequest(event.request))
})
重定向URL
const destinationURL = "https://www.baidu.com"
const statusCode = 301

async function handleRequest(request) {
  return Response.redirect(destinationURL, statusCode)
}

addEventListener("fetch", async event => {
  event.respondWith(handleRequest(event.request))
})
缓存一些静态文件
cons CACHE_NAME="my-site-v1"

self.addEventListener('install', function (event) {
  
  let url_list=[
      '/',
      '/static/xxx.css',
      '/static/xxx.js',
      'https://www.baidu.com/img/pc_629ee8886a9c20e7f3cb1d2889c3e45d.gif',
      '/static/xxx`.txt',
  ]; 
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(function(cache) {
        consloe.log("缓存打开成功");
        cache.addAll(url_list).then(function(){
            consloe.log("所有资源都已获取并缓存");
        });
      }).catch(function(error) {
          console.log('缓存打开失败:', error);
        })
  ); 
});

更多例子

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容