mizdra's blog

ぽよぐらみんぐ

スナップショットテストの向き不向きについて考えてみる

ふとスナップショットテストってなんだろう、どういう場面で向いていて、どういう場面には向いていないんだろうと考える機会があって色々調べてました。丁寧な記事にしようとしたのですが、上手くまとまらなくて挫折してしまった… とはいえこのまま手元に置き続けておくのも勿体ないので、下書き段階のものを公開して供養します。

スナップショットテストとは

スナップショットテストとは、あるプログラムの出力を以前の出力と比較し、両者に差分があるかをテストする手法のことです。予め以前のバージョンのプログラムの出力 (スナップショット) のどこかに保存しておき、新しいバージョンのプログラムの出力と比較し、差分があったら fail させます。これにより、プログラムの出力内容が予期せぬうちに変わってしまっていた場合に気づくことができます。

例: React コンポーネントのテストへの適用

代表的な利用例が Jest を使った React コンポーネントのテストです。

// Jestのドキュメントから引用
// from: https://deltice.github.io/jest/docs/ja/snapshot-testing.html
import React from 'react';
import Link from '../Link.react';
import renderer from 'react-test-renderer';

it('renders correctly', () => {
  const tree = renderer.create(<Link page="http://www.facebook.com">Facebook</Link>).toJSON();
  expect(tree).toMatchSnapshot();
});

この例では、<Link>コンポーネントの render 結果をシリアライズし (const tree = renderer.create(...))、その結果をスナップショットとして保存・比較するよう Jest に指示しています (expect(tree).toMatchSnapshot())。このテストを初めて実行すると、ファイルシステムに以下のようなスナップショットファイルが作成されます。

exports[`renders correctly 1`] = `
<a
  className="normal"
  href="http://www.facebook.com"
  onMouseEnter={[Function]}
  onMouseLeave={[Function]}
>
  Facebook
</a>
`;

2 回目以降テストを実行する際は、このスナップショットファイルとの差分が計算されます。もし<Link>コンポーネントの実装にミスがあり、予期せぬうちに render 結果が変わってしまった場合はスナップショットに差分が発生し、テストが fail するようになります。

スナップショットテストに対する一般的な理解

これは個人的な意見ですが、スナップショットテストが React コンポーネント以外にどんな場面へ適用可能なのか、というのはあまり知られていないように思います (僕も含めて)。スナップショットテストは非常に優れた性質を持っており、様々な場面へ適用できる有用なテスト手法です。しかしスナップショットテストがどういった特性を持っていて、どういう向き不向きがあるのか、といったことはほとんど知られていません。ここぞという時に使えるよう、使い時くらいは分かっていると便利ですよね。

どういう場面では向いているか

という訳でここからは、スナップショットテストがどういった場面で有用なのかが分かるよう、スナップショットテストの向き不向きについて考えてみます。ざっとスナップショットテストが向いている場面について書き出すと、こんな感じでしょうか。

  • 出力に差分が出たことを検知したい時
    • 差分があった時に fail して警告ほしい、どういう差分が出たのか教えてほしい、みたいなケースを想定
  • 有意な出力を出せる時
    • もうちょっと噛み砕くと、出力にテストしたいことが埋め込まれていて、かつ人間の目に読みやすい形になっている時
    • 例えばコンパイラが出力するコンパイルエラーのメッセージにて、エラーの発生行数が期待通りに印字されているかをテストしたい、みたいなケースを考えます
      • この場合はターミナルに出力されたエラーメッセージに発生行数がerror: index.ts:10:3みたいな形式で埋め込まれているので、ターミナルへ出力されたテキストををそのままスナップショットとして取れれば良いです
      • 逆にエラーメッセージが何件出たかをこれでテストするのは難しいです
        • 沢山エラーメッセージが出ると巨大なスナップショットが生成されてしまいます
        • 人間の目で数えることになって大変
    • 素朴にexpect(typeCheck(program).errors.length).toEqual(100)のような assertion を書くくらいで十分なはず
  • テスト対象がテスタブルなインターフェイスを公開してくれていない時
    • Virtual DOM など
      • <TodoList items={items} />が render された結果に items がitems.length個含まれているかをテストしたい、みたいなケースを想定
      • jsdom をインストールして DOM API を使ってテストしようと思えばできなくはないです (expect(document.querySelectorAll('.item').length).toEqual(5);)
      • けど Virtual DOM をそのままシリアライズしてそのスナップショットを取ったほうがずっと簡単にテストできます
    • スナップショットテストであれば出力さえ取れればテストできるので、テスタブルなインターフェイスが無くても何とかなる可能性があります
  • テストがない朽ちたコードを触る時
    • 触ろうにもテストが無いのでまずはテストを追加したい / けどテストを追加しようにもユニットテストを差し込めるようなインターフェイスになっていない…みたいな状況を想像してください
    • こういう時スナップショットテストが役に立ちます
    • ロガーを用意して、そのロガーを使ってアプリケーションのあらゆる場所でデバッグ出力をして、その出力をスナップショットテストする、というもの
      • 何かの拍子でログ出力の順番が変わったり、出力の内容が変わった時に気づくことができます
      • アプリケーションコードにはログ出力するコードを差し込むだけで、インターフェイスを全く変更する必要が無いので安全かつ簡単に導入できます
    • Golden Master と呼ばれている手法らしいです

