目次
05-03 令和
05-03 ServiceWorker と indexedDB
タイトルは内容と無関係。
とりあえず、天皇が代替わりして元号が変わりましたよ、という記録として。
そして、記録なので、多くの人が知っているあたりまえのことの説明から始める。
さて、長すぎるゴールデンウィーク。
今年は代替わりの影響で、天皇誕生日がない。
そこで、即位の日である5月1日が祝日に設定された。
4月29日は昭和の日で、5月3日は憲法記念日で祝日だ。
ここで、5月1日が祝日になると、「前後を祝日に挟まれた日は休日」というオセロのようなルール(国民の祝日に関する法律 第三項第三条)によって、4月30日と5月2日は休日となる。
このオセロルール、もともとは憲法記念日の5月3日と、こどもの日の5月5日に挟まれた5月4日を休日にするために作られた。
1985年、バブルのころに作られた法律だ。
当時は、日本人は働きすぎだ、と思われていた。一方で、バブル景気で長期休みを取って旅行する人も多かった。
そこで、休日を増やせば旅行などで消費が増え、一石二鳥! と作られた法律だ。
その後、同じような考えで連休を作り出すためにいくつかの祝日が、日付固定から「月曜日」に移動され(いわゆるハッピーマンデー法案:1998年成立)、その結果、本来は離れていた敬老の日と秋分の日が近くなり、上に書いた「祝日に挟まれた日は祝日」ルールが不意に発動したりするようになった。
ちなみに、日本は世界的に見ても祝日が多い、「平日」が少ない国だそうだ。
働きすぎだと言われるのは、休日がないからではなく、休日でも働いたり残業するのが当たり前だったりする雰囲気が作られているからだ。
休日を増やしたからと言って働きすぎが解消されるわけではない。
いきなり話がそれ気味だが、毎年ゴールデンウィークには有給休暇を混ぜて長い連休を作る人がいる。
しかし、今年はそんなことをせずとも、カレンダー通りで10連休となった。
…休みが長すぎて、「そんなに休んでいるわけにはいかない」と働いている人・働かざるを得ない業界続出だけどな。
通常のゴールデンウィークなら、有休を使って連休に「する人もいる」だけなのである程度緩急があり対応しやすいが、今年はみんな連休だから。
かくいう僕も、24時間稼働しているサーバー関連のお仕事もしているために、ゆるく仕事を頼まれていたりする。
管理をお手伝いしているあるサービスでは、5月1日に内容更新があった。
プログラム変更を伴ったので、僕も作業した。
そしたら、管理担当者は、いつも西暦で書いている「更新情報」に、いきなり「令和元年5月1日更新」って書きやがった。
まぁ、書きたくなる気持ちはわかる。次回からはまた西暦に戻すそうだ。
こんなゴールデンウィーク中に出かけたら、混みすぎていて楽しめないだろう…と、我が家ではどこにもいかないよ、と子供にあらかじめ宣言してあった。
その代わり、家の中で楽しめそうなことはする。
一応、「どこか行きたい」という気持ちの対策として、4月21日の次女の誕生日にはキッザニアに行ってあるしね。
26日には、長女の誕生日パーティも家でやっている。
29日は、妻が家の庭の手入れなどしつつ、庭でバーベキュー。
焼き鳥 50本と、ハーブ入りフランクフルトを買ってきた。買いに行ったら、スーパーがいつもより混んでいて驚いた。
30日、出かけないといいつつ、陽光台にある子供宇宙科学館へ。
この日は雨だったし、祝日ではなく「休日」だったので、多少人出が少ないだろうと思ったんだ。
でも、朝早くから出て、10時過ぎには到着した。
科学館は9時半オープンなのだけど、入り口前に行列ができていた。
科学館に行ったのは、以前から長女が行きたがっていたから。
また、次女が小学校でもらってきた科学館のチラシに、「暦」をテーマにした小さなイベントをやると書いてあったから。
(僕は暦好きです。チラシを見ただけで内容はだいたい想像できたけど、知らないこともあるかもしれないので見てみたかった)
行列が長くて入れそうにないし、雨も降っているので、すぐ近くのどんぐりハウスへ。
小学生の向けの無料施設で、本来は両親共働きの小学生が、放課後に大人の保護下で遊んで待つことができる施設だ。
実は、長女が「科学館に行きたい」と言っていた理由の半分は、こちらに来たかったからだ。
すぐ隣の施設なので、行くときはセットで考えていたのだな。
子供が遊んでいる間に、妻が科学館の様子を見に行ってくれた。
人が多すぎて入場制限をしているようで、列はさらに伸びた、と報告。
2時間弱遊んだが、あきらめて帰ることにする。
子供の洋服が少し足りない感じなので、デパートに行って買い物して帰ろう。
…というわけで港南台に行ったら、駐車場が混んでいてなかなか入れない。
みんなで車に乗っているのは無駄なので、妻と子供にはおりてもらい、先に昼ご飯を食べててもらう。
30分ほどかけてやっと駐車し、合流。僕も軽く食べて買い物へ。
ユニクロに行ったら、スプラトゥーンTシャツを売っていた。
子供も欲しがるので、良し買うか! と子供コーナーに行ったら、売り切れていた。
あるのは大人用のみ。
ネットで調べたら、ネット店舗ではまだあるようだ。ネットで買うことにする。
(夜、家族全員分を買った。5千円分買わないと送料かかるから。)
それ以外に、長男の服が足りないので数着購入。
この日は、以上。どこ行っても混んでいる、と分かったので家に帰って夕食食べました。
夕食は、パーティー気分で…残っていた焼き鳥などを適当に。
5月1日は、先に書いたように僕の仕事があったのでどこも出かけず。
5月2日は、長女・次女が歯医者に行かねばならなかったので、歯医者以外出かけず。
鎌倉の歯医者に行くのは、きっと道が大渋滞だろう、と恐怖でしかなかったのだが、案外空いていた。
でも、人混みはすごかった。
鎌倉だから、多くの人は車で来るのではなく、東京から電車で来るのだね、と行ってから気づいた。
以上、昨日分までの日記でした。
このほかに、隙間時間を見ながら仕事のプログラムをしていて新しい知見を得たので、次にそのことなど書く。
同じテーマの日記(最近の一覧)
関連ページ
別年同日の日記
申し訳ありませんが、現在意見投稿をできない状態にしています。 |
「ゴールデンウィーク明けにはリリースしたいので、その前までにお願いね」
と言われていたプログラム改良があった。
その改良は、ちゃんと四月半ばまでに仕上げた。
WEB ブラウザ側で動く Javascript のプログラムだったのだけど、同時にサーバー側も変更することになっていた。
サーバー変更は僕の仕事ではなかったのだけど、そちらが間に合っていない。
急遽、Javascript 側で何とかできないかと、ゴールデンウィーク直前に ねじ込まれた。
目的は、URL の一部隠蔽だ。
ブラウザ側プログラムの改良により、画面遷移が一段階へり、目的画面の URL が変わることになった。
この URL にはセッション情報が含まれており、見えているのが好ましくない。
そこで、セッション情報を URL に入れるのではなく、POST にするか、Cookie にするなどして、消すことになっていた。
つまり、これらはサーバー側の改良だ。
それができなくなったので、Javascript 側でどうにかならないか、というのが、追加の依頼だった。
セッション情報はややこしい文字列になっているので、長い時間表示されているのでなければ、一瞬なら出てもよいという。
つまり、セッション情報付きの URL でアクセスしてから、取得したページの Javascript の中ですぐに URL を変更し、消せればいいということだ。
ただし、該当ページは都合上、時々リロードする場合がある。
セッション情報なしではサーバーにアクセスできないので、リロードする際にはすぐに URL を元に戻したい。
URL を変更するのは簡単だ。history.replaceState 関数を使うと、現在アクセス中として表示されている URL を変更することができる。
いわゆる ajax …画面遷移を伴わないサーバーアクセスを行うようなプログラムで多用されているテクニックだ。
だから、セッション情報がない URL にすることはできる。
この時に、本来の URL を変数に保存しておいて、リロードの際に元に戻せればいいだろう。
しかし、javascript に「リロードイベント」というのは存在しない。
#イベントとは、「何かが起きた」とプログラムが知ることができる、概念上の存在。
Javascript では、特定のイベントに対してプログラムを紐づけることができ、紐づいたプログラムは、イベントが起きると自動的に実行される。
これにより、イベントが生じたらプログラムを動かす、ということが可能になっている。
リロードされたかどうかを知る方法はあるのだが、それはリロードされた「あと」のページで検知できるだけだ。
そして、リロードの際には表示している URL にアクセスが行くので、そのままではセッション情報がないためにサーバーから拒否される。
リロード「あと」のページの表示自体が行えない状態で、リロードに伴う不都合をこの方法で解消することはできない。
「ページが終了した」というイベントはある。unload イベントだ。
しかし、Javascript プログラムの寿命は、そのページが終了するまでだ。
そのため、unload イベントにプログラムをセットしておいても、実行されることはない。
実行されないんじゃ意味ないじゃん、ってことで、beforeunload というイベントもある。
「unload の直前」というイベントだ。
よく、「ページを離れてもいいですか?」というダイアログが表示されるのは、このイベントの時に動くプログラムによるものだ。
このイベントは、次にアクセスする URL が決定し、新しいページの取得準備が整うと呼び出される。
つまり、このイベントの中で URL を書き換えても、すでに URL は決定していて意味がない。
このイベントでできることは、新しいページへの移行を中断し、今のページに居続けるかどうかをユーザーに問いかける、ということのみだ。
そんなわけで、ブラウザ側の Javascript で解決するのは無理とわかった。
でも、まだ最後の望みがあった。
一般に「ブラウザ側の Javascript」というのは、WEB ページに付随するものだ。
しかし、最近のブラウザには、WEB ページではなく「ブラウザに」Javascript を登録し、動かす方法があるのだ。
ServiceWorker という。
Chrome/FireFox では早くから実装され、便利なので使いたいプログラマも多かったのだけど、Safari ではなかなか実装されなくて使いどころが限られていた。
昨年、やっと Safari も対応したので、使われる局面が増えてきている。
これを使うと、WEB ページがサーバーに問い合わせた瞬間に、そのリクエストを横取りして、Javascript がサーバーの「ふりをして」返事を返したりできる。
だから、オフラインなのにキャッシュしておいたデータを渡したり、逆にサーバーにデータを送ろうとしたときにキャッシュしておいて、あとでオンラインになった時にこっそり送信したりできる。
Google Docs がオフラインでも使えるのはこの仕組みを使っているからだ。
Android では、この仕組みを使って WEB ページを「アプリ化」することもできる。
WEB ページに行くと「このページをインストールしますか?」と促され、インストールを選ぶとホーム画面にアイコンが現れる。
このアイコンをタップすれば、いつでも WEB ページが提供する機能を使える。
Google Docs の例と同じように、適切に処理されていれば、オフラインでだって使える。
Service Worker の利用例は、多くは「WEB を オフラインでも使えるようにする」のと、それをさらに拡張した「アプリ化」だ。
だけど、先に書いたように、サーバーへのリクエストを横取りして、いろいろなことができる。
キャッシュデータを渡してオフラインでも動くようにする、というのは代表的な応用例にすぎない。
これなら、リロードの問題を解決できそうに思えた。
…一筋縄ではいかなかったのだけど、実際解決できた。
基本戦略はこうだ。
・WEB ページ側で、history.replaceState を使って URL を一部削除。
同時に、削除前の URL を ServiceWorker に渡して、データ保持。
・サーバーへのアクセスが生じた際、それが URL を一部削除したものだと判明したら、
保持していた本来の URL にアクセスし、そのデータを WEB ページ側に渡す。
実験過程を端折るが、実際にこの流れを作ったらうまく動かなかった。
WEB ページ側で動いている Javascript は、内部でセッションデータを使用していたのだ。
(僕は途中から参加して改良してきただけなので、そうした部分があるのを知らなかった)
だから、サーバーアクセスの際の流れを少し変えることにした。
・サーバーへのアクセスが生じた際、それが URL を一部削除したものだと判明したら、
保持していた本来の URL にリダイレクトする指示を作り出し、WEB ページ側に渡す。
リダイレクトが挟まるので、WEB ページ側としては少し動作が遅くなってしまう。
しかし、一度本来の URL に戻ってからリロードすることになるので、内部で動作するプログラムでも
URL に入れたセッションデータを使えることになり、問題は出なくなる。
さて、実際に作る上では、いくつか問題があった。
一番重要なのが、Javascript の生存期間だ。
WEB ページでは、そのページが存在する間、Javascript が生存する。
変数などもこの間は保持されるので、データ保持も変数に入れればよい。
しかし、ServiceWorker では、生存期間の考え方が二つあるのだ。
先に書いたように、ServiceWorker はブラウザ側で動き、WEB ページが閉じられても残り続ける。
しかし、それは「残っている」だけで、生きているわけではない。
じつは、必要な時に動作し、動作を終わるとすぐに終了してしまうのだ。
「データを渡す」という動作があったとしたら、その動作が終わった瞬間に終了し、変数は消える。
その後、「サーバーアクセス」という動作があり、動き始めたときには変数は存在していない。
だから、変数ではない方法でデータを保持しないといけない。
ServiceWorker は非常に強力な仕組みなので、悪さができないように厳しい制限が課されている。
データ保持方法としては、indexedDB しか使えない。
古くから知られるデータ保持方法には、Cookie や webStorage があるが、それらよりも新しい仕組みだ。
まぁ、それしか使えないのであれば、それを使おう。
…これが、イベントで動作する仕組みになっていて使いにくい。
DB に接続する、という指示を出すと、少し後で「接続成功」というイベントが起きる。
だから、接続成功イベントに、接続後のプログラムを登録しておく。
接続後には、アクセスしたい対象を指定してから「データ取り出し」を指示する。
しかし、データはすぐ取り出されるわけではない。少し後で「取り出し成功」イベントが起きる。
だから、ここでもイベントに対してプログラムを登録しておく必要がある。
非常に面倒くさい。
しかし、他に方法はないのでその方法でプログラムを組む。
ところが、もう一つ問題があるのだ。
ServiceWorker 自体は、こうしたイベントによって動くモデルではないのだ。
いや、先に書いたように「サーバーへのアクセスが生じた」などのイベントはある。
でも、Javascript ってとにかく「何かしたら後で呼び出して」という書き方が多くて、ここ数年で新しい書き方が取り入れたのだ。
そして、ServiceWorker は、基本的にその書き方で書かれないといけないようになっている。
この書き方は Promise というやつで…
この概念の歴史から入ろうと思ったが、無駄に長いのでやめた。
同時に、概念だけ説明するのも同じくらい長くなるのでやめた。
ともかく、概念自体は古いとはいえ、10年たっていないのではないかな。
Javascript の言語仕様に取り入れられたのは、2015年なので、4年程度しかたっていない。
しかし、便利なので、これまでイベントモデルで作られていた Javascript が、最近は Promise モデルで機能追加されるようになってきている。
上に書いたが、ちゃんと理解していないと混ぜて使えない。
ちなみに、Promise の概念の前に Deferred があり、こちらのことは以前に書いたことがある。
微妙に違うとはいえ、概念的に慣れていたのですぐに…でもないけど、1日程度で理解できた。
そんなわけで、同じことで悩んでいる人にとっての「答え」を示そう。
ServiceWorker では、fetch (URL アクセス)イベントに対して、最終的な「返事」を event.respondWith として返せばよい。
ここで、返す内容は Promise でなくてはならない。
Promise は、resolve を引数とする関数として定義される。
resolve はコールバック関数であり、Promise の処理終了時には、resolve を呼び出してやらなくてはならない。
この時、resolve には、「結果」を引数として渡す。
通常の関数における、return と同じような感じで、何が結果になるのかはその時の処理内容による。
respondWith は、URL アクセスに対する答えを返す関数なので、答えは http の Response となる。
これは Response クラスとして定義でき、リダイレクトしたいなら headers の Location に URL を渡せばよい。
(http と同じだ)
以上の「きまりごと」さえ守れば、あとはどんな書き方をしようとも自由だ。
関数内部で indexedDB のイベント呼び出しをたくさん定義しても構わない。
僕が仕事で必要だった関数の場合、次のようになった。
self.addEventListener('fetch',function(event){
let url = event.request.url;
let param = url.match(/targetDirectory\/([0-9]+)(\?session=)?/);
if(!event.isReload || !param || param[2])return;
event.respondWith(new Promise(function(resolve){
let openReq = indexedDB.open('targetDB');
openReq.onsuccess = function(event){
let db = event.target.result;
let trans = db.transaction('fullURL','readonly');
let store = trans.objectStore('fullURL');
let getReq = store.get(param[1]);
db.close();
getReq.onsuccess = function(event){
let redirectResponse = new Response('',{
status: 302,
statusText: 'Found',
headers: {
Location: event.target.result.url
}
});
resolve(redirectResponse);
}
}
}));
});
説明しておくと、/targetDirectory/123?session=**** というような URL が隠蔽対象だ。
リロードにより fetch イベントが起きているときは、すでに隠蔽されて ?session= 以降はなくなっている。
しかし、初回アクセス時や、リダイレクト直後は ?session= 以降もついているので、判別して処理を変える必要がある。
そして、123 の部分の数値はページの分類を示していて、ブラウザの複数のタブで開かれる可能性がある。
それぞれでリロードしたときに混乱しないように、123 を DB のキーとして、元の URL を格納してある。
#格納部分のプログラムは別途必要。そちらは Promise と無関係に書けるので、それほど難しくない。
…というわけで、URL を検査して、隠蔽されていない場合はそのままアクセスを通す。
隠蔽されている場合は、123 にあたる部分をキーとして DB を検索し、得られた URL へのリダイレクトを生成する。
ちなみに、僕の要件ではリダイレクトが必要だったが、URL を差し替えてそのままサーバーにアクセスしたい、という場合もあるだろう。
実はそちらの方がプログラムは簡単で、redirectResponse を生成せずに、
resolve(fetch(event.target.result.url));
としてしまえばよい。
ここで重要なのは、
・fetch イベントに対する反応は、すぐに返さないといけない。
・event.respondWith が呼び出されれば、そこで帰ってくる反応を待つ。
・呼び出されなければ、本来の URL に対してアクセスを行う。
ということだろう。
respondWith は「結果を返す関数」ではなく、「結果を返すと約束する関数」だ。
結果を返すものだと思って、結果が出てから呼び出そうとすると、「fetch に対して respondWith が返されなかった」ことになり、本来のサーバーにアクセスが行ってしまう。
そして、結果を返すと約束するのだから、返すのは Promise (約束)だ。
Promise は、最終的に resolve として渡される関数を呼び出しさえすれば、その内部でどれほどの時間がかかっても構わない。
だから、時間がかかる indexedDB の処理は、この Promise 内部で行うようにする。
…ただこれだけのことだ。
プログラム例を示したら、なんてことのない、当たり前に見えるコードだろう。
けど、こういう例がネットを探してもなかなか見つからないんだ。
ServiceWorker というと、「キャッシュを使ってオフラインでも動作するようにしましょう」ってお決まりのパターンばかりで。
#ちょっと気が利いた例では、簡単な文字列を「WEB ページ」として生成して返したりする実験をしていた。
しかし、indexedDB を使って何かのデータを返すような処理を見つけることはできなかった。
この日記が、同じような処理をしようとしている人の助けになれば幸いである。
同じテーマの日記(最近の一覧)
関連ページ
別年同日の日記
申し訳ありませんが、現在意見投稿をできない状態にしています。 |