1. 導入
現在、高品質なオフライン対応ウェブコンテンツは、ユーザーにとって容易に発見できるものではない。ユーザーがオフライン時に コンテンツを閲覧できるようにするには、どのウェブサイトがオフラインで動作するかを 知っている必要があるか、インストール済みのPWAが必要になる。 これは、利用可能なコンテンツを発見するための入口が存在しないため、優れたユーザー体験ではない。これに対処するため、 この仕様は、開発者が自身の特定のコンテンツについてブラウザーに伝えることを可能にする新しいAPIを 扱う。
コンテンツ索引により、ウェブサイトはオフライン対応コンテンツをブラウザーに登録できる。ブラウザーは その後、ウェブサイトのオフライン機能を改善し、ユーザーがオフライン時に閲覧できるコンテンツを提供できる。 このデータは、端末上の検索を改善し、閲覧履歴を補強するためにも使用できる。
このAPIを使用すると、次の例のユースケースについて、ユーザーがより容易にコンテンツを発見できるようになる:
-
ニュースサイトが最新の記事をバックグラウンドでプリフェッチする。
-
コンテンツストリーミングアプリが、ダウンロード済みコンテンツをブラウザーに登録する。
1.1. 例
function deleteArticleResources( id) { return Promise. all([ caches. open( 'offline-articles' ) . then( articlesCache=> articlesCache. delete ( `/article/ ${ id} ` )), // この関数が `contentdelete` イベントの結果として呼び出された場合、 // これは何もしない。 self. registration. index. delete ( id), ]); } self. addEventListener( 'activate' , event=> { // service workerがアクティブ化されたとき、古いコンテンツを削除する。 event. waitUntil( async function () { const descriptions= await self. registration. index. getAll(); const oldDescriptions= descriptions. filter( description=> shouldRemoveOldArticle( description)); await Promise. all( oldDescriptions. map( description=> deleteArticleResources( description. id))); }()); }); self. addEventListener( 'push' , event=> { const payload= event. data. json(); // 記事を取得して保存し、その後それを登録する。 event. waitUntil( async function () { const articlesCache= await caches. open( 'offline-articles' ); await articlesCache. add( `/article/ ${ payload. id} ` ); await self. registration. index. add({ id: payload. id, title: payload. title, description: payload. description, category: 'article' , icons: payload. icons, url: `/article/ ${ payload. id} ` , }); // 緊急の場合は通知を表示する。 }()); }); self. addEventListener( 'contentdelete' , event=> { // ユーザーによる削除後、基礎となるコンテンツを消去する。 event. waitUntil( deleteArticleResources( event. id)); });
上記の例では、shouldRemoveOldArticleは開発者定義の関数である。
2. プライバシーに関する考慮事項
contentdeleteイベントを発火すると、ユーザーがページを離れた後にユーザーのIPアドレスが明らかになる可能性がある。
これを悪用すると、位置履歴の追跡に使用できる。ユーザーエージェントは、イベントの持続時間を制限することにより、
追跡を制限するべきである。
contentdeleteイベントを発火するとき、ユーザーエージェントは、ウェブサイトが新しいコンテンツを追加することを
防ぐべきである。
これにより、スパム的なウェブサイトが同じコンテンツを再追加すること、およびユーザーに削除したばかりのコンテンツが
表示されることを防ぐ。
登録済みコンテンツをすべて表示すると、 悪意あるウェブサイトが露出を最大化するために、自身のコンテンツでユーザーをスパムする原因となり得る。 ユーザーエージェントには、すべてのコンテンツを表示するのではなく、ユーザー体験の改善を目的とした ユーザーエージェント定義のシグナルの集合に基づいて、表示する 適切なコンテンツを選択することが強く推奨される。
3. 基盤
3.1. service worker登録への拡張
service worker登録は、さらに以下を持つ:
-
コンテンツ索引 エントリー(map)。各キーはDOMStringであり、各項目はコンテンツ 索引エントリーである。
-
エントリー編集キュー(並列キュー)。初期値は、新しい並列キューを開始する結果である。
3.2. コンテンツ索引エントリー
コンテンツ索引 エントリーは、以下で構成される:
-
記述(
ContentDescription)。 -
起動URL(URL)。
-
service worker登録(service worker登録)。
3.2.1. 表示
ユーザーエージェントは、entryがコンテンツ索引 エントリー(entry)として、service worker登録のコンテンツ索引エントリー内に存在する限り、任意の時点でentryを表示してもよい。
注: ユーザーエージェントは、ユーザーに表示されるエントリーが多すぎることを避けるため、表面化するコンテンツを制限するべきである。
-
UIは、entryのservice worker 登録のスコープ urlのoriginを目立つように表示しなければならない。
-
UIは、entryの記述の
descriptionを表示してもよい。 -
UIは、entryのアイコンのいずれかを 画像として表示してもよい。
-
UIは、UIによって公開された基礎となるentryをユーザーが削除する方法を提供するべきであり、 その場合、entryについてコンテンツ索引エントリーを削除するを 実行する。
-
UIは、ユーザーがそれをアクティブ化する方法(たとえばクリックによる)を提供しなければならず、その場合、 entryについてコンテンツ索引エントリーをアクティブ化するを 実行する。
3.2.2. 非表示
4. アルゴリズム
4.1. コンテンツ索引 エントリーを削除する
-
contentIndexEntriesを、entryのservice worker 登録のコンテンツ索引エントリーとする。
-
entryのservice worker 登録のエントリー編集キューに、次の手順をキューに入れる:
-
entryを非表示にする。
-
contentIndexEntries[id]を削除する。
-
entryについてcontent deleteイベントを発火する。
-
4.2. コンテンツ索引 エントリーをアクティブ化する
-
activeWorkerを、entryのservice worker 登録のactive workerとする。
-
activeWorkerがnullである場合、これらの手順を中止する。
-
newContextを新しいトップレベル閲覧コンテキストとする。
-
newContextの
Windowオブジェクトの環境設定オブジェクトの担当イベントループ上で、ユーザー操作タスクソースを用いて、次の手順を実行するタスクをキューに入れる:-
HandleNavigate: newContextを、entryの起動URLへ、例外有効および置換 有効でナビゲートする。
-
HandleNavigateとラベル付けされた手順で呼び出されたアルゴリズム手順が例外を投げる場合、 これらの手順を中止する。
-
frameTypeを"`top-level`"とする。
-
visibilityStateを、newContextのアクティブ文書の
visibilityState属性値とする。 -
focusStateを、newContextのアクティブ文書を 引数としてhas focus手順を実行した結果とする。
-
ancestorOriginsListを、newContextのアクティブ文書の関連するグローバルオブジェクトの
Locationオブジェクトの祖先originリストの 関連付けられたリストとする。 -
serviceWorkerEventLoopを、activeWorkerのグローバルオブジェクトのイベントループとする。
-
serviceWorkerEventLoop上で、DOM操作 タスクソースを用いて、次の手順を実行するタスクをキューに入れる:
-
newContextの
Windowオブジェクトの環境設定オブジェクトの 作成URLのoriginが、activeWorkerのoriginと同じでない場合、これらの手順を中止する。 -
newContextの
Windowオブジェクトの環境設定オブジェクト、 frameType、visibilityState、focusState、および ancestorOriginsListを引数として、Create Window Clientを実行する。
-
-
ここで新しい Browsing Contextを作成することは正しいことか? (issue)
4.3. content deleteイベントを発火する
ContentIndexEventを使用する
"contentdelete"という名前の機能イベントを発火する:
5. API
5.1.
ServiceWorkerGlobalScopeへの拡張
partial interface ServiceWorkerGlobalScope {attribute EventHandler ; };oncontentdelete
5.1.1. イベント
以下は、ServiceWorker
インターフェイスを実装するすべてのオブジェクトにより、イベントハンドラーIDL属性として
サポートされなければならないイベントハンドラー(および対応するイベントハンドラーイベント型)である:
| イベントハンドラーイベント型 | イベントハンドラー | インターフェイス |
|---|---|---|
contentdelete
| oncontentdelete
| ContentIndexEvent
|
5.2. ServiceWorkerRegistrationへの拡張
partial interface ServiceWorkerRegistration { [SameObject ]readonly attribute ContentIndex index ; };
ServiceWorkerRegistrationは、
コンテンツ索引(ContentIndex)を持ち、初期値は
そのservice worker登録がコンテキスト
オブジェクトのservice worker登録である
新しいContentIndexである。
index属性の取得子は、
コンテキストオブジェクトのコンテンツ索引を返さなければならない。
5.3. ContentIndex
現在のエンジン1つのみ。
OperaなしEdgeなし
Edge (Legacy)なしIEなし
Firefox for AndroidなしiOS SafariなしChrome for Android84+Android WebView84+Samsung InternetなしOpera Mobile60+
enum {ContentCategory ,"" ,"homepage" ,"article" ,"video" , };"audio" dictionary {ContentDescription required DOMString ;id required DOMString ;title required DOMString ;description ContentCategory = "";category sequence <ImageResource >= [];icons required USVString ; }; [url Exposed =(Window ,Worker )]interface {ContentIndex Promise <undefined >add (ContentDescription );description Promise <undefined >delete (DOMString );id Promise <sequence <ContentDescription >>getAll (); };
現在のエンジン1つのみ。
OperaなしEdgeなし
Edge (Legacy)なしIEなし
Firefox for AndroidなしiOS SafariなしChrome for Android84+Android WebView84+Samsung InternetなしOpera Mobile60+
ContentIndexは、
service worker登録(service worker登録)を持つ。
5.3.1.
add()
現在のエンジン1つのみ。
OperaなしEdgeなし
Edge (Legacy)なしIEなし
Firefox for AndroidなしiOS SafariなしChrome for Android84+Android WebView84+Samsung InternetなしOpera Mobile60+
add(description)メソッドは、呼び出されたとき、
新しいpromisepromiseを返し、
次の手順を並列に実行しなければならない:
-
registrationを、コンテキスト オブジェクトのservice worker登録とする。
-
registrationのactive workerがnullである場合、promiseを
TypeErrorで拒否し、 これらの手順を中止する。 -
descriptionの
id、title、description、 またはurlのいずれかが空の文字列である場合、promiseをTypeErrorで拒否し、 これらの手順を中止する。 -
launchURLを、コンテキストオブジェクトの関連する設定オブジェクトのAPI基底URLで、descriptionの
urlをパースする結果とする。注: より狭いスコープを持つ新しいservice worker登録が 後に導入される可能性がある。
-
matchedRegistrationを、launchURLを引数としてMatch Service Worker Registration アルゴリズムを実行した結果とする。
-
matchedRegistrationがregistrationと等しくない場合、promiseを
TypeErrorで拒否し、これらの手順を中止する。 -
registrationのactive workerの拡張イベントの集合が
FetchEventを含まない場合、 promiseをTypeErrorで拒否し、これらの手順を中止する。 -
iconsを空のリストとする。
-
任意で、ユーザーエージェントはdescriptionの
iconsから使用するアイコンを選択してもよい。 その場合、正常にパースされた後の、descriptionのiconsの 選択済みアイコンの各画像リソース(resource)について、 次の手順を実行する: -
entryを、次を持つ新しいコンテンツ索引エントリーとする:
- 記述
-
description。
- 起動URL
-
launchURL
- service worker 登録
-
registration。
- アイコン
-
icons
-
idを、descriptionの
idとする。 -
contentIndexEntriesを、registrationのコンテンツ索引 エントリーとする。
-
registrationのエントリー編集キューに、次の手順をキューに入れる:
注: 既存のIDを持つ記述を追加すると、 前の値が上書きされる。
5.3.2.
delete()
現在のエンジン1つのみ。
OperaなしEdgeなし
Edge (Legacy)なしIEなし
Firefox for AndroidなしiOS SafariなしChrome for Android84+Android WebView84+Samsung InternetなしOpera Mobile60+
delete(id)メソッドは、呼び出されたとき、
新しいpromisepromiseを返し、次の
手順を並列に実行しなければならない:
-
registrationを、コンテキスト オブジェクトのservice worker登録とする。
-
contentIndexEntriesを、registrationのコンテンツ索引 エントリーとする。
-
registrationのエントリー編集キューに、次の手順をキューに入れる:
5.3.3.
getAll()
getAll()メソッドは、呼び出されたとき、新しいpromisepromiseを返し、
次の手順を並列に実行しなければならない:
-
registrationを、コンテキスト オブジェクトのservice worker登録とする。
-
contentIndexEntriesを、registrationのコンテンツ索引 エントリーとする。
-
descriptionsを空のリストとする。
-
registrationのエントリー編集キューに、次の手順をキューに入れる:
5.4. ContentIndexEvent
現在のエンジン1つのみ。
OperaなしEdgeなし
Edge (Legacy)なしIEなし
Firefox for AndroidなしiOS SafariなしChrome for Android84+Android WebView84+Samsung InternetなしOpera Mobile60+
dictionary :ContentIndexEventInit ExtendableEventInit {required DOMString ; }; [id Exposed =ServiceWorker ]interface :ContentIndexEvent ExtendableEvent {(constructor DOMString ,type ContentIndexEventInit );init readonly attribute DOMString ; };id