はてなスターのひみつ

ハッピーホリデー!id:cockscombです。この記事ははてなエンジニアAdvent Calendarの8日目のエントリです。

今年1月、はてなスターのリニューアルを行いました。リニューアルの内容は告知をご参照ください。

はてなスターのリニューアルでは、クロスオリジンの問題を解決するために特別な実装をしています。今回は、ホリデーシーズンをお祝いして、そのひみつを(つまび)らかにします。

はてなスターとクロスオリジン

はてなスターは、はてなブログなどに埋め込んで利用されます。はてなブログは hatenablog.comhatenadiary.jp などのサブドメインを利用しており、さらにはてなブログProでは独自のドメインを設定できます。 はてなスターは複数の異なるドメイン名のサイトから利用される、ということです。

要するにはてなスターはクロスオリジンで利用されます。一方ではてなスターをつけるには資格情報が必要なので、何か少し難しい仕組みが必要です。このような難しさは今年10月にも話題になりましたね。

はてなスターとiframe

はてなスターでは、s.hatena.ne.jp のAPIを呼び出すため、s.hatena.ne.jp のページをiframeで埋め込んでおり、クロスオリジンを回避しています。ただし、スターが横に伸びていくというレイアウトの都合から、スターの表示はiframeの外側で行なっています。iframe内にはスターの追加を行うスターボタンがあり、APIとの通信はiframeの内部で行います。

スターボタンはiframe内に表示

iframeの内外では MessageChannel を介して postMessage() でコミュニケーションします。WindowpostMessage() を使わずに MessageChannel を利用するのは、ドキュメント中に複数のはてなスターが埋め込まれるときに、混線させないためです。

また postMessage() はComlinkを使って抽象化しています。

はてなスターとサードパーティCookie

前述のようにはてなスターではiframeを使っていますが、iframe内はいわゆるサードパーティCookieを利用するような状態になります。サードパーティCookieはページ訪問者の詳細なトラッキングに利用される場合があり、近年ではブラウザのプライバシー保護機能によって単純には利用できなくなってきています。

リニューアル以前のはてなスターは、Safariなどでプライバシー保護機能を有効にしていると正常に動作しない問題がありました。リニューアル後のはてなスターは、利用者のプライバシーと利便性を両立するため、Storage Access APIを使用しています。

Storage Access APIは、利用者がクリックなどの明示的な操作を行なった際に(場合によってはさらにダイアログを表示し)、特別にサードパーティCookieなどを利用できる権限を得る、というものです。はてなスターでは、スターボタンが押下された際に権限を要求します。スターボタンを押下したということは、利用者が明確にはてなスターを利用する意思を持っているということなので、ちょうどいいタイミングのはずです。

Storage Access APIはもともとSafariがIntelligent Tracking Protection (ITP)の導入を進める中で提案していたものですが、最近ではFirefoxやEdgeにも実装されています。

はてなスターと状態の同期

はてなスターには、カラースターパネルというウインドウがあります。カラースターの追加やスターの削除を行うためのインターフェースです。カラースターパネルでスターが追加されたり削除されたりした際に、それがちゃんと元のページに反映されないと、変な感じになってしまいます。

はてなスターでは次の図のように、スターボタンの内部状態が元のページに反映され、スターボタンとカラースターパネルは内部状態が同期している、というふうになっています。

ページ(main window)とスターボタン(iframe)、カラースターパネル(sub window)で状態を同期

この状態の同期をどうするとうまく実現できるでしょうか。ウインドウやiframeの間のやりとりは、全て postMessage() を介して行なっています。postMessage() では構造化複製アルゴリズムでコピーできるオブジェクトだけをやりとりできます。

はてなスターでは、状態を管理するのにReduxを使うことにしました。ReduxのStoreはウインドウ単位で保持し、Actionをウインドウ間で複製することにしました。Actionはピュアなオブジェクトとして表すのが推奨されているため、ふつうは構造化複製アルゴリズムでコピーできます。原理的には、Reducerの実装が同じであれば、Actionを複製すると、Storeの状態を同期できるはずです(厳密にはActionの順序まで一致する必要があります)。実装上は、Reduxにミドルウェアを追加して、Storeが受け取ったActionを異なるウインドウにレプリケーションしています。

この仕組みで、スターボタンとカラースターパネルの間では完全に状態が同期されるようにして、スターボタンと元のページでは最低限の状態を同期しています。元のページでは第三者のスクリプトが実行されているので、ページのオーナーが知っていい情報しか持たせない、という意図です。

はてなスターのまとめ

はてなスターのリニューアルでは、ここまで紹介してきたテクニックを使って、クロスオリジンでもうまく動作するようにしました。セキュリティやプライバシーが侵害されず、また利便性も損なわないように、なるべく注意を払っています。今回紹介した以外にも、表示が少しでも早くなるように(ページの表示に悪影響を与えないように)実行タイミングを調整したり、軽量化のためにReactではなくpreactを使うなど、工夫をしています。

ブラウザには継続的にセキュリティやプライバシーのための改善が行われており、その中にはiframeのサンドボックス化やサードパーティCookieの制限があります。一方で、はてなスターのようにクロスオリジンで機能するサービスのために、ウインドウ間メッセージングやStorage Access APIが用意されています。はてなでは引き続き、こういった仕組みに適応して、サービスの改善に努めてまいります。

【PR】はてなは、ブラウザが大好きな仲間を募集しています。

お疲れさまでスター!