どういう場面では向いていないか

逆にどういう場面では向いていないかというと…

  • 上記のメリットを殆ど享受できない場合、スナップショットテストは向いてません
    • そんなの当たり前では、と思うかもしれないけど、この観点は意外と重要だったりします
    • というのもこういうケースの場合、他のテスト手法を採用したほうが適切な場合が多いため
    • 例えば 2 つの整数を受け取ってその和を返すadd関数のテストがしたくて、以下のようなテストを書いたとします
    • さて何が問題でしょうか
    • まず答えが何になるべきなのかスナップショットテストからは分かりません
      • パッと見では3になるべきですが、正確なことはスナップショットファイルを見に行かなければ分かりません
    • また、スナップショットテストにしてしまうと、正解のデータを書き換えるには一度テストを走らせる必要があります
      • expect(add(1, 2)).toEqual(3)ならexpect(add(1, 3)).toEqual(4)と書き換えるだけで良いはずでした
    • ...というようにテストとしては動いているけど、なんか過剰な感じがしてしまう
    • こういう場合は素朴に数値として assert したほうが、テストが落ちた時にデバッグしやすいし、テストが読みやすいし、スナップショットファイルも要らなくなってシンプルになります
    • 小学校レベルの知識で解けるのであれば、大学レベルの知識を使って解くのではなくてそっちを使って解きましょう、みたいな
    • ゴブリンを倒すのであれば大量に MP を消費する大魔法を使わずとも、錆びた剣を振りかざすので十分なはず
    • 乱用することなく、状況に応じて道具を使い分けるという話
  • 出力の差分が巨大になる場合、スナップショットテストは向いてません
    • 例: 2 京行の差分が出る時
    • 例: Visual Regression Test で、5GB の画像が出力される時
    • テストフレームワークが処理するのに時間が掛かるし、最悪の場合クラッシュします
  • 出力の差分を見て、その妥当性を判断するのが困難な場合、スナップショットテストは向いていません
    • 例: 5 万行の差分が出るテスト
    • 1 分で 50 行見たとしても見終わるのに 1000 分掛かります
      • 9 時に出社して差分を見始めて、お昼過ぎてもまだまだで、定時過ぎてもまだまだで、終電が無くなった頃にようやく見終わる、みたいなスケール
      • 機械より人間が頑張るスタイル
    • ただし差分の内容を見ずに変わったことだけ分かれば良い場合や、最初の 100 行だけ見て妥当ですねと判断できる場合であればこの限りではないです
  • 出力の変化が激しい場合
    • 例: 現在時刻を表示するクロックコンポーネントのテスト
      • テストを実行する時刻によって出力が変わって毎回テストが fail します
      • 毎回人の目でチェックしないといけません
      • 機械より人間が頑張るスタイル 2
      • ただし何らかの手段で出力を固定できれば、問題を回避できる場合があります
        • クロックコンポーネントの例では時間を司る API をモックして時刻を固定すれば良い
        • Visual Regression Test なら変化する部分を黒塗りしてから比較すれば良い
        • このような出力の固定化が困難な場合はスナップショットテスト以外のテスト手法を検討しましょう
    • 例: アプリケーションのソースコードそのもののスナップショットテスト
      • ソースコードを 1 文字書き換えたらテストが fail する、みたいな
      • 誰もそんなテスト書かないと思いますが…
      • 良い具体例が思いつかなかった!本当はちょっとどこかを弄ったら毎回 fail するのは良くないよね、みたいなことを言いたかった。

事例

今までで見つけた利用例などを書いてみます。

その他使えそうなところ

以上の情報を元に他にどういう場面で使えそうか、というのを考えてみます。

  • nginx が返すレスポンスのテスト
    • ある URL にリクエストを飛ばした時に、期待されるヘッダや body から構成されるレスポンスが返ってくるかをテストしたい、みたいな
    • ちゃんと期待するヘッダが付いてるレスポンスが返ってくるかなどをテストできるはず

自分を鼓舞する言葉を思い出しながら文章を書く

ここ一番は頑張りたいだとか、ここは力を入れてやりたいとか、そういった場面に遭遇する時が時々ある。最近だとイベントで発表する資料を作ったりだとか、卒業論文書いたりだとか、1つの技術をひたすら深堀りしていく記事を書いたりだとか。けどそういったものを成し遂げるのは本当に大変で、途中で何度も挫けそうになる。そこで挫けずにモチベーションを保ったまま進めるように、自分を鼓舞する言葉というのを集めていて、それを執筆中に思い出すようにしている。文章書いてる時に思い出す言葉を中心にリストアップしたけど、いくつかは普遍的な言葉なので、文章書く時以外でも使えるはず。

折角やるんだからより良くやろう/丁寧にやろう

折角文章書いたり発表するのだから全力でやろう。折角取り組むチャンスを得たのだから、その機会を大切にしよう、という考え。

これだと勿体ないよ

