PWA做离线数据缓存实现与探索

PWA

Service workerPWA得以实现的核心技术

service worker

Service worker 是一个独立的worker线程,独立于当前网页进程,是一种特殊的web worker。主要功能在生命周期函数中实现。

service worker注册

if('serviceWorker' in navigator) {
  const sw = await navigator.serviceWorker.register(serviceWorker文件路径);
}
//使用serviceworker-webpack-plugin插件注册方式
import runtime from 'serviceworker-webpack-plugin/lib/runtime'
if('serviceWorker' in navigator) {
  const sw = await runtime.register();
}

serviceworker-webpack-plugin插件可以将所有打包后的目录文件注入到打包后的sw.js文件,通过global.serviceWorkerOptions.assets获取所有目录文件名,便于做静态资源的缓存。

PWA用到的service worker生命周期函数

  • install缓存所有你需要的静态资源

    self.addEventListener('install', async () => {
      console.log('service worker installing');
      const cache = await caches.open(CACHE_NAME); //cacheStorage中缓存的名称
      const CACHE_URL = global.serviceWorkerOptions.assets.concat(['/']); //缓存的静态资源目录,不要忘记缓存'/'目录文件,在断网情况下,页面首先加载的是'/'目录资源。
      await cache.addAll(CACHE_URL); //此处只要有一个资源无法下载,静态资源的缓存都会失败
      await self.skipWaiting(); //跳过等待,保持运行最新的service worker
    })
    
  • active删除旧的缓存

    self.addEventListener('activate', async () => {
      console.log('service worker activate');
      const cacheKeys = await caches.keys();
      cacheKeys.map(async item => { //删除旧的缓存
        if (item !== CACHE_NAME) {
          await caches.delete(item);
        }
      })
      await self.clients.claim();//接管所有页面
    }
    
  • fetch可以拦截所有的请求,并做数据缓存

    self.addEventListener('fetch', async e => {
      console.log('service worker fetch');
      const req = e.request;
      const url = new URL(req.url);
      const api = new URL(apiHost); //自己使用的请求域名
      let isNetworkerFirst, isCacheFirst;
      if (isNetworkFirst) {
        e.respondWith(networkFirst(req)); //网络优先
      } else if (isCahceFirst) {
        e.respondWith(cacheFirst(req));//缓存优先
      }
    })
    
    const cacheFirst = async req => { //先从缓存中获取数据,如果没有匹配到,再发起网络请求
      const cache = await caches.open(CACHE_NAME);
      let cacheData = await cache.match(req);
      if (!cacheData) {
        cacheData = await fetch(req);
        if (!cacheData || cacheData.status !== 200) return cacheData;
        const cache = await caches.open(CACHE_NAME);
        cache.put(req, cacheData.clone());
      }
      return cacheData;
    };
    
    const networkFirst = async req => { //先发起网络请求,如果失败则再从缓存中匹配
      const cache = await caches.open(CACHE_NAME);
      let fetchResult;
      try {
        await Promise.race([requestPromise(req).then(res => {
          fetchResult = res;
          if (timer) clearTimeout(timer);
          if (isNetworkSlowly) isNetworkSlowly = false;
          hasShowNotification = false;
        }), timeout_promise()]);
        if (!fetchResult || fetchResult.status !== 200) return fetchResult;
        cache.put(req, fetchResult.clone());
        return fetchResult;
      } catch (e) {
        const cacheData = await cache.match(req);
        if (navigator.onLine && cacheData && e === 'request timeout' && isNetworkSlowly && !hasShowNotification) {
          showLocalNotification('网络不给力,当前访问的是缓存数据');
          isNetworkSlowly = false;
          hasShowNotification = true;
        }
        console.log(e, cacheData, 'error')
        return cacheData;
      }
    }
    
    //设置一定时间,在原本的fetch请求还没有响应的情况下,让service worker中的fetch报出’request timeout‘错误,从而转向向cache中匹配请求资源,实现在弱网情况下的网页正常浏览
    const timeout_promise = () => {
      return new Promise((resolve, reject) => {
        timer = setTimeout(() => {
          if (!isNetworkSlowly) isNetworkSlowly = true;
          reject('request timeout');
        }, 9000);
      });
    } 
    
    const showLocalNotification = (title, body) => {
      const options = {};
      try {
        self.registration.showNotification(title, options);
      } catch (error) {
        console.warn(error);
      }
    };
    

    根据自己的需求选择网络优先还是缓存优先,例如isNetworkFirst = url.origin === self.origin && req.method === 'GET'e.respondWith()对拦截的请求,把缓存匹配的或者网络请求到的数据返回,作出最后的响应。self.registration.showNotification()向浏览器发送消息。

其他相关

  • 缓存使用到的cacheStorage可见详情文档

  • 浏览器可以通过addEventListener('offline', () => {})addEventListener('online', () => {})来监听浏览器网络在线与离线状态。但无法判断弱网状态,弱网状态请求缓存数据的具体实现也可根据自己的项目逻辑来定(例如:请求超过10秒还未得到响应,判定为弱网状态)。

  • service worker 注册后,对静态资源的下载缓存会占用部分带宽,影响项目首页的加载速度,可以设置一个定时器,在一定的时间后才启动注册程序。

  • cacheStorage无法缓存POST请求的数据。

  • 向浏览器发送消息,首先需要获取相应的权限。permission = await window.Notification.requestPermission()获取浏览器发送提醒消息权限,permission = 'granted'时,允许发送消息。

  • 使用self.skipWaiting()可以保证执行最新的sw,但新旧sw的交替,往往都要经过service workerinstall->waiting->active,因此总会有页面前后期由不同的sw来处理的问题。

你可能感兴趣的:(PWA做离线数据缓存实现与探索)