Service Worker メモ

(※この記事は 別媒体に投稿した記事 のバックアップです。 canonical も設定しています)

2020-10-09

※この記事ははてなブログ別アカウント(hyiromori)から引っ越しました
※2018年7月頃に書いていたものを発掘して再掲したものなので、情報が古い可能性があります。

Service Worker とは?

Service Worker でできること

ネイティブアプリでないと難しかったことが、Webアプリでも出来るようになってきた印象ですね!
まだ、機能によっては対応しているブラウザが少ないですが、この流れが進むと、Webアプリでもネイティブアプリとほぼ同等のことが出来るようになってくると思います。

Service Worker でできないこと

Service Worker のライフサイクル

INSTALLING

INSTALLED

ACTIVATING

ACTIVATED

REDUNDANT

Service Worker で使用できるイベント

install

activate

message

fetch
sync
push

サンプルソース

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 イベント時に介入したい処理
});

Cache API (利用できるブラウザ)

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 というサイトに様々なパターンの例があり、とても参考になる。

Push API (利用できるブラウザ

Webページ側(プッシュ通知の登録処理)
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);
  }
};
Service Worker 側
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));
});

Background Sync API (利用できるブラウザ)

IndexedDB の定義(Dexie というライブラリを使用しています)

結構ソースが大きくなってしまった。
とりあえず、こんな感じで 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,
};
Webページ側(バックグラウンド同期の登録処理)
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);
};
Service Worker 側
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);
      }
    }
  }
});

App Cache(参考)

これまでにあった、キャッシュの仕組みらしい。(使ったことは無いのであまり知らない)

機能

問題点

App Cache と Service Worker との違い

参考文献