メッセージングによるService Workerのコントロール
Published at 2016-05-13
Service Workerでハンドルするリソースは、Service Workerのスクリプトに静的に記述しているケースが多い。Service Workerでやっているアレコレをメッセージングで動的にできないか試行錯誤したログ。
よくある?キャッシュパターン
チュートリアル等でもよく見かける、Service Workerのインストール時に指定のリソースをキャッシュしているパターン。
const CACHE_KEY = 'cache-key';
const CACHE_LIST = [
'index.html',
'app.js',
'app.css'
];
self.addEventListener('install', e => {
// CACHE_KEYをキーにとるCacheオブジェクトを開いて
// CACHE_LISTをキャッシュするPromise
let promise = caches.open(CACHE_KEY)
.then(cache => cache.addAll(CACHE_LIST))
.catch(error => console.log(error));
e.waitUntil(promise);
});
小さい用途であれば、これで何ら問題はない気はする。開発規模が大きくなってくるとリソースの増加やらで人力でメンテナンスするのが辛くなってくる。
GoogleChrome/sw-precacheはService Workerのスクリプトを書き出すツールで、キャッシュしたいパスのパターン指定などが可能。これでひとまず人力で管理していくリスクは低減できるが、細かい処理を書くには不向き。なので、Service Workerの処理内容を自由に書く余地を残しつつ、何をキャッシュするかを動的にできないかを模索したところ、手段の1つとしてメッセージングを使う方法が浮かんだ。
fetch
イベントでリクエスト内容を見て動的に判断するなどはできるが、後述の Service Workerが最長24時間更新されない問題 などもあるので、コントロール手段のひとつとして覚えておくのは良さ気。
メッセージングでService Workerのコントロール
Service WorkerはJavaScript Workerのひとつ。なので、ブラウザスレッドとService Workerとでメッセージのやり取りが可能。Cache APIもPromiseな設計なので、e.waitUntil()
もあることだし処理はPromiseで書くと良さ気。
// browser.js
function sendMessage(message) {
return new Promise((resolve, reject) => {
const channel = new MessageChannel();
channel.port1.onmessage = e => {
if (e.data.error) {
reject(e.data.error);
} else {
resolve(e.data);
}
};
navigator.serviceWorker.controller.postMessage(message, [channel.port2]);
});
}
sendMessage('A message to Service Worker!');
// service-worker.js
self.addEventListener('message', e => {
let promise = Promise.resolve()
.then(() => {
console.log(e.data);
// e.dataにメッセージの内容が入ってる
})
.catch(error => {
console.error(error);
});
e.waitUntil(promise);
});
例えば、ブラウザから送るメッセージを{ command : 'add', url : 'app.css' }
なオブジェクトにすれば、e.data.command
でswitch
するなどして、メッセージの内容に応じた処理も可能。実際にこのメッセージを元にapp.css
をキャッシュさせる実装をすると次のようになる。
// browser.js
sendMessage({
command : 'add',
url : 'app.css'
});
// service-worker.js
self.addEventListener('message', e => {
let promise = caches.open('cache-key')
.then(() => {
let command = e.data.command; // add
let url = e.data.url; // app.css
switch (command) {
case 'add':
// app.css のリクエストオブジェクト
let request = new Request(url);
// fetch() してレスポンスをキャッシュに格納する
return fetch(request)
.then(response => cache.put(url, response));
default:
return Promise.resolve();
}
})
.catch(error => {
console.error(error);
});
e.waitUntil(promise);
});
こんな感じで、キャッシュのリソース追加だけでなくリソース削除やキャッシュそのものの削除まで出来ることを確認した。
使いどころ
メッセージングでリソースの追加・削除をする不都合は、 何をキャッシュさせているか を管理しにくくなるところか。キャッシュにヒットするかどうかだけならfetch
イベント内でハンドルすれば十分だが、Service Workerはブラウザが必要に応じて起動・終了するので、メッセージの内容を変数に格納して保持しておくことは出来ない。やるならIndexedDBを使うことになりそうだが、キャッシュリストの管理のために、Cache APIとIndexedDBを併用するのも微妙に思う。
Service Workerで唯一使いにくい可能性があるのは、Service Worker自体の更新チェックが最大24時間になってしまう点。Service Worker自体に更新があるかどうかはServiceWorkerRegistration#update()
を介してチェックが可能だが、Cache-Controlで長生き設定されていると、たとえページロードのタイミングで毎回呼んだとしてもブラウザキャッシュが効いてしまう(長生き設定しなければ良いという話でもあるんだけど)。
その点、メッセージ経由でキャッシュの全クリアとかもできるので、purge
コマンドを用意して失敗ビルドを配信してキャッシュされてしまったような状況に備えるのは悪くないかもしれない。パージの他にも、キャッシュ追加・キャッシュ削除・キャッシュリストの取得といった処理のミニマム実装は1000ch/sw-sandboxにあげた。
良い使い方が浮かんだら書き足す。