これは SmartHR Advent Calendar 2020 の 22日目のエントリです。
自分の日本語ブログに書こうかと思ったのですが、Jekyll だったのを思い出して Qiita に書くことにしました。
SmartHR ではフロントエンド MTG を毎週行なっており、その中にはフロントエンド情報について共有する時間があります。
以前ブログに書いた React v17 についてその中で紹介したものでした。
今回はフロントエンド MTG で紹介したものの中から、Web Platform 関連のトピックを 2 つ紹介したいと思います。
今年は Chrome が User-Agent 文字列固定の提案をしたり Referrer-Policy のデフォルトを strict-origin-when-cross-origin に変更したり、各ブラウザの SameSite=Lax のデフォルト化、Safari の CNAME Cloaking 対応など色々とありましたね。
今回は上に比べると地味ですが image-orientation: from-image
のデフォルト化と addEventListener の AbortSignal 対応を紹介したいと思います。
image-orientation: from-image
のデフォルト化では Web Platform の挙動を変える難しさを、addEventListener の AbortSignal 対応は AbortSignal の今後のポテンシャルを感じました。
image-orientation: from-image がデフォルトに
各ブラウザが足並みを揃える形で CSS のプロパティである image-orientation のデフォルトを from-image にする変更が適用されました。
これにより、img 要素などで画像を表示する際に Exif 情報に orientation が画像に含まれている場合、orientation 情報が尊重されるようになります。
現状は下記の Issue にあるように、Canvas 内での image の扱いや naturalWidth/Height の扱いがブラウザ毎に異なる部分があります。
そのため、ユーザーがアップロードした画像をアプリケーション側で回転させたりしている場合には注意が必要です。
下記は上の Issue で紹介されている image-orientation の適用状況を一覧にしたページです。左から Chrome 87, Firefox 83, Safari 14.0.2, Safari Technology Preview 14.1(Release 117) でブラウザ毎に差分があることがわかります。
このデモの画像はクロスオリジンの画像であり、はこのあとで紹介するクロスオリジンの画像にケースに該当します。Safari Technology Preview の image-orientation: none
の結果が Safari と異なるのはそのためです。
Exif の orientation の値については、以前作成した npm パッケージで紹介しているのでそちらを参照してください。
今回は、この変更に関連した image-orientation: none
が SOP (Same Origin Policy) に違反しているという Issue の話が面白かったので紹介したいと思います。
前提として、image-orientation: none
は Exif の orientation を無視するというこれまでのデフォルトであった動作に戻すための指定です。
この Issue では
- クロスオリジンの画像は SOP により Opaque として扱われるべきであり、CORS の指定なしでは画像が持つ情報は取得できるべきではない(Canvas でクロスオリジンの画像のピクセルデータを取得する際に CORS の指定が必要だったりしますね)
- 今のブラウザでは Exif の orientation 情報を元に画像を配置するのがデフォルトの状態である
-
image-orientation: none
を指定すると Exif の orientation 情報を無視することができ(デフォルトの挙動を変える)、これはクロスオリジンの画像に対しても適用できる - デフォルトで適用される Exif の orientation が無視されることは、Opaque な Exif の orientation に対して影響を及ぼしていて、これは情報がリークしている
という指摘がされています。
元々は HTMLImageElement から orientation 情報を取得できるようにしたいという提案から始まっています(2 個目の Issue)。すでに naturalWidth/Height は取得できるのでそれと同じという意見もありますが、他の metadata のリークにつながる可能性があるとされています。
これに対する対策としてはクロスオリジンの画像リソースに対しては image-orientation: none
を無視する(= 常に from-image として扱う)という方法があります。
Safari はすでにこの方法で実装済みで、他のブラウザでは Issue はあるけどこれからどうするか検討という段階です。
画像を CDN から別のオリジンとして配信している構成は一般的であり、実際に影響を受けるサイトもあるため、慎重に進めるべきという意見も出ています。
- Safari: https://trac.webkit.org/changeset/268249/webkit
- Firefox: https://bugzilla.mozilla.org/show_bug.cgi?id=1655598
- Chrome: https://bugs.chromium.org/p/chromium/issues/detail?id=1110330
そのため、将来的には image-orientation: none
を指定していても Exif の orientation に応じて回転するようになるため、none を指定しているプロダクトでクロスオリジンの画像を扱う場合は注意が必要です。
最初、Issue のタイトルを見た時に意味がわからず、image-orientation: none
でそんな影響があるとは全く頭にありませんでしたが、新しいものを仕様に追加するのは難しいという学びがある話でした。
AbortController によるキャンセル処理
Chrome88 の beta で EventListener に対する AbortController/AbortSignal 対応が入りました。
これは addEventListener でイベントハンドラを登録する際に AbortSignal を渡すことで、AbortController によるイベントハンドラの登録解除ができるようになる機能です。
// AbortController を作る
const controller = new AbortController();
// signal を渡す
element.addEventListener('click', handler, { signal: controller.signal });
// :
// 解除
controller.abort();
removeEventListener だと関数自体の参照を保持しておく必要がありましたが、これだと Controller を保持しておけばいいので楽ですね。
AbortController によるキャンセルはすでに fetch API で使えるようになっているため馴染みがあるかも知れません。
またこの AbortController を使ったキャンセルの仕組みは fs や stream 、child_process といったNode のコアモジュールでの対応が進んでいます。
- https://github.com/nodejs/node/pull/35993
- https://github.com/nodejs/node/pull/35911
- https://github.com/nodejs/node/pull/36431
- https://github.com/nodejs/node/pull/36308
また、AWS の SDK が AbortController を使ったキャンセルの仕組みを提供していたりと、今後キャンセルのためのインターフェイスとして AbortController の利用がブラウザ、Node、ライブラリ問わず増えていくことが予想できます。
React でも新しい Cache コンポーネントの実装の中でキャンセルの仕組みとして AbortSignal が言及されていたりします。
addEventListener の話に戻ると、AbortSignal を複数のイベントハンドラに対して渡すことで複数のイベントハンドラをまとめて解除することができます。
複数のイベントハンドラをまとめて解除する仕組みは、過去にも議論があり違った形の仕様も提案されていたのですが、今回の AbortSignal 対応によって解決されました。
過去の Issue:
- https://github.com/whatwg/dom/issues/208
-
https://github.com/whatwg/dom/pull/469
- こちらは具体的な
group
というまとまりを作ってまとめて解除しようという提案
- こちらは具体的な
上のような疑問を Tweet したら答えてくれて Close してくれました。
React や Vue などを使っていると意識することは少ないと思いますが、View のライフサイクするに応じたイベントハンドラの削除はコードが煩雑になったり解除忘れが発生しやすいのですが、AbortSignal を使うことでシンプルに実現できるようになります。
(7〜8 年前くらいに欲しかった…)
React Hooks で書くとこんなイメージです。
const App = () => {
useEffect(() => {
const controller = new AbortController();
const { signal } = controller;
document.addEventListener('keydown', () = {}, { signal });
document.addEventListener('keyup', () = {}, { signal });
document.addEventListener('keypress', () = {}, { signal });
return () => {
controller.abort();
};
}, []);
return // ...;
}
addEventListener が AbortSignal に対応したというだけを見ると小さな話ですが、今回紹介したように今後 Web Platform でのキャンセルのインターフェイスとして AbortController/AbortSignal が使われるきっかけになるようなことを示唆する機能追加にも感じられます。
またライブラリなどでキャンセルのインターフェイスを考える際、いくつかインターフェイスとして選択肢が考えられますが、今後はその中に AbortController を使った形も選択肢として持っておくことは Platform と一貫した API を提供するためにも重要になります。
また、イベントリスナの一括解除が欲しいという Issue がこのような形で解決されるのも面白いなと感じました。
今回紹介したものは、2020 年の Web Platform の変化としてそこまで大きなものではないですが、少し掘り下げてみると色々と面白い部分があることを感じてもらえたら嬉しいです。
余談ですが、今年の 6 月からは毎週フロントエンド MTG に参加できることになりネタ集めが大変な時もあるのですが、単に新しいライブラリや API の紹介ではなく Web Platform やエコシステムの流れ、難しさを共有できたらいいなと思って毎週準備しています。
明日は diescake さんです!