この記事は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
が追加されます。
続いてキャッシュやデータから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イベントが取れないので、そのあたりどういう扱うか問題については課題
それではよいお年をお過ごしください!