Service Worker:透過的なキャッシュ更新

オフラインアプリケーションを作成するためのテクノロジーとしてのService Workesは、さまざまなリソースのキャッシュに非常に適しています。 ローカルキャッシュを使用してサービスワーカーで作業するためのさまざまな戦術が、インターネットで詳細に説明されています。



キャッシュ内のファイルを更新する方法-1つのことを説明していません。 GoogleとMDNが提供する唯一のことは、さまざまなタイプのリソース用にいくつかのキャッシュを作成し、必要に応じてsw.jsワーカーのスクリプトサービスでこのキャッシュのバージョンを変更することです。その後、すべて削除されます。



キャッシュの削除
var CURRENT_CACHES = { font: 'font-cache-v1', css:'css-cache-v1', js:'js-cache-v1' }; self.addEventListener('activate', function(event) { var expectedCacheNames = Object.keys(CURRENT_CACHES).map(function(key) { return CURRENT_CACHES[key]; }); // Delete out of date cahes event.waitUntil( caches.keys().then(function(cacheNames) { return Promise.all( cacheNames.map(function(cacheName) { if (expectedCacheNames.indexOf(cacheName) == -1) { console.log('Deleting out of date cache:', cacheName); return caches.delete(cacheName); } }) ); }) ); });
      
      







つまり、たとえば、10個のjsファイルがあり、そのうちの1つを変更した場合、すべてのユーザーがすべてのjsファイルをオーバーロードする必要があります。 むしろ不器用な仕事。



サードパーティ製品のうち(Google開発者によるものですが)、最も近いService Worker更新ソリューションはsw-precacheライブラリです。 開発者が変更追跡を設定するsw.jsにすべてのファイルのハッシュを追加します。 次回サービスがアクティブになったときにサーバー上でそれらの少なくとも1つを変更すると、クライアントのキャッシュ全体が再び更新されますが、プログラマの特別な動きはありません。 xは包丁に置き換えられました。



問題の声明



キャッシュワーカーサービスのキャッシュファイルの透過的で信頼性の高い更新が必要です。 これは、開発者が変更されたファイルをサーバーにアップロードし、ユーザーのみが次の呼び出し/要求でそれらを自動的に更新することを意味します。 この問題を解決してみましょう。



一般的なGoogleサービスワーカーの例を見てみましょう。「最初はキャッシュから、そうでない場合はネットワークから」という原則に基づいて作業します。



まず、監視対象ファイルのリストが必要であることは明らかです。 また、何らかの方法でそれらをキャッシュ内のファイルバージョンと比較する必要があります。 これは、サーバーまたはクライアントで実行できます。



オプション1



クッキーを使用します。 ユーザーのCookieに、最後にアクセスした時間を記録します。 次回の訪問で、サーバー上で監視対象ファイルの変更時間と比較し、サービスワーカーを登録する直前に、ページのhtmlコードでその時点から変更されたファイルのリストを転送します。 これを行うには、そのようなphpファイルの出力をそこに含めます。



updated_resources.php
 <?php $files = [ "/css/fonts.css", "/css/custom.css", "/js/m-js.js", "/js/header.css"]; $la = $_COOKIE["vg-last-access"]; if(!isset($la)) $la = 0; forEach($files as $file) { if (filemtime(__DIR__ . "/../.." . $file) > $la) { $updated[] = $file; echo "<script src='/update-resource$file'></script>\n"; } } setcookie("vg-last-access", time(), time() + 31536000, "/"); ?>
      
      







$ files-監視対象リソースの配列。 変更されたファイルごとに、 / update-resourceキーワードを使用してスクリプトタグが生成されます。これは、サービスワーカーへの要求を伴います。



そこで、これらのクエリをキーワードでフィルタリングし、リソースをリロードします。



sw.jsフェッチ
 self.addEventListener('fetch', function(event) { var url = event.request.url; if (url.indexOf("/update-resource") > 0) { var r = new Request(url.replace("\/update-resource", "")); return fetchAndCache(r); } //    ,   -  fetchAndCache() ... });
      
      







それだけです。リソースは変更されると更新されます。 ただし、弱点があります。Cookieが消える場合があり、ユーザーはすべてのファイルを再度ダウンロードする必要があります。 また、ユーザーにCookieを設定した後、何らかの理由でユーザーが更新されたすべてのファイルをダウンロードできない場合もあります。 この場合、彼は「壊れた」アプリケーションを持つことになります。 より信頼性の高いものを考えてみましょう。



オプション2



Googleのスタッフと同様に、監視対象ファイルをsw.jsに転送し、クライアント側で変更を確認します。 機能的な手段として、ハッシュバイクを考案することなく、E-TagまたはLast-Modifiedレスポンスヘッダーを取得します。これらはワーカーのキャッシュに完全に保存されます。 Eタグを取得する方がより正確ですが、サーバー側で取得するには、Webサーバーに対してローカルリクエストを行う必要があります。これは少しオーバーヘッドであり、Last-Modifiedはfilemtime()を使用して完全に計算されます。



そのため、sw.jsの代わりに、sw.phpを次のコードで登録します。



sw.php
 <?php header("Content-Type: application/javascript"); header("Cache-Control: no-store, no-cache, must-revalidate"); $files = [ "/css/fonts.css", "/css/custom.css", "/js/m-js.js", "/js/header.js"]; echo "var updated = {};\n"; forEach($files as $file) { echo "updated['$file'] = '" . gmdate("D, d MYH:i:s \G\M\T", filemtime(__DIR__ . $file)) . "';\n"; } echo "\n\n"; readfile('sw.js'); ?>
      
      







sw.jsの先頭で、監視対象リソースの{url、Last-Modified}ペアで初期化された連想配列の宣言を生成します。



 var updated = {}; updated['/css/fonts.css'] = 'Mon, 07 May 2018 02:47:54 GMT'; updated['/css/custom.css'] = 'Sat, 05 May 2018 13:10:07 GMT'; updated['/js/m-js.js'] = 'Mon, 07 May 2018 11:33:56 GMT'; updated['/js/header.js'] = 'Mon, 07 May 2018 15:34:08 GMT';
      
      





さらに、リソースのクライアントからの各リクエストで、URLが更新された配列に入る場合、キャッシュにあるものをチェックします。



sw.jsフェッチ
 self.addEventListener('fetch', function(event) { console.log('Fetching:', event.request); event.respondWith(async function() { const cachedResponse = await caches.match(event.request); if (cachedResponse) { console.log("Cached version found: " + event.request.url); var l = new URL(event.request.url); if (updated[l.pathname] === undefined || updated[l.pathname] == cachedResponse.headers.get("Last-Modified")) { console.log("Returning from cache"); return cachedResponse; } console.log("Updating to recent version"); } return await fetchAndCache(event.request); }()); });
      
      







15行のコード。ファイルを安全にサーバーにアップロードできます。ファイル自体はクライアントのキャッシュで更新されます。



残りの時間-リソースをロードした後、更新された[url.pathname]を新しいresponse.headers.get( "Last-Modified")で更新する必要があります-最後のsw.phpが受信された後にこのファイルが再び更新された可能性があり、不一致が発生します最終変更時刻のヘッダー。このファイルはリクエストに応じて常に更新されます。



結論



sw.js / sw.phpのライフサイクルを覚えておく必要があります。 このファイルは、1つの例外を除いて、標準のブラウザキャッシュのルールに従います。24時間以内にクライアント上で存続し、サービスワーカーの次の登録時に強制的に再起動されます。 sw.phpを使用すると、常に最新バージョンを使用することがほぼ保証されます。



sw.jsの生成に介入したくない場合は、 アクティブ化ブロックでサーバーからLast-Modifiedから監視対象リソースのリストをダウンロードできます。これはおそらくより正しい方法ですが、サーバーへの追加リクエストが1つ発生します。 また、オプション1のように、ページのhtmlコードにクラッシュし、そこにjsonデータを含むajaxリクエストをサービスワーカーに作成し、更新された配列を初期化することで処理できます-これはおそらく最適で動的なオプションであり、必要に応じてキャッシュリソースを更新できますService Workerを再インストールせずに。



このスキームをさらに発展させると、監視対象の各リソースが遅延をロードする宣言機能を追加しても問題ありません。まずキャッシュからクライアントに戻り、次にネットワークからダウンロードして次のインプレッションを取得します。



アプリケーションの別の例は、異なるサイズの画像です(srcsetまたはソフトウェアのインストール)。 そのようなリソースをロードする場合、最初にキャッシュ内のより高い解像度のイメージをキャッシュで検索して、サーバー要求を保存できます。 または、メイン画像をロードするときに、より小さい画像を使用します。



一般的なキャッシュテクニックのうち、時期尚早の読み込みも興味深いものです。アプリケーションの次のリリースで、新しいリソースや重い画像などの追加リソースが表示されることがわかっているとします。 loadイベントによって事前にキャッシュに読み込むことができます-ユーザーがページ全体を開き、読み始めたとき。 それは知覚不可能で効果的です。



最後に、いくつかのキャッシュ(phpスクリプトによって生成された画像のキャッシュを含む)でsw.js(上記のsw.phpと連携して動作する)の例と、2番目のオプションに従って透過的なキャッシュ更新を実装しました。



sw.js
 // Caches var CURRENT_CACHES = { font: 'font-cache-v1', css:'css-cache-v1', js:'js-cache-v1', icons: 'icons-cache-v1', icons_ext: 'icons_ext-cache-v1', image: 'image-cache-v1' }; self.addEventListener('install', (event) => { self.skipWaiting(); console.log('Service Worker has been installed'); }); self.addEventListener('activate', (event) => { var expectedCacheNames = Object.keys(CURRENT_CACHES).map(function(key) { return CURRENT_CACHES[key]; }); // Delete out of date cahes event.waitUntil( caches.keys().then(function(cacheNames) { return Promise.all( cacheNames.map(function(cacheName) { if (expectedCacheNames.indexOf(cacheName) == -1) { console.log('Deleting out of date cache:', cacheName); return caches.delete(cacheName); } }) ); }) ); console.log('Service Worker has been activated'); }); self.addEventListener('fetch', function(event) { console.log('Fetching:', event.request.url); event.respondWith(async function() { const cachedResponse = await caches.match(event.request); if (cachedResponse) { // console.log("Cached version found: " + event.request.url); var l = new URL(event.request.url); if (updated[l.pathname] === undefined || updated[l.pathname] == cachedResponse.headers.get("Last-Modified")) { // console.log("Returning from cache"); return cachedResponse; } console.log("Updating to recent version"); } return await fetchAndCache(event.request); }()); }); function fetchAndCache(url) { return fetch(url) .then(function(response) { // Check if we received a valid response if (!response.ok) { return response; // throw Error(response.statusText); } // console.log(' Response for %s from network is: %O', url.url, response); if (response.status < 400 && response.type === 'basic' && response.headers.has('content-type')) { // debugger; var cur_cache; if (response.headers.get('content-type').indexOf("application/javascript") >= 0) { cur_cache = CURRENT_CACHES.js; } else if (response.headers.get('content-type').indexOf("text/css") >= 0) { cur_cache = CURRENT_CACHES.css; } else if (response.headers.get('content-type').indexOf("font") >= 0) { cur_cache = CURRENT_CACHES.font; } else if (url.url.indexOf('/css/icons/') >= 0) { cur_cache = CURRENT_CACHES.icons; } else if (url.url.indexOf('/misc/image.php?') >= 0) { cur_cache = CURRENT_CACHES.image; } if (cur_cache) { console.log(' Caching the response to', url); return caches.open(cur_cache).then(function(cache) { cache.put(url, response.clone()); updated[(new URL(url.url)).pathname] = response.headers.get("Last-Modified"); return response; }); } } return response; }) .catch(function(error) { console.log('Request failed:', error); throw error; // You could return a custom offline 404 page here }); }
      
      








All Articles