自分がしたことがちゃんと伝わるように書こう、折角面白いことしたのにそれが伝わらないのは勿体ないよ、という考え。元々学部の時に居た研究室の指導教官がよく言っていて、良い言葉だなと思ったので研究室を離れた後もずっと使っている。

言葉はごまんとあるのだから、今よりも良い表現がきっとあるはず

文章を推敲する時に思い出す言葉。何かイマイチな文章があるけど、どうやって書き換えたら良くなるのか全然思いつかない時に使う。

答えが見えていないものを推敲するのは多くの場合勇気が要る。いざ推敲にチャレンジしても対して良い文章にならないかもしれないし、もしかしたら悪化するかもしれない。どれだけ考えても良い表現を見つけられず、時間を浪費するだけ (そして締切までに残された時間が短くなるだけ) かもしれない。

そういう時によくこの言葉を思い出して、きっとより良い表現はあるので、恐れずにチャレンジしようと言い聞かせている *1

ここは重要なところなので丁寧に書こう

ここを丁寧に書けば読者の理解がより深まって、後続の文章がより効果的に理解されるようになってお得、という考え。とりあえずコスパが良い箇所なので最優先で取り組もう、という気持ちになれる。

これちょっと捻ってみませんか => もうちょっと捻ってみませんか => もう1回くらい捻りませんか => ラストもう1回

捻れば捻るほど面白くなるのでどんどん捻っていこうという考え。辛い作業なのでゲーム感覚で口ずさんでいくと気が楽になって良い。


自分を鼓舞する言葉に込めた本意

一見するとこれらの言葉は自分をキツく縛り付けるような厳しい言葉のように聞こえて、ついつい無理をしなければ!と考えてしまいがちだけど、個人的には是非その真逆の捉え方をしてほしいと思っている。

重要なことは、これらの言葉を使うのは苦しい思いをして良い文章を書くためのものではなく、気楽に良い文章を書くためのものであるということ。だから言葉も厳しいものというよりは、ポジティブで背中を押してくれるような表現から構成するようにしている。執筆作業の辛さを少しでも和らげるようなまじないを掛けることで、無理なくより良い文章を書くことができる。それがこれらの言葉に込めている本意です。

皆さんもオススメのまじないがあったら教えて下さい。

*1:実際に歯を食いしばって推敲するとより良い文章になるので、ただのまじないではないと思っている。

2020年ブックマークランキング

年間ブックマークランキングジェネレーター で作ってみた。手軽に1年を振り返れて便利。8位は去年に投稿した記事だけど何故かランキングに入っている。まあ気にしないことにします。

mizdra's blogの2020年ブックマークランキングベスト8(累計600ブックマーク)

# タイトル
1位 画像による Layout Shift が無くなる Web がやって来る - mizdra's blog
2位 polyfill を深堀りする - mizdra's blog
3位 趣味で創作する時は常に何かしら新しいことに挑戦する - mizdra's blog
4位 target=''_blank" な <a> タグに noopener だけでなく noreferrer も付けるべきか - mizdra's blog
5位 150B で動く strictly-typed な Event Emitter を作った - mizdra's blog
6位 Scrapboxのページ内に埋め込まれているアイコンをsuggestして挿入できるUserScript作った - mizdra's blog
7位 日向縁さんの笑い声について思いを馳せる - mizdra's blog
8位 OK Google, 今日のゆゆ式 - mizdra's blog

generated by 年間ブックマークランキングジェネレーター

日向縁さんの笑い声について思いを馳せる

この記事はゆゆ式 Advent Calendar 2020 13日目の記事です。12日目はセレーノ (@haidorenzia) さんの尻尾ファンタジアでした。

adventar.org

私事ですが最近COMIC FUZできららを定期購読するようになりました。以前は一気に読んだときの充実感が味わえない点、いちいち書店に足を運ぶのが面倒な点が嫌で単行本派だったのですが、意外と不満なく過ごせています。何よりゆゆ式が月に1回読めるのが最高です。ゆゆ式は話単位で読んでもちゃんと充実感がある。後々刊行される単行本を読んだ時に、本誌で既に読んでいたことで充実感が味わえなくなってしまうのではないか、という不安があったのですが、全然そんな事なかったです。単行本めっちゃ充実感あった… ゆゆ式の圧倒的面白さが不安を吹き飛ばしてくれました。月イチでゆゆ式が楽しめて、単行本でまた楽しめる。定期購読して本当によかった。


さて今年のゆゆ式 Advent Calendarのテーマは「日向縁さんの笑い声」です。ゆゆ式の登場人物の中でも日向縁さんは本当によく笑います。見ているとほんわかします。日向縁さんはゆゆ式の登場人物の中でも屈指の癒やしキャラですが、笑いがその癒やしを成り立たせていると言っても過言ではないと思っています。

そしてその笑い声も多彩です。「アハハッ」と普通に笑うこともあれば「アッハハハハハ!!!」と大笑いすることもあるし、「クフフッ…」みたいな笑い声もある… 本当に色々な笑い声をされるので、それを見ているだけでも楽しめます。僕は「おっ、ここではこういう笑い声なのか〜」と眺めてはなるほどと言っています。多分僕だけじゃないはず。

