关于前端:Service-Worker离线应用与后台同步的解决方案

42次阅读

共计 9724 个字符,预计需要花费 25 分钟才能阅读完成。

前端罕用缓存技术

前端罕用缓存技术个别分为 http 缓存和浏览器缓存。

HTTP 缓存

Expires

HTTP1.0 的内容,服务器应用 Expires 头来通知 Web 客户端它能够应用以后正本,直到指定的工夫为止;

Cache-Control

HTTP1.1 引入了 Cathe-Control,它应用 max-age 指定资源被缓存多久,次要是解决了 Expires 一个重大的缺点,就是它设置的是一个固定的工夫点,客户端工夫和服务端工夫可能有误差;

Last-Modified / If-Modified-Since

Last-Modified 是服务器通知浏览器该资源的最初批改工夫,If-Modified-Since 是申请头带上的,上次服务器给本人的该资源的最初批改工夫。而后服务器拿去比照。
若 Last-Modified 大于 If-Modified-Since,阐明资源有被改变过,则响应整片资源内容,返回状态码 200;
若 Last-Modified 小于或等于 If-Modified-Since,阐明资源无新批改,则响应 HTTP 304,告知浏览器持续应用以后版本。

Etag / If-None-Match

Etag 是服务器依据每个资源生成的惟一标识符,当文件内容被批改时标识符就会从新生成。服务器存储着文件的 Etag 字段,能够在与每次客户端传送 If-none-match 的字段进行比拟。如果相等,则示意未修改,响应 304;反之,则示意已批改,响应 200 状态码,返回数据。

浏览器缓存

Storage

简略的缓存形式有 cookie,localStorage 和 sessionStorage,都是浏览器内置贮存性能。

mainfest

html5 引入的新规范,能够离线缓存动态文件。

Service Worker

ServiceWorker 介绍

什么是 ServiceWorker

Service Worker 实质上是充当 web 应用程序与浏览器之间的代理服务器,也能够在网络可用时作为浏览器和网络间的代理。它们的目标是创立无效的离线体验、拦挡网络申请并依据网络是否可用采取适当的操作,以及更新服务器上的资产。它们还容许拜访推送告诉和后盾同步 API。

