利用ServiceWorker实现页面的快速加载和离线访问

Service workers 本质上充当Web应用程序与浏览器之间的代理服务器,也能够在网络可用时做为浏览器和网络间的代理。它们旨在(除其余以外)使得可以建立有效的离线体验,拦截网络请求并基于网络是否可用以及更新的资源是否驻留在服务器上来采起适当的动做。他们还容许访问推送通知和后台同步API。(引用自:连接)javascript

简单的来讲,ServiceWorker(后文简称sw)运行在页面后台,使用了sw的页面能够利用sw来拦截页面发出的请求,同时配合CacheAPI能够将请求缓存到客户本地html

所以咱们能够:java

  • 将页面的文件存储在客户端,下次打开页面能够不向服务器发出资源请求,极大的加快页面加载速度
  • 离线打开页面的同时能够在sw发出请求,更新本地的资源文件
  • 实现离线访问页面

可是也存在着一些问题linux

  • 因为打开页面再也不向服务器发出页面请求,所以当服务器上的页面有新版本的时候客户端没法及时升级
  • sw存在必定的兼容性问题

sw-compatible

IE全面扑街,pc上兼容性不太好,移动端安卓支持良好,ios要12+。但考虑到sw并不会影响的页面的正常运行,因此项目上仍是能投入生产的。webpack

基本例子

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Sw demo</title>

</head>
<body>
    
</body>
<script src="index.js"></script>
<script> if(navigator.serviceWorker){ navigator.serviceWorker.register('sw.js').then(function(reg){ if(reg.installing){ console.log('client-installing'); }else if(reg.active){ console.log('client-active') } }) } </script>
</html>
复制代码

index.js

document.body.innerHTML="hello world!";
复制代码

sw.js

var cacheStorageKey = 'one';

var cacheList = [
    "index.html",
    "index.js"
]
self.addEventListener('install', function (e) {
    console.log('sw-install');
    self.skipWaiting();
})
self.addEventListener('activate', function (e) {
    console.log('sw-activate');
    caches.open(cacheStorageKey).then(function (cache) {
        cache.addAll(cacheList)
    })
    var cacheDeletePromises = caches.keys().then(cacheNames => {
        return Promise.all(cacheNames.map(name => {
            if (name !== cacheStorageKey) {
                // if delete cache,we should post a message to client which can trigger a callback function
                console.log('caches.delete', caches.delete);
                var deletePromise = caches.delete(name);
                send_message_to_all_clients({ onUpdate: true })
                return deletePromise;
            } else {
                return Promise.resolve();
            }
        }));
    });
    e.waitUntil(
        Promise.all([cacheDeletePromises]
        ).then(() => {
            return self.clients.claim()
        })
    )
})
self.addEventListener('fetch', function (e) {
    e.respondWith(
        caches.match(e.request).then(function (response) {
            if (response != null) {
                console.log(`fetch:${e.request.url} from cache`);
                return response
            } else {
                console.log(`fetch:${e.request.url} from http`);
                return fetch(e.request.url)
            }
        })
    )
})
复制代码

说明

这样就完成了一个简单的sw页面了,如今经过服务器访问页面html、js资源将直接从客户端本地读取,实现页面的快速打开和离线访问ios

  • 客户端和sw都有不一样的事件回调,这些事件将在不一样的sw生命周期中被触发,后续会有详细介绍
  • 首次打开页面的时候sw先进行install回调,执行self.skipWaiting()后将接着执行activate,activate内对缓存列表的文件进行缓存
  • cacheStorageKey是缓存的识别码,当cacheStorageKey的值变化,sw的activate会将旧的缓存给删除掉,从新调用cache.addAll设置缓存
  • sw的fetch事件会拦截页面发出的请求,将根据缓存状况做出不一样的处理

生命周期与事件

sw应用的生命周期我简单抽象为三种git

  • 安装:页面首次打开,加载对应的sw文件
  • 活动:加载过sw文件后,打开页面
  • 更新:当服务器的sw文件与客户端的不一致的时候打开页面

客户端

名称 installing active
安装 触发 不触发
活动 不触发 触发
更新 不触发 触发

sw

名称 install activate fetch
安装 触发 触发 不触发
活动 不触发 不触发 触发
更新 触发 触发 不触发

总结一下:github

  • 客户端除了在首次打开的时候触发installing,其余都是触发的active
  • sw端活动状态只执行fetch,安装和更新状态只执行install和activate

页面与sw通讯