という訳で今回は僕が好きな日向縁さんの笑い声10選を紹介します。ちなみに訳あって集計対象は1〜10巻までです。

4巻 61ページ 左側 4コマ目

初めは最もメジャーな笑い方、大笑いです。よくゆずこのボケを受けてこの大笑いをしてくれます。 f:id:mizdra:20201213130705j:plain

4巻 102ページ 右側 4コマ目

こっちはちょっと堪えつつ大笑い。「酒のみてえ」の回です。唯さんが「そんなに…」というくらいの元気な笑いです。 f:id:mizdra:20201213130726j:plain

5巻 11ページ 左側 2コマ目 / 5巻 11ページ 左側 4コマ目

なま言ってんじゃねえの回から2コマセットで紹介。笑いを堪える縁さんに対し、ゆずこがトドメを指すシーンです。堪えてからの笑いっぷりがお気に入りです。ちゃんと縁さんの顔もいかにも「ぷはっ」と笑っていそうな感じで素敵ですよね。三上小又先生は笑い顔を書くのが上手いなあとつくづく思います。 f:id:mizdra:20201213130736j:plain f:id:mizdra:20201213132741j:plain

9巻 112ページ 左側 3コマ目

超巨大スペイン回から。頑張って体全体でうずくまって笑いを堪えているシーンです。かわいい。

f:id:mizdra:20201213132803j:plain
スキャンに失敗して黄色い線が入ってますが気にしないで下さい

4巻 70ページ 左側 4コマ目

海回の1コマです。堪え笑い…というよりは笑いすぎて死にそうになっている様子です。大変元気でよろしい。 f:id:mizdra:20201213130717j:plain

10巻 53ページ 右側 4コマ目

お米だいすき回より「ひぃ〜〜〜...」です。涙が出るくらいの笑い、こちらも良い笑いっぷりですね。 f:id:mizdra:20201213132848j:plain

5巻 105ページ 右側 1コマ目

どっちが先に寝るか選手権より1コマ。これはもっと大胆な笑いっぷりで、床で笑い転げています。あの日向縁さんなのだから床で笑い転げるくらい得意技だろう、と思っていたのですが、意外にもこのコマしかないんですよね。つまりレアです。 f:id:mizdra:20201213132751j:plain

2巻 15ページ 右側 4コマ目

こちらは2巻、初期のゆゆ式の1コマです。実は日向縁さんが声を出して笑うのは2巻のこのコマが初めてなんですよね。意外にも1巻では笑顔になったりニンマリすることはあれど、声を出して笑うことはないのです。皆さんもこういった普段と異なる視点でゆゆ式を読み返して見ると面白いかもしれません。 f:id:mizdra:20201213130624j:plain

10巻 83ページ 左側 7コマ目

最後はポンコツ料理ロボの回の1コマです。同じく堪え笑いでも、ネタを振りながらの堪え笑いです。笑いつつもちゃんとネタを振っててえらい。ゆゆ式の中でもかなり好きなコマの1つです。 f:id:mizdra:20201213132855j:plain

おわりに

かなりマニアックな話題だったと思うのですが、いかがでしたでしょうか。あ〜これこれと言いながら眺めてもらえていたら嬉しいです。他にももっと素敵な笑い声あるよ!こういう笑い声もオススメだよ!とかあれば是非教えて下さい。

f:id:mizdra:20201213155229j:plain
最後は縁さんの笑いっぷりに対するゆずこからの一言でお別れです

余談ですが好きな笑い声のコマを集める作業は、日向縁さんが登場するコマ一覧を眺めるアプリを自作してやっていました。自炊した単行本のデータを id:esuji5 さんの yonkoma2data に与えて作りました。id:esuji5 さん、いつもありがとうございます *1。お世話になりすぎているので何かしらの形でフィードバックしたい…!

私はこれを「ゆかりビューワー」と名付けました

明日の担当は id:esuji5 さんです!

adventar.org

*1:去年もゆゆ式プリンターを作る際にお世話になりました。

polyfill を深堀りする

この記事ははてなエンジニア Advent Calendar 2020 5日目の記事です。4日目は id:syou6162 さんで、数字のバラ付きを考慮して意思決定する技術でした。

qiita.com

developer.hatenastaff.com

こんにちは、id:mizdra です。今年新卒としてはてなに入社し、WebアプリケーションエンジニアとしてGigaViewerというマンガビューワーを作っています。

最近のはてな社内では「tech-future」という、様々な技術を見つめ直すワーキンググループを運営しています。この会では、ある技術についての要点をまとめるだけでなく、その技術にまつわる歴史を紐解いて整理し、その上で全体を俯瞰して将来その技術がどういう方向に向かうのかを議論し、未来を予測する手がかりを作る、といった挑戦的な取り組みをしています。既に弊社のエンジニアから「tech-future」の取り組みの一部が公開されてますので、是非読んでみて下さい。

developer.hatenastaff.com

developer.hatenastaff.com

developer.hatenastaff.com