ServiceWorker 个性

  • Service Worker 实质上是一个 Web Worker,它独立于 Javascript 主线程,因而它不能间接拜访 DOM,也不能间接拜访 window 对象,然而能够拜访 navigator 对象,也能够通过消息传递的形式(如 postMessage)与 Javascript 主线程进行通信。
  • Service Worker 独立于 Javascript 主线程,所以不会造成阻塞。它设计为齐全异步,同步 API(如 XHR 和 localStorage 不能在 Service Worker 中应用。
  • Service Worker 是基于 HTTPS 的,因为 Service Worker 中波及到申请拦挡,所以必须应用 HTTPS 协定来保障平安。如果是本地调试的话,localhost 是能够的。
  • Service Worker 领有独立的生命周期,与页面无关 (关联页面未敞开时,它也能够退出,没有关联页面时,它也能够启动)。注册 Service Worker 后,浏览器会默默地在背地装置 Service Worker。

ServiceWorker 生命周期

Service Worker 的生命周期能够分为 6 个阶段:解析 (parsed)、装置 (installing)、装置实现 (installed)、激活 (activating)、激活实现 (activated)、闲置 (redundant)。

Parsed

当咱们第一次尝试注册 Service Worker 时,用户代理会解析脚本并获取入口点。如果解析胜利(并且满足其余一些要求,例如 HTTPS),咱们将能够拜访 Service Worker 注册对象。其中蕴含无关 Service Worker 的状态及其作用域的信息。

if ('serviceWorker' in navigator) {navigator.serviceWorker.register('./sw.js')
  .then(function(registration) {console.log("Service Worker Registered", registration);
  })
  .catch(function(err) {console.log("Service Worker Failed to Register", err);
  })
}

Service Worker 注册胜利并不意味着它已装置结束或处于激活状态,而只是意味着脚本已胜利解析,它与文档处于同一源上,且源为 HTTPS。注册实现后,服务 Worker 将进入下一个状态。

Installing

一旦 Service Worker 脚本被解析,用户代理就会尝试装置它,并进入装置状态。在 Service Worker 的 registration 对象中,咱们能够在 installing 属性中查看此状态。

并且,在 installing 状态下,install 事件会被触发,咱们个别会在这个回调中解决缓存事件。

navigator.serviceWorker.register('./sw.js').then(function(registration) {if (registration.installing) {// Service Worker is Installing}
})
self.addEventListener('install', function(event) {
  event.waitUntil(caches.open(currentCacheName).then(function(cache) {return cache.addAll(arrayOfFilesToCache);
    })
  );
});

如果事件中有 event.waitUntil() 办法,其中的 Promise 只有在 resolve 后,install 事件才会胜利。如果 Promise 被回绝,install 就会失败,Service Worker 就会变为 redundant 状态。

self.addEventListener('install', function(event) {
  event.waitUntil(return Promise.reject(); // Failure
  );
});

Installed / Waiting

如果装置胜利,Service Worker 的状态变为 installed(也叫 waiting)。处于这个状态时,Service Worker 是无效的然而是未激活的 worker,临时没有管制页面的势力,须要期待从以后 worker 取得控制权。

咱们能够在 registration 对象的 waiting 属性中检测到此状态。

navigator.serviceWorker.register('./sw.js').then(function(registration) {if (registration.waiting) {// Service Worker is Waiting}
})

咱们能够在这个机会去更新新版本或自动更新缓存。

Activating

在以下状况之一时,处于 Waiting 状态的 worker 的 Activating 状态会被触发:

  • 以后没有处于激活状态的 worker
  • self.skipWaiting() 在 sw.js 中被调用,间接跳过 waiting 阶段
  • 用户导航来到以后页面,从而开释了前一个 active worker
  • 通过了指定时间段,从而开释了前一个 active worker

在以后状态下,activate 事件会被触发,在这个回调中咱们通常用于革除旧缓存。

self.addEventListener('activate', function(event) {
  event.waitUntil(
    // Get all the cache names
    caches.keys().then(function(cacheNames) {
      return Promise.all(
        // Get all the items that are stored under a different cache name than the current one
        cacheNames.filter(function(cacheName) {return cacheName != currentCacheName;}).map(function(cacheName) {
          // Delete the items
          return caches.delete(cacheName);
        })
      ); // end Promise.all()}) // end caches.keys()); // end event.waitUntil()});

同 install 事件,如果 Promise 被 reject 了,则 activate 事件失败,Service Worker 变为 redundant 状态。

Activated

如果激活胜利,Service Worker 状态会变成 active,在这个状态下,Service Worker 是一个能够齐全管制网页的激活 worker,咱们能够在 registration 对象的 active 属性中检测到此状态。

navigator.serviceWorker.register('./sw.js').then(function(registration) {if (registration.active) {// Service Worker is Active}
})

当 Service Worker 被胜利激活后,即可解决绑定的 fetch 和 message 事件。

self.addEventListener('fetch', function(event) {// Do stuff with fetch events});

self.addEventListener('message', function(event) {// Do stuff with postMessages received from document});

Redundant

以下任一状况,Service Worker 都会变成 redundant。

  • install 失败
  • activate 失败
  • 有新的 Service Worker 将其代替成为现有的激活 worker

Service Worker 离线缓存

Service Worker 最重要的性能之一,就是能够通过缓存动态资源来实现离线拜访咱们的页面。

Service Worker 的缓存基于 CacheStorage,它是一个 Promise 对象,咱们能够通过 caches 来获取它。CacheStorage 提供了一些办法,咱们能够通过这些办法来对缓存进行操作。

caches.open(currentCacheName).then(function (cache) {
  /** 能够通过 cache.put 来增加缓存
   *  它接管两个参数,第一个参数是 Request 对象或 URL 字符串,第二个参数是 Response 对象
  */
  cache.put(new Request('/'), new Response('Hello World'));
  
    /** 能够通过 cache.addAll 来增加缓存资源数组
   *  它接管一个参数,这个参数能够是 Request 对象数组,也能够是 URL 字符串数组
  */
  cache.addAll(['/'])

  /** 能够通过 cache.match 来获取缓存
   *  它接管一个参数,这个参数能够是 Request 对象,也能够是 URL 字符串
  */
  cache.match('/').then(function (response) {console.log(response);
  });
  
  /** 能够通过 cache.delete 来删除缓存
   *  它接管一个参数,这个参数能够是 Request 对象,也能够是 URL 字符串
  */
  cache.delete('/').then(function () {console.log('删除胜利');
  });

  /** 能够通过 cache.keys 来获取缓存的 key
   *  而后通过 cache.delete 来删除缓存
  */
  cache.keys().then(function (keys) {keys.forEach(function (key) {cache.delete(key);
    });
  });
});

缓存资源

咱们在介绍生命周期的时候咱们介绍了在 installing 状态下会调用 install 办法,通常咱们会在 install 事件中缓存一些资源。

self.addEventListener('install', function (event) {
  event.waitUntil(caches.open(currentCacheName).then(function (cache) {
      return cache.addAll([
        '/',
        '/index.css',
        '/axios.js',
        '/index.html'
      ]);
    })
  );
});

下面的代码中咱们缓存了一些资源,所以咱们能够在 fetch 事件中获取并返回刚刚缓存的资源。

self.addEventListener('fetch', function (event) {
  event.respondWith(caches.match(event.request).then(function (response) {if (response) {return response;}
      return fetch(event.request);
    })
  );
});

下面的代码中咱们应用 caches.match 来匹配申请,如果匹配到了,那么就返回缓存的资源,如果没有匹配到,那么就从网络中获取资源。

缓存更新

在下面的步骤中,咱们曾经缓存了咱们的资源,并且该资源并不会随着咱们代码或者资源的更改而更新缓存。因而,咱们能够通过版本号来管制更新。

介绍生命周期时,咱们有理解到在 activating 状态下会触发 activate 回调,在该回调中咱们能够革除旧缓存,而后在 install 事件中缓存新的资源。

const version = '2.0';
const currentCache = 'my-cache' + version;

self.addEventListener('activate', function (event) {
  event.waitUntil(caches.keys().then(function (cacheNames) {
      return Promise.all(cacheNames.map(function (cacheName) {if (cacheName !== currentCache) {return caches.delete(cacheName);
          }
        })
      );
    })
  );
});

卸载

当咱们的页面不再须要 Service Worker 的时候,能够通过在新版本里应用 unregister 进行卸载。

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.ready.then(registration => {registration.unregister();
  });
}

须要留神的是,Service Worker 卸载并不会删掉咱们之前缓存的资源,所以在卸载之前咱们须要革除所有的缓存。

缓存策略

从下面的例子能够看出,Service Worker 的缓存是通过 Cache 接口和 fetch 事件独特实现的。通过 Cache 接口和 fetch 事件能够实现多种缓存策略。

仅缓存 (Cache only)

实用于你认为属于该“版本”网站动态内容的任何资源,匹配的申请将只会进入缓存。

仅网络 (Network only)

与“仅缓存”相同,“仅限网络”是指申请通过 Service Worker 传递到网络,而无需与 Service Worker 缓存进行任何交互。

缓存优先 (Cache first)

该策略流程如下:

  • 申请达到缓存。如果资源位于缓存中,请从缓存中提供。
  • 如果申请不在缓存中,请转到网络。
  • 网络申请实现后,将其增加到缓存中,而后从网络返回响应。

该策略实用于动态资源的缓存,它能够绕过 HTTP 缓存可能启动的服务器执行任何内容新鲜度查看,从而放慢不可变资源的速度。

网络优先 (Network first)

该策略如下:

  • 先返回网络申请一个申请,而后将响应放入缓存中。
  • 如果您日后处于离线状态,则会回退到缓存中该响应的最新版本。

此策略非常适合 HTML 或 API 申请,当您想在线获取资源的最新版本,同时又心愿离线能够拜访到最新的可用版本。

提早验证 (Stale-while-revalidate)

该机制与最初两种策略相似,但其过程优先思考资源访问速度,同时还在后盾放弃更新。策略大抵如下:

  • 在第一次申请获取资源时,从网络中提取资源,将其放入缓存中并返回网络响应。
  • 对于后续申请,首先从缓存提供资源,而后“在后盾”从网络从新申请该资源,并更新资源的缓存条目。
  • 对于尔后的申请,您将收到在上一步中从缓存中搁置的最初一个网络提取的版本。

Service Worker 后盾同步

假如用户在咱们的页面上操作了数据并提交,此时正好进入一个网络极差甚至断网的环境里,用户只能看着始终处于 loading 状态的页面,直到失去急躁敞开页面,这时申请就曾经被中断了。

下面这种状况裸露了两个问题:

  • 一般页面会随着页面敞开而终止
  • 网络极差或无网络状况下没用一种解决方案可能解决并维持以后申请以待有网时复原申请

后盾同步是构建在 Service Worker 过程之上的另一个性能,它容许一次性或以一个工夫距离申请后盾数据同步。咱们能够充分利用这一性能躲避以上问题。

工作流程

  • 在 Service Worker 中监听 sync 事件
  • 在浏览器中发动后盾同步 sync
  • 就会触发 Service Worker 的 sync 事件, 在该监听的回调中进行操作, 例如向后端发动申请
  • 而后能够在 Service Worker 中对服务端返回的数据进行解决

页面触发同步

if ('serviceWorker' in navigator) {navigator.serviceWorker.register('./sw.js')
  
  navigator.serviceWorker.ready.then(function (registration) {
    let tag = "data_sync";
    document.getElementById('submit-btn').addEventListener('click', function () {registration.sync.register(tag).then(function () {console.log('后盾同步已触发', tag);
      }).catch(function (err) {console.log('后盾同步触发失败', err);
      });
    });
  })
}

因为后盾同步性能须要在 Service Worker 注册实现后触发, 所以咱们能够应用 navigator.serviceWorker.ready 期待注册实现筹备好之后应用 registration.sync.register 注册同步事件。

registration.sync 会返回一个 SyncManager 对象其中蕴含 register 办法和 getTags 办法。

SW 监听同步事件

当页面触发同步事件后,咱们须要通过 Service Worker 来解决 sync 事件。

self.addEventListener('sync', function (e) {let init = { method: 'GET'};
  
  switch (e.tag){
    case "data_sync":
      let request = new Request(`xxxxx/sync`, init);
      e.waitUntil(fetch(request).then(function (response) {return response;})
      );
      break;
  }
});

Taro 我的项目集成

实践说完了,接下来咱们能够在 taro 我的项目里实际接入 Service Worker。

俗话说,站在伟人的肩膀上看世界。

当初市面上实现 SW 的工具十分多,其中 google 团队提供了一个非常弱小且欠缺的插件 workbox-webpack-plugin,接下来咱们将通过这个插件实现 Service Worker 的离线缓存性能。

插件配置

workbox-webpack-plugin 提供了两个类名为 GenerateSW 和 InjectManifest,接下来咱们通过应用 GenerateSW 来实现预缓存文件和简略的运行时缓存需要。

在 taro 我的项目负责打包的 config 文件中退出以下配置:

const {GenerateSW} = require('workbox-webpack-plugin');

const config = {
  ...
  h5: {
    ...
    webpackChain(chain) {
        ...
      chain.plugin('generateSW').use(new GenerateSW({
        clientsClaim: true,
        skipWaiting: true,
        runtimeCaching: [
          {
            urlPattern: /.\/*/, // 须要缓存的门路
            handler: 'StaleWhileRevalidate', // 缓存策略
            options: {
              cacheName: 'my-webcache',
              expiration: {maxEntries: 2000,},
            },
          }],
      }));
    }
  }
}

退出以上配置后,咱们运行 build 命令能够发现该插件为咱们主动生成了 Service Worker 文件。

Service Worker 注册

生成 Service Worker 文件之后咱们须要在我的项目中进行注册。

在 register 文件中解决 Service Worker 的生命周期、状态等信息。

import {register} from 'register-service-worker';

register('./service-worker.js', {registrationOptions: { scope: './'},
  ready(registration) {console.log('Service worker is active.', registration);
  },
  registered(registration) {console.log('Service worker has been registered.', registration);
  },
  cached(registration) {console.log('Content has been cached for offline use.', registration);
  },
  updatefound(registration) {console.log('New content is downloading.', registration);
  },
  updated(registration) {console.log('New content is available; please refresh.', registration);
  },
  offline() {console.log('No internet connection found. App is running in offline mode.');
  },
  error(error) {console.error('Error during service worker registration:', error);
  },
});

在 app.ts 中引入该文件,咱们就实现了简略的 Service Worker 的引入。接下来把我的项目启动,让咱们看看 SW 是否失效。

在失常网络环境中,能够看到咱们发动第一次拜访的申请列表。

在把网络设置成离线状态后,能够看到咱们的申请仍然失常返回,并走的是 Service Worker 的缓存。

咱们也能够在控制台看到所有缓存的文件列表。

总的来说,Service Worker 是一个十分弱小的性能,除了以上介绍的离线缓存和后盾同步性能,还能够通过 SW 实现音讯推送、多页面通信等等性能。

(本文作者:龚思晗)

正文完
 0