ServiceWorker について(1)
目的
LWC などのフロントエンド技術は、極論調べてフレームワークに従っていれば処理できます。
使うだけならその内部を把握する必要はありませんが、どんな機能をどのように使って処理するのかを把握しておけば、応用が効くかも知れません。
ここでは Service Worker の機能について把握していきます。
代表的な使い方はまずはやはりオフラインキャッシュでしょう。
オフラインキャッシュはネットワークが不安定なモバイル等で特に活躍するもので、接続が途切れようと正しく内容を表示し続けることができます。
試してみる
こちらにサンプルを見つけましたので、これを実行していきます。
nodejs でも python でも何でもいいので、clone
してきてサーバを立ち上げるだけですね。
オーソドックスに npx lite-server
で実行します。
するとブラウザが起動して、ダースベイダーさんが見えます。
ではここでサーバを Ctrl + c
で停止してみます。
ブラウザに戻って更新ボタンを押しても…正しく内容が表示されます。
コードを読んでいこう
では一緒にコードを読んでいきましょう。
index.html
はエントリポイントですね。
やっていることは app.js
を読み込んでるだけという。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width" /> <title>Service worker demo</title> <link rel="stylesheet" href="style.css" /> </head> <body> <h1>Lego Star Wars gallery</h1> <section></section> <script type="module" src="app.js"></script> </body> </html>
では app.js
の中身を見ていきます。
import { Gallery } from './image-list.js'; const registerServiceWorker = async () => { if ('serviceWorker' in navigator) { try { const registration = await navigator.serviceWorker.register( 'sw.js', { scope: './', } ); if (registration.installing) { console.log('Service worker installing'); } else if (registration.waiting) { console.log('Service worker installed'); } else if (registration.active) { console.log('Service worker active'); } } catch (error) { console.error(`Registration failed with ${error}`); } } }; const imgSection = document.querySelector('section'); const getImageBlob = async (url) => { const imageResponse = await fetch(url); if (!imageResponse.ok) { throw new Error( `Image didn't load successfully; error code: ${ imageResponse.statusText || imageResponse.status }` ); } return imageResponse.blob(); }; const createGalleryFigure = async (galleryImage) => { try { const imageBlob = await getImageBlob(galleryImage.url); const myImage = document.createElement('img'); const myCaption = document.createElement('caption'); const myFigure = document.createElement('figure'); myCaption.textContent = `${galleryImage.name}: Taken by ${galleryImage.credit}`; myImage.src = window.URL.createObjectURL(imageBlob); myImage.setAttribute('alt', galleryImage.alt); myFigure.append(myImage, myCaption); imgSection.append(myFigure); } catch (error) { console.error(error); } }; registerServiceWorker(); Gallery.images.map(createGalleryFigure);
上から順に見ていきます。
import { Gallery } from './image-list.js';
画像の定義を読み込んでいます。
中身は下記ですが、特筆するものはなさそうだ。
export const Path = 'gallery/'; export const Gallery = { images: [ { name: 'Darth Vader', alt: 'A Black Clad warrior lego toy', url: 'gallery/myLittleVader.jpg', credit: '<a href="https://www.flickr.com/photos/legofenris/">legOfenris</a>, published under a <a href="https://creativecommons.org/licenses/by-nc-nd/2.0/">Attribution-NonCommercial-NoDerivs 2.0 Generic</a> license.', }, { name: 'Snow Troopers', alt: 'Two lego solders in white outfits walking across an icy plain', url: 'gallery/snowTroopers.jpg', credit: '<a href="https://www.flickr.com/photos/legofenris/">legOfenris</a>, published under a <a href="https://creativecommons.org/licenses/by-nc-nd/2.0/">Attribution-NonCommercial-NoDerivs 2.0 Generic</a> license.', }, { name: 'Bounty Hunters', alt: 'A group of bounty hunters meeting, aliens and humans in costumes.', url: 'gallery/bountyHunters.jpg', credit: '<a href="https://www.flickr.com/photos/legofenris/">legOfenris</a>, published under a <a href="https://creativecommons.org/licenses/by-nc-nd/2.0/">Attribution-NonCommercial-NoDerivs 2.0 Generic</a> license.', }, ], };
特に言うこと無いJavascriptでデータが入っているだけですね。
const registerServiceWorker = async () => { if ('serviceWorker' in navigator) { try { const registration = await navigator.serviceWorker.register( 'sw.js', { scope: './', } ); if (registration.installing) { console.log('Service worker installing'); } else if (registration.waiting) { console.log('Service worker installed'); } else if (registration.active) { console.log('Service worker active'); } } catch (error) { console.error(`Registration failed with ${error}`); } } }; // 中略 registerServiceWorker();
何をやっているかといえば、sw.js
ファイルを読み込んで、ServiceWorker
に登録しているだけですね。
残りの部分を先に見てみると
const imgSection = document.querySelector('section'); const getImageBlob = async (url) => { const imageResponse = await fetch(url); if (!imageResponse.ok) { throw new Error( `Image didn't load successfully; error code: ${ imageResponse.statusText || imageResponse.status }` ); } return imageResponse.blob(); }; const createGalleryFigure = async (galleryImage) => { try { const imageBlob = await getImageBlob(galleryImage.url); const myImage = document.createElement('img'); const myCaption = document.createElement('caption'); const myFigure = document.createElement('figure'); myCaption.textContent = `${galleryImage.name}: Taken by ${galleryImage.credit}`; myImage.src = window.URL.createObjectURL(imageBlob); myImage.setAttribute('alt', galleryImage.alt); myFigure.append(myImage, myCaption); imgSection.append(myFigure); } catch (error) { console.error(error); } }; // 中略 Gallery.images.map(createGalleryFigure);
image-list.js
の定義をループして createGalleryFigure
でHTMLのDOM(≒タグ)に変換して表示。
getImageBlob
は fetch
コマンドを使って値を読み込んでいるだけですね。
fetch
は ここ を見ると、サービスワーカーに fetch
イベントを飛ばして応答を拾う処理のようです。
肝はサービスワーカー本体
ではここで sw.js
(サービスワーカーの本体) を読んでみます。
const addResourcesToCache = async (resources) => { const cache = await caches.open('v1'); await cache.addAll(resources); }; const putInCache = async (request, response) => { const cache = await caches.open('v1'); await cache.put(request, response); }; const cacheFirst = async ({ request, preloadResponsePromise, fallbackUrl }) => { // First try to get the resource from the cache const responseFromCache = await caches.match(request); if (responseFromCache) { return responseFromCache; } // Next try to use the preloaded response, if it's there const preloadResponse = await preloadResponsePromise; if (preloadResponse) { console.info('using preload response', preloadResponse); putInCache(request, preloadResponse.clone()); return preloadResponse; } // Next try to get the resource from the network try { const responseFromNetwork = await fetch(request); // response may be used only once // we need to save clone to put one copy in cache // and serve second one putInCache(request, responseFromNetwork.clone()); return responseFromNetwork; } catch (error) { const fallbackResponse = await caches.match(fallbackUrl); if (fallbackResponse) { return fallbackResponse; } // when even the fallback response is not available, // there is nothing we can do, but we must always // return a Response object return new Response('Network error happened', { status: 408, headers: { 'Content-Type': 'text/plain' }, }); } }; const enableNavigationPreload = async () => { if (self.registration.navigationPreload) { // Enable navigation preloads! await self.registration.navigationPreload.enable(); } }; self.addEventListener('activate', (event) => { event.waitUntil(enableNavigationPreload()); }); self.addEventListener('install', (event) => { event.waitUntil( addResourcesToCache([ './', './index.html', './style.css', './app.js', './image-list.js', './star-wars-logo.jpg', './gallery/bountyHunters.jpg', './gallery/myLittleVader.jpg', './gallery/snowTroopers.jpg', ]) ); }); self.addEventListener('fetch', (event) => { event.respondWith( cacheFirst({ request: event.request, preloadResponsePromise: event.preloadResponse, fallbackUrl: './gallery/myLittleVader.jpg', }) ); });
100 行行かないのだからコレは読んで行きましょう…。
まず注目するのは最後のほう
const enableNavigationPreload = async () => { if (self.registration.navigationPreload) { // Enable navigation preloads! await self.registration.navigationPreload.enable(); } }; self.addEventListener('activate', (event) => { event.waitUntil(enableNavigationPreload()); }); self.addEventListener('install', (event) => { event.waitUntil( addResourcesToCache([ './', './index.html', './style.css', './app.js', './image-list.js', './star-wars-logo.jpg', './gallery/bountyHunters.jpg', './gallery/myLittleVader.jpg', './gallery/snowTroopers.jpg', ]) ); }); self.addEventListener('fetch', (event) => { event.respondWith( cacheFirst({ request: event.request, preloadResponsePromise: event.preloadResponse, fallbackUrl: './gallery/myLittleVader.jpg', }) ); });
文脈を考えるなら、self
はサービスワーカーのインスタンスです。
fetch
はapp.js
で見たfetch
コマンドの実態。install
イベントはサービスワーカーのインストール完了イベントだそうです。メソッド名からaddResourcesToCache
はリソースの読み込みとキャッシュを行うようですね。activate
はサービスワーカーが有効化された際に実行されるイベントです。enableNavigationPreload
は「サービスワーカーのナビゲーション先読み」だそうです。
有効にすると、ナビゲーション先読み機能は、フェッチ要求がなされるとすぐに、サービスワーカーの起動と並行してリソースのダウンロードを開始します。 これにより、サービスワーカーが起動するまで待つ必要がなく、ページへのナビゲーションですぐにダウンロードが開始されるようになります。 この遅延は比較的稀にしか発生しませんが、発生した場合は避けられないものであり、重大なものになる可能性があります。
つまり、fetch
を開始した瞬間、fetch
のイベント処理だけでなく、リソースの読み込みを並行して行うって事ですね。
では改めて残りを読み込んでみましょう。
初期化&キャッシュ処理
const addResourcesToCache = async (resources) => { const cache = await caches.open('v1'); await cache.addAll(resources); }; // 中略 self.addEventListener('install', (event) => { event.waitUntil( addResourcesToCache([ './', './index.html', './style.css', './app.js', './image-list.js', './star-wars-logo.jpg', './gallery/bountyHunters.jpg', './gallery/myLittleVader.jpg', './gallery/snowTroopers.jpg', ]) ); });
caches
は MDN 曰く CacheStrage の事とのこと。
要するにキャッシュを蓄える入れ物ですね。
const cache = await caches.open('v1');
のコマンドでv1
と言うラベルのCache
オブジェクトを取得。Cache オブジェクトの仕様はこちらawait cache.addAll(resources);
でv1
キャッシュに読み込んでキャッシュしたいリソースを列挙しておきます。
サービスワーカーが起動したら、先読み機能を有効化
const enableNavigationPreload = async () => { if (self.registration.navigationPreload) { // Enable navigation preloads! await self.registration.navigationPreload.enable(); } }; self.addEventListener('activate', (event) => { event.waitUntil(enableNavigationPreload()); });
先程ちょっと説明した機能ですね。
fetch
でリソースを要求した瞬間に、キャッシュ処理は行いますが、キャッシュがうまく拾えなかった場合に備えて並行してリソースの再取得をしています。
fetch
されたらキャッシュを返す。なかったら get してくる
ちょっと長いのでコメントを書き換える形で説明。
const putInCache = async (request, response) => { const cache = await caches.open('v1'); await cache.put(request, response); }; const cacheFirst = async ({ request, preloadResponsePromise, fallbackUrl }) => { // キャッシュの中に、要求したリソースがないか検索。 // あればそれを応答 const responseFromCache = await caches.match(request); if (responseFromCache) { return responseFromCache; } // キャッシュがなかったので、平行して読み込んでいたリソースを取得 // 取得できたら `putInCache` でキャッシュしつつ、取得したリソースを応答 const preloadResponse = await preloadResponsePromise; if (preloadResponse) { console.info('using preload response', preloadResponse); putInCache(request, preloadResponse.clone()); return preloadResponse; } // 何かの理由で取得が失敗していた場合の再取得 try { // fetch をして取れれば応答+キャッシュ const responseFromNetwork = await fetch(request); putInCache(request, responseFromNetwork.clone()); return responseFromNetwork; } catch (error) { // fetch エラーがあった。 // エラー応答のリソースを返すが、無ければ 408 応答。 const fallbackResponse = await caches.match(fallbackUrl); if (fallbackResponse) { return fallbackResponse; } return new Response('Network error happened', { status: 408, headers: { 'Content-Type': 'text/plain' }, }); } }; // 中略 self.addEventListener('fetch', (event) => { event.respondWith( cacheFirst({ request: event.request, preloadResponsePromise: event.preloadResponse, fallbackUrl: './gallery/myLittleVader.jpg', }) ); });
まとめ
キャッシュを行うサービスワーカーのコードリーディングを通して、サービスワーカーの使い方の一つを学んでいきました。
モバイルだと移動しながらで通信が不安定になることは多いですから、特にアプリケーションの様な多くのリソースを使うものではこの恩恵は大きいと思われる。