この記事では「(Web) Frontend-Ops」をテーマとして開催された回の内、polyfill に関する説明を切り出したものです。 最初は Frontend-Ops 全体をテーマとしてまとめて記事にしようと考えたのですが、polyfill だけでも色々と面白い話が出てきたので、今回は polyfill をピックアップして紹介しようと思います。

polyfillとは何か

polyfill とは、仕様で策定されている機能や Proposal *1 を、それがサポートされていない環境でエミュレートするためのライブラリです。これにより、モダンブラウザでしか実装されてない機能が古いブラウザ(IE11など)でも動かせるようになります。例えば String#startsWithのpolyfillは以下のようになります。

// https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith#Polyfill より
if (!String.prototype.startsWith) {
    Object.defineProperty(String.prototype, 'startsWith', {
        value: function(search, rawPos) {
            var pos = rawPos > 0 ? rawPos|0 : 0;
            return this.substring(pos, pos + search.length) === search;
        }
    });
}

このように、その環境にあるAPIを組み合わせて、実装されていないAPIと互換性のあるものを作ることで、古いブラウザでも String#startsWithを呼び出せるようになります。

polyfill の由来

実は polyfill という言葉は元々あったものではなく、2009年に Remy Sharp 氏 (nodemonの作者) によって生み出された造語です。いくつもの(poly)テクニックを使用して足りない穴を埋める(fill)ことから「polyfill」と命名したとのことです。まさにピッタリの語ですね。元々 Introducing HTML5 という書籍を書くにあたって生み出された造語で、書籍を通じて次第に世の中に広まっていったとされています。当時の経緯については氏のブログで紹介されています。

remysharp.com

トランスパイラとの違い

仕様で策定されている機能やProposalをエミュレートする技術として「トランスパイラ」があります。説明だけ見るとpolyfillと似たように見えますが、ちゃんと違いがあります。

  • polyfillは関数、クラスなどのビルトインオブジェクトのエミュレートを行うもの
    • その環境にあるAPIを組み合わせて、実装されていないAPIと互換性のあるものを作る、というアプローチを取る
    • 全て実行時に行われる
    • ライブラリとして提供される
  • 一方トランスパイラは構文 (syntax) のエミュレートを行うもの
    • a ?? b(a !== undefined && a !== null) ? a : bに変換したり
    • 実行時にできないようなことはこっちでやるイメージ
    • ツールとして提供される

具体例を挙げると requestidlecallbackcore-js が polyfill、babel や TypeScript がトランスパイラに相当します。

shimとの違い

また同様に polyfill と同じように使われる言葉として「shim」というのがありますが、これもまたpolyfillとは違いがあります *2

  • polyfill はエミュレーション対象の仕様と似たようなインターフェイスを持っていて、同じように扱えるもののことを指す
    • 新しいブラウザ向けにstr.startsWith('prefix')と書いたら、それが古いブラウザでも同じように動く、みたいな
    • str.startsWithByPolyfill('prefix')みたいにpolyfillのことを意識して書く必要がない
  • shimはエミュレーション対象の仕様と同じ機能を持っているものの、インターフェイスが異なるものを指す
    • 同じことを実現できるけど、古いブラウザ向けと新しいブラウザでは別々のAPIを使ってそれを実現する、とか
    • 最近だと lazysizes が shim の良い例
      • 画像の遅延ロードをしてくれるライブラリ
      • 新しいブラウザでは<img src"..." loading="lazy">が使えるけど、古いブラウザだとJSで動的にスクロール位置を判定して実現しないといけない
      • これを良い感じにラップして<img data-src"..." class="lazyload">で遅延ロードできるようにするというのが lazysizes の仕事
      • 古いブラウザでも遅延ロードを実現する都合上、<img src"..." loading="lazy">から微妙にAPIが変わってしまっている
      • こういうのを shim と呼んでいる

polyfill の限界

requestIdleCallbackの polyfill である requestidlecallback を例に挙げてみます。requestIdleCallbackは CPU の Idle time に渡されたコールバックを実行するための API ですが、この API は Safari や IE11 では実装されていません。そのため、そうしたブラウザでもrequestIdleCallbackを使ったアプリケーションを実行するには、requestidlecallback を読み込む必要があります。しかし、この polyfill は完全にrequestIdleCallbackの仕様に準拠している訳ではありません。というのも、polyfill というものは実行環境にあるAPIを組み合わせて、実装されていないAPIと互換性のあるものを作る、というアプローチを取っています。そのため、実行環境のAPIで表現しきれないような挙動を実現することができません。例えば、requestIdleCallbackではCPUが idle になっているかを知るAPIがSafariやIE11に存在しないため、requestIdleCallbackの仕様通りに動くpolyfillを作ることができません。そこでこうしたpolyfillでは多くの場合、可能な範囲で互換性を保ち、それっぽく動くよう設計されます。requestidlecallback ではsetTimeoutを用いてコールバックが非同期で実行されるという性質は維持しつつ、idle time でコールバックが実行されるという性質は諦めるような設計となっています。

このようにpolyfillが完全に仕様に準拠しているかは、そのpolyfillがエミュレーションしようとしている機能と、ターゲットとしている実行環境によって変わってきます。polyfillがあるからと言って必ずしも全てのブラウザで同じような挙動をする訳ではない、という点を抑えておきましょう。