通讯方面我以前有翻译过文章,连接地址,你们感兴趣能够看看。这里我直接展现把封装好的通讯接口接口web

有了通讯接口,咱们就能够优化不少事情,比方说在 cacheStorageKey发生变化的时候通知页面给予客户必定的响应npm

客户端

function send_message_to_sw(msg){
    return new Promise(function(resolve, reject){
        // Create a Message Channel
        var msg_chan = new MessageChannel();
        // Handler for recieving message reply from service worker
        msg_chan.port1.onmessage = function(event){
            if(event.data.error){
                reject(event.data.error);
            }else{
                resolve(event.data);
            }
        };
        // Send message to service worker along with port for reply
        navigator.serviceWorker.controller.postMessage(msg, [msg_chan.port2]);
    });
}
复制代码

sw

function send_message_to_client(client, msg){
  return new Promise(function(resolve, reject){
      var msg_chan = new MessageChannel();
      msg_chan.port1.onmessage = function(event){
          if(event.data.error){
              reject(event.data.error);
          }else{
              resolve(event.data);
          }
      };
      client.postMessage(msg, [msg_chan.port2]);
  });
}
function send_message_to_all_clients(msg){
  clients.matchAll().then(clients => {
      clients.forEach(client => {
          send_message_to_client(client, msg).then(m => console.log("SW Received Message: "+m));
      })
  })
}
复制代码

动态缓存资源文件

上述的作法须要事先写好cacheList,有必定的维护量,如今介绍一种不须要维护cacheList的作法:

self.addEventListener('fetch', function (e) {
  e.respondWith(
    caches.match(e.request).then(res => {
      return res ||
        fetch(e.request)
          .then(res => {
            const resClone = res.clone();
            caches.open(cacheStorageKey).then(cache => {
              cache.put(e.request, resClone);
            })
            return res;
          })
    })
  )
});
复制代码

这样作的话缓存资源的操做将从activate转移到fetch事件内,fetch事件先判断有没有缓存,没有缓存的话将发出对应的请求并进行缓存

这样的作法的缺点是没法在首次加载页面的时候就完成静态化,由于sw的安装声明周期是不会触发sw的fetch事件的。

页面url带参数

针对一些页面渲染结果与url参数有关的状况,上述的架构没法完成对应的本地化需求。以前的作法是在cacheList加入了入口页面的地址,没法适应带动态参数url的状况。

在fetch内动态缓存请求

具体作法在动态缓存资源文件章节有描述,再也不重复描述。

使用通讯接口通知sw缓存入口页面

客户端

navigator.serviceWorker.register(file).then(function (reg) {
    if (reg.installing) {
        //send_message_to_sw({pageUrl:location.href})
    }
    if (reg.active) {
        send_message_to_sw({pageUrl:location.href})
    }
    return reg;
})
复制代码

sw

self.addEventListener('message',function(e){
  var data=e.data;
  if(data.pageUrl){
    addPage(data.pageUrl)
  }
})
function addPage(page){
  caches.open(cacheStorageKey).then(function(cache){
    cache.add(page);
  })
}
复制代码

在客户端的active发消息给sw,sw就可以获取到对应的页面url进行缓存。

:客户端的installing事件无法使用消息接口,你们能够在sw的activate事件向客户端发出一个消息请求获取当前页面url

常见问题

sw文件至少要与入口页面文件在同一目录下,如:

  • /sw.js 能够管理 /index.html
  • /js/sw.js 不能管理 /index.html

笔者在这里踩了好久的坑...

webpack-sw-plugin

介绍一个笔者写的webpack的sw插件,在弄sw页面的时候很方便,github地址

安装

npm install --save-dev webpack-sw-plugin
复制代码

webpack配置

const WebpackSWPlugin = require('webpack-sw-plugin');
module.exports = {
    // entry
    // output
    plugins:[
        new WebpackSWPlugin()
    ]
}
复制代码

客户端配置

import worker from 'webpack-sw-plugin/lib/worker';
worker.register({
    onUpdate:()=>{
        console.log('client has a new version. page will refresh in 5s....');
        setTimeout(function(){
            window.location.reload();
        },5000)
    }
});
复制代码

效果

  • 自动生成页面与sw交互的体系,无需提供额外的sw文件
  • 自动适配url带参数的状况
  • 当webpack输出文件变化的时候,客户端的onUpdate将会被触发,上述例子中当输出文件变化时,客户端将会在5秒后进行刷新,刷新后将会使用全新的文件
相关文章
相关标签/搜索