(※この記事は 別媒体に投稿した記事 のバックアップです。 canonical も設定しています)
2020-10-09
※この記事ははてなブログ、別アカウント(hyiromori)から引っ越しました
※2018年7月頃に書いていたものを発掘して再掲したものなので、情報が古い可能性があります。
https
or localhost
でしか動作しないfetch
イベントに介入(キャッシュなどのコントロールが出来る)ネイティブアプリでないと難しかったことが、Webアプリでも出来るようになってきた印象ですね!
まだ、機能によっては対応しているブラウザが少ないですが、この流れが進むと、Webアプリでもネイティブアプリとほぼ同等のことが出来るようになってくると思います。
window
にはアクセスできませんself
が Service Worker 自身を指すようです。fetch
イベントなどへの介入が出来るようになるINSTALL
時に呼び出されるACTIVATED
時に呼び出されるCache API
を使用して、コントロールすることが出来るようになる。TypeScript
チャレンジ中!(要するに、あまり慣れていないです😓)
index.html
とかの記述例<script type="text/typescript">
if (navigator.serviceWorker) {
navigator.serviceWorker
.register('/service_worker.js')
.then((registration) => { // 登録成功
console.log('scope:', registration.scope);
})
.catch((error) => { // 登録失敗
console.log('failed: ', error);
});
}
</script>
/service_worker.js
の記述例(抜粋)self.addEventListener('install', (event: any) => {
event.waitUntil(
// インストール処理後に実行したい処理
);
});
self.addEventListener('fetch', (event: any) => {
// fetch イベント時に介入したい処理
});
1度 fetch
に成功したものはキャッシュし、2度目以降はキャッシュを返す例
self.addEventListener('fetch', (event: any) => {
event.respondWith(
async () => {
// キャッシュがあった場合は、キャッシュの内容を返す。
const cacheResponse = await caches.match(event.request);
if (cacheResponse) {
return cacheResponse;
}
// request を複製する(ストリームは再利用できないので)
const fetchRequest = event.request.clone();
const fetchResponse = await fetch(fetchRequest);
// レスポンスが正しくない場合はそのまま返却
if (!fetchResponse || fetchResponse.status !== 200 || fetchResponse.type !== 'basic') {
return fetchResponse;
}
// response を複製する(こちらも同じくストリームは再利用できないので)
const responseToCache = fetchResponse.clone();
const cache = await caches.open(CacheName);
// cache に登録する
cache.put(event.request, responseToCache);
return fetchResponse;
}
);
});
The offline cookbook というサイトに様々なパターンの例があり、とても参考になる。
const registerPushNotification = async () => {
if (navigator.serviceWorker && window.PushManager) {
const swRegistration = await navigator.serviceWorker.register('/service_worker.js');
console.log('ServiceWorker is registered', swRegistration);
// サーバから渡された公開鍵をバイト配列に変換します
const applicationServerKey = urlB64ToUint8Array(publicKey);
await swRegistration.pushManager.getSubscription();
const params = { userVisibleOnly: true, applicationServerKey };
// この段階でブラウザからプッシュ通知の許可ウィンドウが表示されます。
const subscription = swRegistration.pushManager.subscribe(params);
// プッシュ通知に必要な情報が subscription に入っています。(不許可の場合は何も入っていません)
console.log('User is subscribed:', subscription);
}
};
self.addEventListener('push', (event: any) => {
const dataText = event.data.text();
const title = 'Push Test';
const options = { body: dataText };
event.waitUntil(self.registration.showNotification(title, options));
});
IndexedDB
と使って渡す必要がある(結構面倒だった)LocalStorage
は使用できない(MDN - サービスワーカーの使用 にも「メモ: localStorageはサービスワーカーキャッシュと同じように動作しますが、同期処理のため、サービスワーカー内では許可されていません。」と記述があった)結構ソースが大きくなってしまった。
とりあえず、こんな感じで IndexedDB
の登録&取得処理を書いているんだな、という雰囲気だけ感じ取ればよいかと思います。
import Dexie from 'dexie';
const DB_VERSION: number = 1;
const now = (): number => (new Date()).getTime();
interface BackgroundSyncRow {
id?: number,
path: string,
body: string,
result: string,
createdAt?: number,
}
class BackgroundSyncDatabase extends Dexie {
public backgroundSync!: Dexie.Table<BackgroundSyncRow, number>;
public constructor() {
super('BackgroundSyncDatabase');
this.version(DB_VERSION)
.stores({ backgroundSync: '++id,path,body,result,createdAt' });
}
}
const db = new BackgroundSyncDatabase();
const addBackgroundSyncRow = (row: BackgroundSyncRow): Promise<number> => db
.transaction('rw', db.backgroundSync, async () => {
const createdAt: number = now();
const _row: BackgroundSyncRow = { ...row, createdAt };
return await db.backgroundSync.add(_row);
});
const updateBackgroundSyncRow = (
id: number,
row: BackgroundSyncRow,
): Promise<void> => db
.transaction('rw', db.backgroundSync, async () => {
await db.backgroundSync.update(id, row);
});
const getBackgroundSyncRow = (id: number): Promise<BackgroundSyncRow | null> => db
.transaction('r', db.backgroundSync, async () => {
const results = await db.backgroundSync.where({ id });
const count: number = await results.count();
return count === 1 ? results.first() : null;
});
const getBackgroundSyncRows = (limit: number = 30): Promise<Array<BackgroundSyncRow>> => db
.transaction('r', db.backgroundSync, async () => {
return await db.backgroundSync
.orderBy('createdAt')
.reverse()
.limit(limit)
.toArray();
});
export {
addBackgroundSyncRow,
getBackgroundSyncRow,
getBackgroundSyncRows,
updateBackgroundSyncRow,
};
const backgroundSyncTest = async () => {
const syncData = {
path: '/api/v1/echo/test',
body: JSON.stringify({ test: 'OK' }),
result: '',
};
const id: number = await addBackgroundSyncRow(syncData);
const tag: string = `background-sync:${id}`;
const swRegistration = await navigator.serviceWorker.ready;
swRegistration.sync.register(tag);
};
self.addEventListener('sync', async (event: any) => {
if (event != null && typeof event.tag === 'string') {
if (event.tag.match(/^background-sync:\d+$/)) {
const id: number = parseInt(event.tag.substr(16), 10);
const syncData = await getBackgroundSyncRow(id);
const { path, body, result } = syncData;
if (result === '') {
const response = await fetch(path, { method: 'POST', body });
syncData.result = await response.text();
await updateBackgroundSyncRow(id, syncData);
}
}
}
});
これまでにあった、キャッシュの仕組みらしい。(使ったことは無いのであまり知らない)
HTTPS
じゃない場合に問題らしい