polyfillの歴史

現在

今後

昔と比べてブラウザの自動アップデートが当たり前になり、仕様の標準化が進んでブラウザの差異は解消されつつありますが、全てのユーザに最新のバージョンのブラウザの利用を強制するのが困難なこと、またChrome/Safari/Firefoxのようにブラウザが複数存在し、それぞれの実装に差異があることを考えると、今後もcore-jsを使っていくことになるでしょう。以前と変わらず、必要に応じてcore-jsでサポートされていない部分のpolyfillを入れていくようにしましょう。

おまけ: バンドルサイズの削減

近年 Web Vitals などの文脈で、Webでもパフォーマンスが重視されつつありますが、この話は polyfill にも関わってきます。というのも、polyfill はバンドルに含めてユーザに配信するため、polyfill を導入することでバンドルサイズが増えてしまうからです。IE11でしか使われないpolyfillがChromeやFirefoxなどでもダウンロードされてしまう、といったように、本来polyfillを必要としていないブラウザで、余計なオーバヘッドが掛かってしまう恐れがあります。

そうした問題を解決するため、次のような対策が取られることがあります。

  • browserlist+useBuiltInsで必要なpolyfillのみがbundleされるように
  • <script type="module"> / <script nomodule>でエントリポイントを分け、nomodule側にのみpolyfillを仕込む
    • これでIE11だけでpolyfillがfetchされるようになる
    • <script type="module">をサポートするブラウザの中で更に分岐させてpolyfillを出し分ける、といったことまではできない
  • polyfill.io
    • UAを見てそのブラウザに必要なpolyfillだけを返してくれる

おまけ: Proposalにおけるpolyfill

  • まだ仕様化されていない機能をお試しするためにpolyfillが用意される場合もある

おわりに

polyfill の概要の説明から始まり、 閑話休題も挟みつつ、polyfill の今と昔、それから未来について見てきました。polyfill という小さなトピックを取り上げましたが、色々な話が出てきて面白かったですね。そのうち Hatena Developper Blog でも Frontend-Ops をテーマとした記事が色々と出てくると思うので、良かったらそちらもご覧になって下さい。

そうそう、面白い話というと近々弊社のイベントで登壇して、チームにおけるフロントエンドの属人化を頑張って解消していく話をする予定です。近々告知が出る予定ですので、良かったらそちらもご覧になって下さい。 => 告知でました! (2020/12/12 追記)

developer.hatenastaff.com

明日は id:stefafafan さんです。

qiita.com

*1:標準化される前の提案段階の機能のこと。代表的なものとして、ECMAScript の top-level await などがあります: https://github.com/tc39/proposal-top-level-await

*2:こちらも先程紹介した Remy Sharp 氏の記事 が出典です。

ポケモン乱数調整Wikiという取り組みについて

この記事はPokémon RNG Advent Calendar 2020 1日目の記事です。

adventar.org

皆さんお元気でしょうか。楽しみにされていた方は大変お待たせしました。Pokémon RNG Advent Calendar、2年振りの開催となります。去年はとある問題により開催を渋々見送らさせて貰っていたのですが、今年は何とか開催に漕ぎ着けることができました。めでたい!!!また様々なメンバーによる、ユニークなコンテンツを見れると思うと楽しみですね。クリスマスまで楽しんでいきましょう。

さて、今年のオープニングは「ポケモン乱数調整Wiki」についてです。

背景

乱数調整は、個体値のポケモンを出したい、色違いポケモンを狙って出したい、珍しいアイテムを手に入れたい、などなど誰もが抱くであろう願いを実現するために編み出されました。そしてその有用性や応用性が明らかになるにつれ、乱数調整の手法も発展していきました。今日では単に珍しいポケモンやアイテムを入手するための手段としてだけでなく、乱数調整を行うこと自体に楽しさを見出し、面白おかしな乱数調整を探求する人も現れるなど、乱数調整は多くの人に、様々な角度から楽しまれています。インターネット上ではそうした人々によるコミュニティが形成され、コミュニティから日々乱数調整に関する記事やツールが多く公開されています。

しかしながら、乱数調整について何も知らない状態から乱数調整に挑戦するのは非常に難しい、という話題があります。これは単に乱数調整に関する入門資料がない、というだけでなく、インターネット上に存在する記事の多くで誤解を招く説明や正確でない説明がなされている、というのも一因として挙げられます。よく考えてみるとこれは自然なことで、乱数調整というものはコンピュータの性質 *1 を逆手に取って成り立っているので、どうしても丁寧に説明しようとすると計算機科学などへの理解が求められます。これを理解するのは多少計算機の知識がある人ならまだしも、そうでない人からしたら本当に大変なことです。加えて正確で明瞭な用語を選んで、できるだけ誤解されないような説明をするのも、高度なスキルを要求される非常に難しい作業です。そうしたことを踏まえると、インターネット上に乱数調整の入門の取っ掛かりとなる良い資料が無い、というのは仕方のないこと言えるでしょう。

