Service Worker とは
Service Worker とは、Webページとは別にバックグラウンド(別スレッド)で動作するJavascript環境のことです。Javascriptは言語仕様として単一スレッドでしか動作することができませんが、Service Worker を使うことでこれを実現できます。
Service Worker を使用することでプッシュ通知やバックグラウンドでのデータ同期が可能になり、Webアプリのオフライン環境での動作をサポートすることができます。
Javascript の並行処理と非同期処理
Javascript では setTimeout
や Promise
を使って非同期処理を実現できます。これら非同期処理を組み合わせることで疑似的に並行処理を実現することもできますが、これらはすべて単一のスレッドで実行されています。
完全な別スレッドでの並行処理を実現するには Service Worker を使う必要が出てきます。
Service Worker でできることできないこと
できること
- ページからのリクエストをプロキシ
- ページとの間でイベントハンドラを使ってのメッセージ送受信
- IndexedDBへのアクセス: 状態を保持したい場合はIndexedDBを使えます。
ページからのリクエストをプロキシすると具体的に何ができるのかというと、オフライン時の動作をサポートできます。
たとえばオンライン時にレスポンスをキャッシュしておいて、オフライン時にリクエストがあった場合はキャッシュを返すことが可能です。こうすることでオフライン時でもオンライン時と変わらぬ動作を実現できます。
できないこと
- DOMへのアクセス: 別スレッドなので直接DOMにアクセスはできません。
- 状態の保持: Service Worker はブラウザによって不要になったら破棄されます。したがって状態は保持できません。
- HTTP通信での利用 – リクエストを操作できる強力な機能をもつので、セキュアなHTTPS通信でのみ利用可能です。
Service Worker のライフサイクル
Service Worker には Webページと異なるライフサイクルがあります。Service Worker を使用するには Webページに Service Woker を登録する必要があります。登録後、ブラウザに Service Worker がバックグラウンドでインストールされ、有効化(activate)されます。
有効になった Service Worker の状態は、メモリ節約のために終了されているか、ページで起こったネットワークリクエストに対して fetch もしくは message イベントの処理をしているかのいずれかとなります。
図にすると以下の通りです。(引用)
Service Worker の実行環境
ブラウザサポート
Can I use… Support tables for HTML5, CSS3, etc
Service Worker は Edge, Chrome, Safari(iOS含む), Firefox, Opera で利用できます。
IE11には対応していませんのでその点はご注意ください。
HTTPS通信が必須
Service Worker を使うには HTTPS通信が基本的には必須となります。Service Worker はリクエストをプロキシするという強力な機能を持つため、接続のフィルタリングや改竄も可能になります。もし Service Worker のスクリプトがネットワークの途中で改竄されてしまうと大変なリスクとなります。したがって Service Worker を実行するには HTTPS の環境が必要になります。
ただし例外的に localhost の場合は動作します。開発中は localhost で試すとよいでしょう。
Service Worker を使ってみる
上述の通り、Service Worker には以下のライフサイクルの流れがあります。
- 登録
- インストール
- 有効化
- 処理
- アンインストール
順に実装で見ていきます。
Service Worker の登録
まずは以下のような内容でサンプル用のフォルダを作成します。必要なファイルは4つです。
このフォルダに http://localhost ...
でアクセスできるようにローカルサーバーに設定してください。
ファイルはそれぞれ次の通りとします。my-service-worker.js ファイルはひとまず空のままでよいです。
index.html
<!DOCTYPE html>
<html lang="ja">
<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">
<script src="index.js"></script>
<title>Service Worker Test</title>
</head>
<body>
<h1>Service Worker Test</h1>
<img src="/my.png">
</body>
</html>
index.js
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/my-service-worker.js', { scope: '/' })
.then(function(reg) {
console.log('登録に成功しました。 Scope は ' + reg.scope);
}).catch(function(error) {
console.log('登録に失敗しました。' + error);
});
}
my.png は、適当な画像ファイルを用意してください。表示されればなんでもOKです。
navigator.serviceWorker.register
navigator.serviceWorker.register()
で Service Worker を登録します。すでに登録済の場合でも問題ありません。scope
で指定できるのは登録した Service Worker がアクセスできる範囲のスコープです。基本的には同じ階層を指定しておくのがいいでしょう。
このメソッドはPromiseで登録成功の可否を返します。
登録に成功すると次のような内容がコンソールに出力されます。
登録に成功しました。 Scope は http://localhost:8765/
window で htmlファイルを直接 file:///C:/.../index.html
で開くと次のようなエラーになりました。IIS なり Apache なりにデプロイしないといけません。
登録に失敗しました。SecurityError: Failed to register a ServiceWorker: The URL protocol of the current origin ('null') is not supported.
スコープについて
スコープは Service Worker が fetch イベントを発火する際のページのルートを指定します。
navigator.serviceWorker.register('/my-service-worker.js', { scope: '/' })
この例だと、スコープはルート以下すべてのページからのリクエストを fetch します。
例えば “/scope/” というパスをスコープにすると、”/scope/xxx.html” というページようなページからしか fetch されません。スコープはfetchするパスを絞るのではなく、fetchするリクエストを送るページパスを絞るという点に注意しておきましょう。
なおデフォルトでスコープは、register()
を呼び出したファイルと同階層以下となります。つまり上の例だと設定しなくてもデフォルトで “/” がスコープになります。
Service Worker を使用するのはシングルページアプリケーションが多いと思いますが、基本は index.html と同じ位置をスコープに設定しておけばよいと思います。
Service Worker のインストールとアクティベート
Service Worker のインストール時に処理を行う場合、Service Worker のファイルに以下のように記述します。
my-service-worker.js
// Service Worker インストール時に実行される
self.addEventListener('install', (event) => {
console.log('service worker install ...');
});
// Service Worker アクティベート時に実行される
self.addEventListener('activate', (event) => {
console.info('activate', event);
});
これで Service Worker インストール時にコンソールにログが出力されます。なお、インストールは登録後自動で行われるので、ページをリフレッシュしてもこのログが出ない場合があります。
てっとり早いのはブラウザのシークレットモードなりで起動してみることです。またブラウザが管理している Service Worker を確認し、そこから強制的に登録を解除させることもできます。
ChromeだとF12で開発者ツールを開き、Applicationタブで Service Workers の項目を開けば現在のドメインでの Service Worker が確認できます。
ここから Unregister をクリックすれば登録を解除できます。
アクティベートは登録後に実行されます。この後のスコープ内からのリクエストが fetch されるようになります。
Service Worker の fetch
fetch イベントでは、ページのリクエストをトリガーに発火します。以下のコードでリクエストをコンソールに出力します。
my-service-worker.js
self.addEventListener('fetch', (event) => {
console.log('service worker fetch ... ' + event.request);
});
Service Worker がインストールされたページで、F5を押してリロードしてみてください。
Service Worker が更新されているため、インストールが再度実行されます。ただしこの状態だと、アクティベートされません。級に切り替えてデータの不整合が発生するかもしれないので、ページが閉じられるまで更新が行われないようになっています。
一度閉じて、再度開いて確認してみてください。もしくは、Chromeだと開発者ツールから skipWaiting
でアクティベートの待機状態をスキップできます。
activate 直後は fetch しない
activate された Service Worker は、次のページ表示時から fetch を始めます。activate のログが確認できたら、ページを更新してみましょう。
fetch http://localhost:8765/index.js
fetch http://localhost:8765/my.png
上記のようなログが確認できます。スコープ内のページからのリクエストなので fetch しています。例えば、この fetch であらかじめキャッシュしておいたファイルがあれば、それを返し、なければリクエストを投げるなどが可能になります。
以下代表的な使い方であるファイルのキャッシュしてオフライン環境下での動作を実装してみます。
Service Worker でファイルをキャッシュするサンプル
index.html と index.js は変わらずです。
index.html
<!DOCTYPE html>
<html lang="ja">
<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">
<script src="index.js"></script>
<title>Service Worker Test</title>
</head>
<body>
<h1>Service Worker Test</h1>
<img src="/my.png">
</body>
</html>
index.js
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/my-service-worker.js', { scope: '/' })
.then(function(reg) {
console.log('登録に成功しました。 Scope は ' + reg.scope);
}).catch(function(error) {
console.log('登録に失敗しました。' + error);
});
}
Service Worker の処理は以下のようになります。
my-service-worker.js
const version = 'v1';
// インストール時にキャッシュする
self.addEventListener('install', (event) => {
console.log('service worker install ...');
// キャッシュ完了までインストールが終わらないように待つ
event.waitUntil(
caches.open('v1').then((cache) => {
return cache.addAll([
'/index.html',
'/index.js',
'/my.png',
]);
})
);
});
self.addEventListener('activate', (event) => {
console.info('activate', event);
});
self.addEventListener('fetch', function(event) {
console.log('fetch', event.request.url);
event.respondWith(
// リクエストに一致するデータがキャッシュにあるかどうか
caches.match(event.request).then(function(cacheResponse) {
// キャッシュがあればそれを返す、なければリクエストを投げる
return cacheResponse || fetch(event.request).then(function(response) {
return caches.open('v1').then(function(cache) {
// レスポンスをクローンしてキャッシュに入れる
cache.put(event.request, response.clone());
// オリジナルのレスポンスはそのまま返す
return response;
});
});
})
);
});
基本はコメントにある通りです。インストール時にキャッシュストレージを開き、対象となるパスのデータをこの時点で全て取得しています。もちろん非同期処理ですが、このデータ取得が完了するまでインストールが完了しないように event.waitUtil
を使っています。
fetch 時には、キャッシュストレージに一致するデータがあるかを確認します。これはリクエストのパスをキーにしています。キャッシュがあればそれを返し、なければリクエストを投げるようにしています。
リクエストを投げて取得したデータは、キャッシュに入れるようにすることで、次のリクエスト時にはキャッシュが参照できるようになります。
動作の流れ
- Service Worker の登録インストール、アクティベート
まず、Service Worker の登録が呼び出され、その後インストールが実行されます。それからアクティベートされます。ここまでが初回表示時の流れです。
コンソールのログを見ると、これらのログが確認できます。
インストールが完了しているということは既にキャッシュストレージにデータが取得されています。Chromeの開発者ツールから確認してみましょう。Applicationタブの Cache Storage を確認すると、次のようになっています。
ちゃんと指定して3ファイルがストレージに保存されているのがわかります。ファイルを選択すると、その中身も確認できます。
- 再表示時に Service Worker のキャッシュが参照される
F5で更新して再表示してみましょう。すると画面は変わりませんがコンソールを見ると、my.png と index.js の fetch ログが確認できると思います。
例えば、この時本当に表示されている画像ファイル(my.png)がキャッシュを参照しているのでしょうか。これを確認するには同じく開発者ツールの Network タブを使います。
このページでリクエストされたファイルが一覧になっていますが、Size を見ると、(from ServiceWorker) となっており、Service Worker 経由で参照できていることがわかります。
処理時間も短くなっているのがわかります。
- オフライン時の挙動
最後にオフライン時の挙動も確認しましょう。キャシュに保存されている状態だと、オフライン時にもキャッシュを参照することでオンライン時と同様に動作させることが可能です。
オフライン時の挙動を確認するには、Chrome の開発者ツールで Application タブの Service Worker で Offline にチェックを入れます。こうすることでオフライン時の挙動をブラウザでエミュレートしてくれます。
**ここに画像06
3ファイルが正しくキャッシュされているとオフラインでも同様の画面を確認できます。
まとめ
- register() で登録
- fetch でリクエストをプロキシできる
- cacheしておけばオフラインでも動作可能
ほかは記事に書いたとおり。その他にも message のイベントハンドラでページとのやり取りを行えます。が、長くなるので他の記事に譲ります。
以上。
コメントを書く