mizdra's blog

ぽよぐらみんぐ

Scrapboxのページ内に埋め込まれているアイコンをsuggestして挿入できるUserScript作った

Scrapboxで会議の議事録を取っていると、誰の発言かを記録するために他人のアイコンを挿入したい、という場面が出てくる。Scrapboxでは ctrl+i と入力すれば簡単に自分のアイコンは挿入できるけど、他人のアイコンを簡単に挿入する方法は意外にない。一応以下のような手順で任意のページのアイコンを挿入できるというテクニックもあるけど、キーボード上を結構ダイナミックに指を動かす必要があってお手軽ではない & そもそもアイコンが無いページもポップアップに出てくるのでノイズが多い。

  1. [ キーを押す
  2. scrap と入力
  3. scrap にマッチするページ一覧がポップアップで出てくるので、Tabキーを数回押して scrapbox にカーソルを合わせる
  4. ctrl+i[scrapbox.icon] を挿入

そこでもっとお手軽に他人のアイコンを挿入できるUserScriptを作った。

gyazo.com

ctrl+l と押すとsuggest-boxが表示されて、ページ内に埋め込まれているアイコンがポップアップで表示される。boxにキーワードを入力すると、そのキーワードにマッチするアイコンが絞り込まれる。Scrapboxネイティブのポップアップと同じように、Tabキーでカーソルを移動し、Enterで確定できる。

パフォーマンス上の制約から、初めにsuggest-boxを押したタイミングでページ内を解析し、ヒットしたアイコン一覧がページごとにキャッシュするようになっている。そのため、一度suggest-boxを表示した後そのページに無かった種類のアイコンを挿入すると、そのアイコンはsuggest-boxには出てこない。もし後から追加したアイコンもsuggest-boxに出したければブラウザリロードするか、suggest-boxの表示中に ctrl+r を押すと良い。キャッシュが破棄・更新され、アイコンが出るようになる。

導入方法

UserScriptを有効化した上で自分のページに以下のスクリプトを追加する.

// ref: https://scrapbox.io/mizdra/icon-suggestion
import { registerIconSuggestion } from '/api/code/mizdra/icon-suggestion/script.js';
registerIconSuggestion();

おまけ

icon-suggestionを作る過程でいくつか有用なコード片が生まれたので、ライブラリとして切り出して公開しておきました。良ければお使い下さい。

画像による Layout Shift が無くなる Web がやって来る

はじめに

Web では古来より <img> タグを用いて画像を読み込んでいました. しかし <img> タグにはアスペクト比に関する情報を埋め込むための属性が用意されていません. そのため, ブラウザが画像をネットワークから fetch して読み込みが完了するまで, レスポンシブな img 要素の寸法を決定できず, ページにガタツキ (Layout Shift) が生じる問題がありました.

この問題を解決するため以前より, アスペクト比を埋め込むための新たな属性の導入が提案されていました. しかし最近議論に動きがあり, 既存の属性を利用する方法が提案され, ブラウザに実装され始めています. ここでは問題の背景, 提案と議論の変遷, そして開発者が取るべき対応について紹介します.

img タグと Layout Shift

Web ページにおいて, できるだけ早く意味のあるものをユーザに表示することはとても重要です. ページの表示の遅れはユーザに不快感を与え, ページから離脱する可能性を高めます. そのためブラウザは初期のページの表示に不要なリソースの読み込みを遅延させ, できるだけ早く意味のあるページを表示します.

例えば画像はテキストと比較し, 非常に容量の大きいリソースです. HTML ファイルのダウンロードは数十ミリ秒でできても png ファイルの場合は数秒, 3G のような低速回線では数十秒掛かるかもしれません. そこでブラウザは画像のダウンロードを待たずにページのレンダリングを開始します. img 要素は初回のレンダリングでは 0x0 で描画され, ダウンロードが完了したら正しい寸法でレンダリングし直されます.

しかしこれには画像のダウンロード前後でページにガタツキ, いわゆる Layout Shift が発生するという問題があります. 試しに以下のサンプルページをタブで開いて, リロードして見て下さい. はじめに ページタイトル, パラグラフ とだけ書かれたページがまず表示され, ページタイトル, パラグラフ の間に画像が表示されます. この画像のダウンロード前後に発生するズレが Layout Shift です. *1.

See the Pen oNjaQVr by mizdra (@mizdra) on CodePen.

こうした問題を回避するため, img タグの width / height 属性や CSS の width / height プロパティで img 要素の寸法を指定することが推奨されています. これにより, ブラウザは画像をダウンロードする前に余白を確保し, ダウンロードされたらその余白に画像を描画できるようになります.

See the Pen oNjaQVr by mizdra (@mizdra) on CodePen.

しかし, レスポンシブな img タグの場合は一筋縄ではいきません. というのも, img タグで HTML の width / height 属性と CSS の width / height プロパティを同時に使用した場合, img タグの寸法の算出にはCSSプロパティのみが利用されるからです. 例えば以下のページではHTML属性で width="200" height="100" を指定していますが, 実際に img タグの寸法の算出に利用されるのはCSSプロパティの width: 100%; height: auto; です. そのため, img 要素は初回のレンダリングでは 400x0 で描画され, ダウンロードが完了したら 400x200 でレンダリングし直され, Layout Shift が発生してしまいます.

<div>
  <h1>ページタイトル</h1>
  <img
    class="image"
    src="https://placehold.jp/200x100.png"
    width="200"
    height="100"
    alt="画像"
  />
  <p>パラグラフ</p>
</div>
div {
  box-sizing: border-box;
  border: 4px solid red;
  width: 400px;
}
.image {
  box-sizing: border-box;
  border: 4px solid #333;
  width: 100%;
  height: auto;
}

intrinsicsize 属性

この問題の根本的なレスポンシブな img 要素の寸法はその画像のアスペクト比が分からないと計算できないにも関わらず, 画像をダウンロードする以外にアスペクト比を知る手段が無いことでした. 裏を返せば, ブラウザが画像をダウンロードする前にアスペクト比を知る手段を提供すれば問題を解決できるということです. そこに着目し, 提案されたのが intrinsicsize 属性です.

github.com

googlechrome.github.io

intrinsicsize はブラウザに画像のオリジナルの寸法を無視し, この属性で指定された寸法であると見せかけるよう指示する属性です *2. ブラウザはこの属性で指定された値を画像のオリジナルの寸法としてレイアウトの計算を行います. これにより, ブラウザは画像のダウンロード前に属性値から画像の寸法 (およびアスペクト比) を算出し, レスポンシブな画像であっても適切な余白を確保できるようになります.

例えば200x100の画像を読み込む img タグには以下のように intrinsicsize="200x100" と記述すると, Layout Shift の発生を抑えつつ レスポンシブに画像を表示することができます.

<div>
  <h1>ページタイトル</h1>
  <img
    class="image"
    src="https://placehold.jp/200x100.png"
    intrinsicsize="200x100"
    alt="画像"
  />
  <p>パラグラフ</p>
</div>
div {
  box-sizing: border-box;
  border: 4px solid red;
  width: 400px;
}
.image {
  box-sizing: border-box;
  border: 4px solid #333;
  width: 100%;
  height: auto;
}

intrinsicsize 属性の問題点

しかし, intrinsicsize 属性には 2 つの問題点があります. 1 つは画像のオリジナルの寸法を取得する API (naturalWidth / naturalHeight) に影響を与えてしまうことです. これは属性の仕様上妥当な挙動ではあるのですが, そうした API に依存しているコードの挙動を変えてしまう恐れがあります. もう 1 つは width / height 属性と名前が紛らわしいことです. intrinsicsize 属性は画像オリジナルの寸法を意味する属性ですが, 歴史的には width / height 属性が画像オリジナルの寸法を表していたはずです.

width / height 属性による解決策

そこで intrinsicsize 属性の代替として提案されたのが WICG/intrinsicsize-attribute#16 (以下, 本提案と呼びます) です.

github.com

本提案は CSS により上書きされ無視されていた HTMLの width / height 属性をアスペクトの算出に利用します. intrinsicsize 属性のように画像オリジナルの寸法を指定すると, それを元にブラウザが画像のアスペクト比を算出し, レイアウトを計算します.

Issue ではこれは以下の CSS がブラウザのデフォルトのスタイルシートに追加されるものとみなせると紹介しています.

img {
  aspect-ratio: attr(width) / attr(height);
}

実際には上記の CSS は現在の仕様やブラウザの実装では利用できない機能 *3 *4 を使っており, 本提案の仕様とも厳密には異なっています (詳しくは後述). しかし本提案の概念を説明するための擬似コードとしては良くできているため, 本記事でもこの擬似コードを使って説明することにします.

例えば, この擬似コードからは以下のような特徴が分かります.

  • アスペクト比は既存の width / height 属性から算出されます
    • 古来の Web から存在する慣習に沿うだけで, Layout Shift を抑えられます
    • セマンティクス的にも優れています
  • アスペクト比の事前算出を指示するような特別なマークを付ける必要はありません
    • 上記の CSS はブラウザのデフォルトのスタイルシートに追加されるため, 全ての img 要素に自動的に適用されます
  • 画像のオリジナルの寸法を取得する API に一切影響を与えません
    • ただアスペクト比が width / height 属性から算出されるだけで, 画像のオリジナルの寸法を取得する API に影響はありません
  • 馴染みのある機能や既に標準化団体で議論されている機能から成り立っています
    • aspect-ratio プロパティや attr() 関数, width / height 属性を組み合わせているだけです
    • intrinsicsize のような新たな属性の導入は一切ありません

つまり, 馴染みのある機能などを元に intrinsicsize 属性で問題とされていたことを解決しつつ, Layout Shift を解消している訳です. めっちゃすごい. すごくないですか? 僕はすごいと思いました. とはいえ本提案がすごいのはこれだけに留まりません. Breaking Changes や前方互換性といった面でも優れた仕様を持つ提案となっているのです. どういうことか詳しく見ていきましょう.

Breaking Changes

intrinsicsize 属性と提案とは異なり, 本提案では新規に属性を追加せず, 既存の属性をアスペクト比の算出に利用します. そのため, width / height 属性を使っている要素の挙動が変わる可能性があります. つまり, Breaking Changes が発生する可能性があります. 本提案の導入により予期せぬ表示の崩れや挙動が発生する可能性があることを考えると, どのような Breaking Changes が発生するか知っておくことは重要です.

しかしそれほど心配する必要はありません. 本提案は画像がレスポンシブで表示される場合に, 属性を余白の寸法の決定に利用するものです. つまり, width="200" height="100" のように固定値で要素の寸法を指定している場合や, width="100%" height="100%" のように親要素の寸法から要素の寸法が分かる場合の挙動は変わりません. これらは元からダウンロード前に余白の寸法を知ることができており, Layout Shift も発生していません. Breaking Changes はありません.

また, 本提案は CSS プロパティで上書きされ, 無意味になっていた HTML 属性をアスペクト比の算出に利用するものです. そのため, どちらか片方を指定している場合の挙動は変わりません. CSS には width: 100%; height: auto; が指定されているものの, HTML には width 属性も height 属性もない, といった場合の挙動は変わりません. このケースでは本提案であっても Layout Shift は発生しますが, Layouyt Shift 自体は元より発生しています. Breaking Changes はありません.

ではどういう時に Breaking Changes が発生するのでしょうか. 1 つは width / height 属性に正しいアスペクト比が設定されていて, CSS でレスポンシブに画像を表示しようとしている時です. この場合, ブラウザは width / height 属性を元にアスペクト比を算出し, 余白の寸法の計算に用います. よって, このケースでは本提案により Layout Shift が発生しなくなります. これは Breaking Changes です.

しかし, 元々発生していた Layout Shift が, 本提案により発生しなくなるのでこれは良い Breaking Changes と言えるでしょう. そもそもこれが本提案の目的ですからね. とはいえ画像の読込中の HTMLImageElement.height に依存して何かをしているページでは, この Breaking Changes の影響を受ける可能性があります. 例えば, width / height が 0 であることを利用して画像が読込中かどうかを判定しているページは, この提案の導入により壊れてしまうでしょう. とは言っても画像の読込中の width / height に依存しているコードはそうそうないはず*5で, 代替手段が存在する場合もある *6 ので, Breaking Changes の影響は僅かなはずです.

もう 1 つ, Breaking Changes が起こるケースがあります. それは width / height が不正なアスペクト比で, width / height プロパティに auto が指定されている時です.

See the Pen oNjaQVr by mizdra (@mizdra) on CodePen.

例えば, 上記のようなページがある時, ブラウザは以下のようにページをレンダリングします.

  • 従来
    1. 400x0 の余白を確保した状態でページが表示されます (HTML 属性が無視されるため)
    2. 画像がダウンロードされると, 400x200 で画像が表示されます (画像からアスペクト比が算出されるため)
  • 本提案
    1. 400x400 の余白を確保した状態でページが表示されます (HTML 属性から間違ったアスペクト比を算出して利用するため)
    2. 画像がダウンロードされると, 400x400 で画像が表示されます (前段の間違ったアスペクト比を再利用するため)

1, 2 どちらの段階でも Breaking Changes が発生していますが, 1 は元から壊れていたものの壊れ方が変わっただけで, 多くの場合問題にはならないはずです. 一方 2 は従来は正しい寸法で表示されていた画像が本提案では間違った寸法で表示されてしまっています. これは良くない Breaking Changes です. 不正なアスペクト比が width / height で指定されている箇所はそうないとは思いますが, Breaking Changes が起きないことに越したことはないでしょう.

そこで本提案では 1 の段階では aspect-ratio: attr(width) / attr(height); に基づきアスペクト比を計算し, 2 の段階では画像オリジナルの寸法からアスペクト比を計算するよう拡張が施されています. これが先程チラッと言っていた擬似コードと挙動が異なる部分です. この工夫のおかげで width / height が不正なアスペクト比であっても, Breaking Changes が発生しないようになっています.

前方互換性

しかし, 実際にこのテクニックを使用するにあたって気になってくるのは前方互換性でしょう. ページを訪問するユーザの環境は様々であり, 新しいブラウザを使用する人もいれば, 古いブラウザ (IE11 など) を使用する人もいます. そのため古いブラウザで本提案を使用したページが壊れないか (前方互換性はあるのか) という点が重要になってきます.

これは Web に限らない話ですが, 前方互換性を保ったまま機能追加を行うには古い環境で無視される方法で実装するのが常套手段です. 例えば intrinsicsize 属性は新規に属性を追加する方式を採用しています. ブラウザは未知の属性を無視するようになっているため, 古いブラウザでは単に無視され, 前方互換性が保たれています.

一方, 本提案は古いブラウザにも実装されている属性を利用します. これは一見すると前方互換性がないかのように見えますが, そうではありません. というのも, 本提案は HTML 属性と CSS プロパティ両方を指定してはじめて機能します. 両方が指定されている場合はブラウザは CSS プロパティの値を実際の img 要素のレンダリングに使うため, 古いブラウザでは HTML 属性は無視されます. そのため本提案は前方互換性が保たれており, 古いブラウザのアクセスがあるページでも安心して本提案を使用できます.

標準化と実装

2019/10 に WHATWG が本提案を承認し, 標準化しています.

また, Chrome, Edge, Firefox, Safari 全てのメジャーブラウザで実装が始まっています. Safari については Safari TP 99+で実験的機能として実装されている状態ですが, その他のブラウザについては安定化され (Chrome 79+, Edge 79+, Firefox 69+), 既に Stable で利用できる状態になっています.

開発者が取るべき対応

Breaking Changes の影響を確認する

本提案は既にブラウザに実装されています. つまり, 上記で説明したような Breaking Changes が既に発生しています. そのためまずはあなたの関わっている Web サイトを見て, Breaking Changes の影響を受けていないか確認すると良いでしょう. とはいえ上記で説明したようにレイアウト上の Breaking Changes は僅かです. あったとしても元からあった Layout Shift が無くなるか, 元からガタついている箇所のガタつき方が変わるだけです. 大きな影響を受けるのは JS で HTMLImageElement#widthHTMLImageElement#getAttribute('width') を利用している箇所くらいです. TypeScriptを使用しているなら, Find all references のような機能で当該のコードを探すと良いでしょう.

新規の img タグは width/height 属性を付けて書く

いきなり既存のコードを見ていって Layout Shift を解消するのも良いですが, まずは新規に書く img タグで Layout Shift が発生しないよう書くよう心がけることが重要です.

これからはレスポンシブな img タグを以下のように書くことが推奨されます.

<style>
  div {
    width: 800px;
  }
  img {
    width: 100%;
    height: auto;
  }
</style>
<div>
  <img
    alt="バナー"
    src="https://placehold.jp/200x100.png"
    width="200"
    height="100"
  />
</div>

レスポンシブではない場合でも, 以下のように常に width / height 属性を記述するスタイルでコードを統一するのも良いかもしれません.

<div>
  <img
    alt="バナー"
    src="https://placehold.jp/200x100.png"
    width="200"
    height="100"
    style="width: 100px; height: 50px;"
  />
</div>

これは厳格な規約であり賛否両論あるとは思いますが, Linter などで問題を検知しやすいといった利点があります. 既存のプロジェクトへの導入は難しいかもしれませんが, 1 からプロジェクトを立ち上げる場合はこの規約を採用するのも良いでしょう.

実際に JSX 内の width / height 属性が設定されていない img 要素を警告する ESLint rules を作ってみたので, 良かったら使ってみて下さい.

github.com

既存の Layout Shift を解消する

気が向いたら既存の Layout Shift を解消してみましょう. 既存のコード全てを見て直すのは大変なので, ページにアクセスした直後にユーザから見える領域 (Above the Fold) など重要なところから対応するのがオススメです.

intrinsicsize 属性のその後

intrinsicsize 属性の提案は放棄されました. 実験的に実装されていた Chrome Canary 本体からも既に実装が削除されています. 今後は width / height 属性を使用しましょう.

bugs.chromium.org

まとめ

  • 画像による Layout Shift を回避する仕組みが標準化された
  • 新たな属性を使わず既存の属性を用いて対応でき, セマンティクス的にも優れている
  • 今まで意味の無かった属性を利用することで, 前方互換性がほぼ保たれている
  • 発生する Breaking Changes はごく僅かになるよう, aspect-ratio の考えを拡張している
  • 既にいくつかのブラウザで導入され始めており, すぐにでも試すことができる

参考

*1:DevTools の Network タブから Disable cache を ON に, Throttling を Fast 3G にして意図的に回線速度を遅くすると分かりやすいです

*2:https://developer.mozilla.org/ja/docs/Web/HTML/Element/img#attr-intrinsicsize より

*3:aspect-ratioCSS Box Sizing Module Level 4 で提案されているドラフト段階のプロパティです.

*4:attr() 関数の content プロパティ以外での利用は実験的であり, 多くのブラウザではサポートされていません.

*5:一般に, 取得したいのは画像が読み込まれた後の要素の寸法であり, 壊れた余白の寸法ではないはずです.

*6:画像が読込中かどうかを判断するのであれば onload が使えます.

OK Google, 今日のゆゆ式

この記事はゆゆ式 Advent Calendar 2019 21日目の記事です. 20日目はJonah Wilsonさんのゆゆ式BDを持って星を見に行った話でした.

これはなに

Google Homeに話しかけるとオススメのゆゆ式の1コマを印刷できるIoT, 「今日のゆゆ式」です.

開発者からのコメント

普段の暮らしにゆゆ式を, そんな思いで作ってみました. 11年分のゆゆ式のコンテンツから1回につき1コマだけを, 一期一会のコマたちに思いを馳せて下さい.

どうやって作ってるの

お店で使われているようなレシートプリンタを使って, コマ画像を印刷しています. 予めリクエストを受けるとランダムでコマを選び, レシートプリンタで印刷するWebhookサーバを用意しておき, そのWebhookをGoogle Home経由で叩くようにしています. Google Home<=>Webhookの連携はIFTTTで, Webhookサーバはadnanh/webhook + ngrok で雑に作ってます. 印刷に使うコマの画像は自炊したデータをyonkoma2dataでコマごとに分割して用意しました. 本当はもっと細かく説明したいのですが, 結構複雑なことをやっていて全部説明しようとすると1冊本が出来てしまうので, 作ってみたい人は自分で調べるなりWebhookサーバのソースコードを見て頑張ってみて下さい.

github.com

スペシャルサンクス

  • えすじさん
    • 単行本の取り込み, コマの分割で大変お世話になりました
  • 三上小又先生
    • いつもお世話になっております

22日目は@Lavendelstraussさんです.

iTerm2 で `cat /dev/urandom` すると印刷ダイアログが出ることがある

皆さんは /dev/urandom と呼ばれるUnixデバイスをご存知でしょうか. /dev/urandom は一言でいうと擬似乱数を出力する疑似デバイスで, catすると以下のようにランダムなバイト列を逐次的に出力してくれます. 出力するバイトの値域に特に制限は無いため, ターミナルに印字不可能な文字が表示されたりします.

良い具合にバイト列が揃うと漢字が流れてきたり, 異国の文字が流れてきたりと眺めているだけでも結構面白いです. 皆さんも是非お試し下さい. 私は絵文字が流れてきたのを見て大喜びしてました.

ところでこの cat /dev/urandom ですが, iTerm2でひたすら動かしていると稀に印刷ダイアログが開くことがあります.

f:id:mizdra:20191204011544j:plain
印刷するともれなく異国の文字たちがお出迎えしてくれます

実は cat /dev/urandom しなくても echo コマンドで簡単に再現できます. 試しにお手元のiTerm2で以下のコマンドを実行してみて下さい.

echo '\e[i'

印刷ダイアログは出ましたか? 出た方は記念に1枚プリントしておきましょう (しなくても良いです). 印刷はともかくとして, \e[i という文字列, これは一体何なのでしょうか.

ANSI Escape Sequence

ターミナルにはANSI Escape Sequenceと呼ばれる一部の文字列をターミナルを制御する特別な文字列として扱う機能が存在し, これを用いることでプログラムからターミナルを制御できるようになっています. 代表的なANSI Escape Sequenceには文字色の変更, スクリーンの消去, カーソルの移動などがあります. vimやemacsでカーソルを移動できたり, シンタックスハイライトが効いたりするのはこうした文字列のおかげなんですね.

もちろん \e[i もそうした文字列の1つで, 「表示されている画面を印刷する」よう指示する文字列として機能します. また \e[i には, カーソルのある行を印刷するよう指示する \e[1i, 入力された文字列をプリンタへとechoするモードに切替える \e[5i, モードを元に戻す \e[4i などの亜種があります.

## Hello World が印刷される
echo '\e[5iHello World!\e[4i'

iTerm2における \e[i

元々ANSI Escape Sequenceは1978年に登場したビデオ端末「VT100」を操作するために開発された仕組みで, 現代の端末エミュレータの多くがこれをエミュレートするよう設計されています*1. iTerm2もその内の1つで, リポジトリを「VT100」で検索するとそれっぽいコードがヒットします.

実際に \e[i を処理しているコードを追ってみると, VT100CSIParser.m:673\e[ni (n は任意の数) をトークンへとパースし, VT100Terminal.m:1791でトークンと n を見て印刷ダイアログなどを呼び出しています. よくよく見るとiTerm2では \e[i \e[5i \e[4i の3つの文字列しかエミュレートしていないことが分かったりします.

端末エミュレータによって対応はまちまちのようで, MacOS標準のTerminal.appでは亜種どころか \e[i すらサポートされていませんでした. というか \e[i をサポートしているiTerm2以外の端末エミュレータはそもそも存在するのでしょうか(Macだとこの2つくらいしか動作確認できなかった). 皆さんからの情報提供をお待ちしています.

活用法

Meta+P が壊れて動かなかない時や, どうしても Meta+P を使いたくない時に活用できそうです (本当に?). ダイアログが出て邪魔という点に注目すると, CUIゲームに登場する敵キャラの妨害攻撃とかにも使えそうですね (本当に?). 真面目なケースだとターミナルブラウザにおける window.print() のエミュレートあたりでしょうか.

おまけ

ダイアログ出せると知ったら当然やりますよね.

ちゃんと対策されてました. 20へえ.

*1:https://ja.wikipedia.org/wiki/%E7%AB%AF%E6%9C%AB%E3%82%A8%E3%83%9F%E3%83%A5%E3%83%AC%E3%83%BC%E3%82%BF より. 平成生まれの人間なので詳しくは知りません.

Review AppsでFirebase Authenticationを使いたい

最近趣味で作っているWebアプリをHerokuにデプロイしてReview Appsを導入したところ, Firebase Authenticationと相性が悪くてちょっとハマったという話.

当該のアプリではFirebase Authenticationを使っていて, ログインボタンを押すとTwitterのOAuth認証画面へとリダイレクトし, 承認するとアプリへ戻りログイン状態になるというよくあるSNS認証機能がある. Firebase Authenticationには承認済みドメインという概念が存在していて, これにあるドメインを追加するとそのドメインからの認証を許可することができる. 逆に言えば, 追加されてないドメインからは認証を拒否される. 認証可能なドメインをホワイトリストで制限することで, ユーザを不正なドメインから認証させ, アクセストークンを盗むフィッシングから守ったりしてくれている. 当該のアプリは example.herokuapp.com のようなドメインで配信されているので, それと同じものをFirebase Authenticationの承認済みドメインに登録している.

が, このアプリに対してReview Appsを導入すると問題が発生する. Review AppsはP-Rごとに環境を立ち上げそれらに自動で example-pr-1.herokuapp.com のようなドメインを割り振ってくれるのだが, 当然これらのドメインは承認済みドメインとして設定されてないため, Firebase Authenticationでログインしようとしても弾かれてしまう. Firebase Authenticationのコンソールを開き, 手動で承認済みドメインに1つずつ登録していくのも手ではあるが, 面倒なのであんまりやりたくない. 困った...

とりあえずパッと思いついた解決策を書き出してみる.

  1. サブドメイン herokuapp.com ごと許可してしまう
    • hogehoge.herokuapp.com のようなドメインからのログインを許可してしまい, 誤ってログインしたユーザがアクセストークンを盗まれる可能性がある
  2. 正規表現を使い, 承認済みドメイン example-pr-*.herokuapp.com を登録する
  3. pr-*.example.app のようなドメインを持つプロキシサーバを立て, 認済みドメインに example.app を登録する
    • サーバの用意が面倒なのが難点
  4. 自動で承認してもらうのを諦め, ログインが必要な機能の検証をしたい時だけ手動でドメインを承認済みドメインに追加する
    • コードを一切変更しなくて良い
    • ただし承認済みドメインの追加, 及び不要になったドメインの削除を全て手動でやらなければならない
  5. 開発環境で利用できるログインボタンを追加する
    • 開発環境用のFirebase Authインスタンスを新規に作成し, そのインスタンスを使ってログインできるボタンを追加で設置する方法
    • 開発用と本番用の認証データを分けるというのがポイント
    • 開発用Firebase Authは開発者しか使わないので, 承認済みドメインのスコープがゆるくても問題ない
      • 開発者自身が怪しいドメインでのOAuth認証を拒否すれば良い
      • 開発環境用のFirebase Authインスタンスの承認済みドメインには herokuapp.com を, 本番用には example.herokuapp.com を設定しておく
      • こうすることでReview Appsで自動生成されるドメインからもログインでき, かつユーザを危険な状態に晒さずに済む
    • 開発環境向けログインボタンは example-pr-*.herokuapp.com では見えるようにし, example.herokuapp.com では隠すようにしておけばユーザから触られる心配もない
    • 一方で開発用と本番用で別々のuidが発行されるので, Review Appsで本番用のユーザデータを使って動作確認できないという問題がある
      • これについては 4 の方法と組み合わせてカバーできそう

3は全てを解決してくれるが多少のガッツが必要でだるい. あとあまりメンテナンスするアプリケーションを増やしたくない... 4はめちゃめちゃ素朴だけど, 個人で開発しているのだったらこれでも十分っぽい. 今回扱っているプロジェクトも個人で開発しているものなので4だけでも十分だけど, DependabotでドカドカP-Rが立ち上がったり, e2eテスト導入してみたいという事情があったためシュッと動作確認できる5も採用することにした.

実装してみた感じでは, 手作業不要でサクッと動作確認できるのは最高. が, 今どちらのfirebase authインスタンスに接続しているかを判定できるようログイン時にlocalStorageに接続先の情報を保存する必要があったりして, 持ち込む複雑さや実装の手間の割にメリットが微妙. 5を採用するかどうかは (少なくとも個人開発しているプロジェクトでは) かなりケースバイケースになりそうだなーという感想です.

ポケットモンスター・ポケモン・Pokémon・は任天堂・クリーチャーズ・ゲームフリークの登録商標です.

当ブログは @mizdra 個人により運営されており, 株式会社ポケモン及びその関連会社とは一切関係ありません.