とはいえ仕方がないといっても乱数調整に入門したい人からしたら困ったままです。加えて、これは乱数調整に慣れている人にとっても深刻な問題です。多少知識があったとしても、不正確な表現がどれであるのかを見抜き、自信を持ってこれは違う!と言える人は僅かです。誤った理解をしてしまい、それを元に誤った記事を書き、また誤った理解が伝播していく…というように誤解はどんどん伝播していきます。事実、インターネット上ではこうした記事が溢れてしまっているのが現状です。

どうするか

さてこれは困った、ということでコミュニティ内でも話し合いが行われ、様々な意見やアイデアが出ました。その中で良さそうなのでやってみよう、ということで動き出しているのが「ポケモン乱数調整Wiki」という取り組みです。

ポケモン乱数調整Wiki

これはポケモンの乱数調整に関する情報を集約した、コミュニティベースによって運営されるwikiを作る、という取り組みです。様々な工夫があるのですが、ここがポイントというのをいくつか書いてみます。

  • 乱数調整に関する情報を集約する
    • 従来はコミュニティメンバーそれぞれの個人ブログなどに別個に書かれていた情報を1箇所にまとめることで、ここさえ見ておけば基本的な情報は手に入る、という状態を目指します
    • 集約することで、説明に登場する用語や表現が統一され、より理解しやすい資料になることを期待しています
  • Wiki内の各記事は査読を通す
    • 記事を公開する前にその記事の内容が妥当かどうか、不正確な表現がないか、誤解を生む説明になっていないかをコミュニティメンバーがチェック・記事の修正をフォローする「査読」という仕組みを導入します
      • 識者が記事のレビューをしてくれるシステムだと思ってもらえれば良いです
    • これにより、Wiki内のコンテンツの品質がある程度保証されることを期待しています
  • 入門者向けのガイドを整備する
  • 無理なくWikiが運営できるように
    • 記事の執筆や査読などをコミュニティメンバーで賄えるよう、無理のない範囲で運用する仕組みを整えます
      • 執筆者向けのガイドを用意して簡単に記事を寄稿できるようにする、査読フローを一部自動化する、みたいなことを考えてます
      • 査読についても特定の人物に負担が集中しないよう、上手く複数人に分散されたり、何かしらの策を検討してます
      • 査読やったことないけど興味ある人に声掛けして査読者になってもらって査読者自体の人数を増やせないか、みたいなことも考えてます
    • これには記事の執筆をしやすくする、といった取り組みも含まれます
      • 査読者サイドなどで記事の書き方などをサポートすることで、誰でも無理なく記事を寄稿できるようなフローを整える予定です

今の所査読者をやってもらえそうな人や記事を書いてくれそうな人に声掛けしたり、Wikiの基盤を作ったり…という感じでまだ1コンテンツもない状態なのですが、少しずつ進めております…!Wikiを立ち上げて、査読のフローを整備して、お試しでお手本となる記事を用意して、一般のコミュニティメンバーに記事を寄稿してもらえる体制を作って…というようにまだまだやることは沢山あるのですが、今暫くお待ち下さい。また続報があればブログ等でお知らせしようと思います。その時にまたWikiを見てフィードバックを頂けたり、Wikiの立ち上げにご協力いただけると助かります。あ、もちろん記事の執筆者や査読者も募集中です!我こそはという方は是非お声がけ下さい。お待ちしてます :)

おわりに

という訳でポケモン乱数調整Wikiという取り組みについてご紹介しました。本当は今頃には皆さんから記事の寄稿を受け付けられる状態にしたかったのですが、全然作業が進んでいなくて申し訳ない!すみませんすみません… 中々まとまって作業する時間取れなくて今暫くお待ち下さい。

Pokémon RNG Advent Calendarですが2日目は担当者不在なので、次は3日目の @sub_827 さんかな? 楽しみですね。あ、もちろん2日目書きたい人がいらしたら今すぐ投稿されても良いんですよ…?

今年もPokémon RNG Advent Calendar楽しんでいきましょう!

adventar.org

target=''_blank" な <a> タグに noopener だけでなく noreferrer も付けるべきか

追記

  • 2020/10/29: モダンブラウザによって noreferrer が自動的に付与されつつある、という説明をしていましたが、正しくは noopener の間違いでした。失礼しました。既に記事の方は修正済みです。

追記終わり。


  • target="_blank" は新しいタブでリンク先を開くためのオプションです
  • しかし、これにはリンク先のページからリンク元のページの window オブジェクトにアクセスできるという、ちょっと癖のある挙動があります
  • この挙動自体は脆弱性ではありませんが、 フィッシング詐欺などに悪用される可能性がある、危険な挙動であることが知られています
  • リンクのへの rel=noopener 付与による Tabnabbing 対策 | blog.jxck.io

noopener

  • そこで一般には、target="_blank"<a> タグには noopener を指定することが推奨されています
  • また、このオプションは Spectre 対策としても利用されます
    • Site Isolation 及び Web のセキュリティモデルの更新 | blog.jxck.io
    • 今まではリンク先のページからリンク元のページへの参照があるために、互いにメモリを共有する必要があり、どうしても同じプロセスで実行しなければならず、結果として Spectre の影響を受ける可能性がありました *1
    • noopener を付けると互いのページが別々のプロセスで実行されるようになり、リンク先のページからリンク元のページのメモリ上のデータを推測することが不可能になります
  • つまり「リンク元のページの window オブジェクトの保護」 + 「リンク元のページのメモリ上に展開されているデータの保護」が noopener の役割

