2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

PWAAdvent Calendar 2019

Day 22

Stream APIのReadableStreamでコンテンツ以外をキャッシュしてあそんだ話

Posted at

この記事はPWA Advent Calendar 2019の22日目の記事です。

気がついたら22日になっていて、日数が経過するスピードに着いていけずカレンダーを見てびびっています。モウイクツネルトオショウガツ…

さてさっそくですが表題の件ですが、先日開催されたJSConf JPの「Streams APIをちゃんと理解する」というセッションで、「Fetch API実行後に受け取れるResponseのBodyはReadableStreamなので、Service WorkerでリクエストをインターセプトしてStreamをResponseとして返すことができる」ということを知ったので少しさわってみました。

ためしてみた

今回は、template.htmlというファイルを用意して、そこからコンテンツの上部と下部を切り出し、Fetch時にリクエストされたらキャッシュした部分と、データとして保持しているHTMLをReadableStreamで読み込んでレスポンスとして返却する、というかんたんなデモを作成しました。動作デモはこちらです。

Service Workerからポイントだけ抜粋します。

下準備として、template.htmlというファイルを用意しService Workerのインストール時にコンテンツの上部と下部に分割してキャッシュします。

const CACHE_LIST = [
  'template.html'
  // ...
];

self.addEventListener('install', (installEvent) => {
  console.log('[Service Worker] Installed');

  installEvent.waitUntil(
    self.skipWaiting(),
    (async () => {
      const cache = await caches.open(CACHE_VERSION);

      await cache.addAll(CACHE_LIST)
      .then(() => cache.match('template.html'))
      .then((response) => {
        return response.text().then(text => {
          const headerEndIndex = text.indexOf('<div class="o-layout -main"><div class="o-layout__inner">');
          const footerStartIndex = text.indexOf('</div></div></main>');
          return Promise.all([
            cache.put('template-start.html', new Response(text.slice(0, headerEndIndex), response)),
            cache.put('template-end.html', new Response(text.slice(footerStartIndex), response))
          ]);
        });
      })
      .then(() => console.log('Required assets successfully cached !'))
      .catch((error) => {
        console.warn('Required assets failed to cache. :', error);
      })
    })());
});

こうするとキャッシュにtemplate.htmlから生成された'template-start.html'とtemplate-end.htmlが追加されます。

スクリーンショット 2019-12-22 午前9.14.22.png

続いてキャッシュやデータからReadableStreamでリクエストを生成する関数を作成します。

const createStream = async (request) => {
  const stream = new ReadableStream({
    start(controller) {
      const url = new URL(request.url);
      const header = caches.match('template-start.html');
      const footer = caches.match('template-end.html');
      const filename = /\/$/.test(url.pathname) ? 
       'index.html' : url.pathname.match(".+/(.+?)([\?#;].*)?$")[1];
      const targetPath = 'assets/data/'+ filename;
      const contents = fetch(targetPath)
      .then(response => {
        if (!response.ok && response.status === 404) {
          return caches.match('assets/data/404.html');
        }
        return response;
      });
      const pushStream = (stream) => {
        const reader = stream.getReader();
        const process = (result) => {
          if (result.done) return;
          controller.enqueue(result.value);
          return reader.read().then(process);
        }

        return reader.read().then(process);
      }

      header
        .then(response => pushStream(response.body)).then(() => contents)
        .then(response => pushStream(response.body)).then(() => footer)
        .then(response => pushStream(response.body)).then(() => controller.close());
    }
  });

  return new Response(stream, {
    headers: {'Content-Type': 'text/html; charset=utf-8'}
  })
}

そしてfetchの際に、ReadableStreamでコンテンツをキャッシュからレスポンスとして返却します。

self.addEventListener('fetch', (fetchEvent) => {
  console.log('[Service Worker] Fetch Event');

  const url = new URL(fetchEvent.request.url);
  const destination = fetchEvent.request.destination;
  const pathname = url.pathname;

  if(url.origin === location.origin) {
    if(destination === 'document') {
      if (pathname === scope || pathname === scope + 'index.html') {
        fetchEvent.respondWith(
          cacheFallingBackToNetwork(fetchEvent)
        );
      } else {
        fetchEvent.respondWith(createStream(fetchEvent.request));
      }

      return;
    }

    fetchEvent.respondWith(
      cacheFallingBackToNetwork(fetchEvent)
    );
  }
});

さわってみた感想

  • headの中身(titleなど)をどうしよう問題にぶち当たるので、静的ページで用いるには向いてなさそう
    • コンテンツ情報を動的に引っ張ってくるページやアプリケーションのフレームなどには使えそう
  • キャッシュの扱い方のひとつとして勉強になりました
  • 初回訪問時などはページのFetchイベントが取れないので、そのあたりどういう扱うか問題については課題

それではよいお年をお過ごしください!

参考資料

2
0
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?