余談: どういう時に noopener を付けるか

  • じゃあ target="_blank"<a> タグ全てに問答無用で付けなければならないのか、という疑問が当然出てくると思いますが、実はそうでもないです
  • リンク元のページの window オブジェクトの保護や、リンク元のページのメモリ上に展開されているデータの保護が不要な時はなくても問題ありません
  • 例えば以下のようなケースでは付けなくても問題ないです
    • リンク先が same-origin なページの場合
      • 悪意のある JS から window.opener を参照されるのは困るけど、same-origin なら所有者は我々であり、悪意のあるコードはないはずなので、 window.opener を参照されても問題ない、という発想
      • 同様の理由でメモリを覗き込まれても問題ないはず
    • リンク先から window.opener をどうしても参照したい時
      • window.opener 使いたいんだったら noopener 付けたら駄目でしょ、という当たり前の話
      • けどこれからの Web は Site Isolation をどんどん推し進めていく方向に発展すると思われるので、可能な限り window.opener を使わないほうが良いです
      • 代わりに MessageChannel という代替技術を使うのが推奨されています
  • とはいえいちいち same-origin かどうかを判定するのは面倒
    • なので基本的には target="_blank"<a> タグ全てに noopener を付けておく、という規約を導入するというのが個人的にはオススメです
      • 別に付けても何ら問題ではない / 付けておくだけ得 / 思考の手間が無くなる
      • うっかり noopener 付いていない <a> タグをコピペしてきて、 フィッシング詐欺に発展してしまう、みたいなことも防げる
    • あとは COOP を有効にするのも手

余談: 自動的な noopener の付与

noreferrer

  • 同じく target="_blank"<a> タグに指定できるオプションとして noreferrer というものがあり、これも noopener と合わせて使われることがあります
  • noreferrernoopener と一緒に使われるのは、古いブラウザで window.openerundefined にするため
    • というのも、window.opener が問題視され始めた当時は、noopener をサポートしていたブラウザには限りがあったため
    • 一方 noreferrernoopener よりも古くからあるオプションで、 IE11 でもサポートされている (ただし win10 の IE11 のみ / win7〜8.1 の IE11 ではサポートしてない)
    • そのため、noopener の fallback として、noreferrer が使われることが多かったはず (はず、というのは僕が当時の Web の状況に詳しくないから)
  • しかし noreferrer には副作用があります
    • Referer header が無くなるので、リンク先からどこからユーザが流入してきたのか追えなくなってしまう
    • アクセス分析をする時に困るかも
  • 一応回避策はあることにはある
    • ?utm_campaign=... みたいなクエリを URL につけておくとか
    • けどこういうので全部カバーしきれるかは分かっていない
    • アクセス分析が重要なサービスでは安全側 (ユーザではなくアクセス分析する我々にとっての安全側) に倒して付けないほうが良いかも?
      • ユーザの安全と我々の安全を天秤にかける、みたいな

今も noreferrer を付けるべきなのか

  • アクセス分析をガリガリやっていきたいとかなら付けないほうが良さそう?
    • けど Referer header があることでどれくらいできることが広がるのか、というのはよく分かっていない
    • noreferrer を付けないと IE11 ユーザを危険な状態に晒すことになるので、慎重に検討しましょう
  • リンク元の URL が外部に流出してほしくないなら付けたほうが良さそう
    • 社内限定のサービスとか
    • YouTube の限定公開 URL とか
  • 単に noopener の fallback として使いたい場合...
    • これは結構悩ましい。サービスによって判断すると良いのではと思ってます
    • IE11 以外のモダンブラウザでは noopener サポートされているので、IE11 サポートしていないサービスでは noreferrer 付けなくて良いはず
    • 一方 IE11 をサポートするブラウザでは noreferrer を付けておくと安全
      • ただしアクセス分析をする時に困る可能性があるので、そこは頑張って天秤にかける、という形になりそう
      • 悩ましい!!!
    • この辺詳しい人に聞いてみたい

*1:では noopener 付けていないページは今危険な状態に晒されているのか、というとそうでもないです。というのも、Spectre を成立させるにはメモリを覗きたいページが同一プロセスに載っていること、高精度のタイマー API が利用可能であること、という 2 つの条件がどちらも成り立っていなければなりません。Spectre が発見されてすぐにブラウザはこの後者の API を JS で無効にしていて、原理的に Spectre を成立しないようにしています。じゃあ Spectre 成立しないなら noopener 付けなくて良いじゃん、というとそうでもなくて、window.opener があることで高精度のタイマー API をブラウザが提供することができなくなってしまいます。他にも、window.opener のセキュリティ上の懸念で実装できない API が今後出てくるかもしれません。そうした API をブロックしてしまわないよう、noopener を付けていく / window.opener を無効にする、というのが今後ますます重要になってきます。要は付けないと Web の進化に置いてかれます。

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

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