mizdra's blog

ぽよぐらみんぐ

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

ここ一番は頑張りたいだとか、ここは力を入れてやりたいとか、そういった場面に遭遇する時が時々ある。最近だとイベントで発表する資料を作ったりだとか、卒業論文書いたりだとか、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

トランスパイラとの違い

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

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

具体例を挙げると requestidlecallbackcore-js が polyfill、babel や tsc (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 の進化に置いてかれます。

150B で動く strictly-typed な Event Emitter を作った

超軽量で strictly-typed な (強く型付けされた) Event Emitter を作りました。

github.com

背景

従来 Event Emitter は ブラウザでは EventTarget として、Node.js では EventEmitter として、それぞれ独自に実装されていました。これらは機能的には変わりがありませんが、インターフェイスの互換性がありません。そのため、両方の環境で同じ API で動作する universal な Event Emitter が欲しければ、eventemitter3 のようなライブラリを利用する他ありませんでした。しかし、最近になってブラウザで実装されている EventTarget が Node.js に実装され始め *1*2EventTarget が Universal な Event Emitter としての地位を確立しつつあります。

EventTarget はネイティブな API であるため、eventemitter3 のような 3rd-party ライブラリと比較してサイズが小さい *3 という利点があります。しかし、一方で EventTargeteventemitter3 のように dispatch するイベントの種類を制限したり、addEventListener に渡したイベント名からリスナーで受け取れるイベントの値の型を推論することができません。そのため、予期せぬ種類のイベントが dispatch される可能性を考慮してコードを記述する必要があったり、明示的に型チェックを行わなければならないといった問題がありました。

@mizdra/strictly-typed-event-target の紹介

そこで、@mizdra/strictly-typed-event-target というライブラリを作りました。EventTargetCustomEvent をベースに作られているため、とても軽量です。ES Module 形式と UMD 形式の2つのビルドが用意されていて、この内 ES Module 形式はなんとたったの 150 B (厳密にいうと 152 B) で構成されています。

使い方

FooEventMap のようなインターフェイスにイベント名とイベントの値の型のリストを記述し、createSTEventTarget に型パラメータとして渡すと、強く型付けされた CustomEventEventTarget が返り値で得られます。簡単ですね。eventemitter3 を触ったことのある人なら分かると思うのですが、EventMap インターフェイスを渡して型付けする部分は eventemitter3 を真似てます。

import { createSTEventTarget } from '@mizdra/strictly-typed-event-target';

interface FooEventMap {
  onmessage: string;
  onerror: Error;
  oninstall: undefined;
}

const [FooCustomEvent, FooEventTarget] = createSTEventTarget<FooEventMap>();

FooCustomEvent / FooEventTarget はネイティブのものと同じ API が生えていて、全く同じように使えます。ただし、FooCustomEventdetail フィールドが EventMap に応じて型付けされていたり、EventMap で定義されていないイベントの dispatch が禁止されていたりと、より強く型付けされた APIとなっています。

const fooEventTarget = new FooEventTarget();

/** `addEventListener` */

// error: Argument of type '"invalid-event"' is not assignable
// to parameter of type '"onmessage" | "onerror" | "oninstall"'.
fooEventTarget.addEventListener('invalid-event', () => {});
fooEventTarget.addEventListener('onmessage', (event) => {
  // `event.detail` is infered `string` type.
});
fooEventTarget.addEventListener('onerror', (event) => {
  // `event.detail` is infered `Error` type.
});

/** `dispatchEvent` */

fooEventTarget.dispatchEvent(
  new FooCustomEvent('onmessage', { detail: 'hello' })
);
// error: Type 'Error' is not assignable to type 'string'.
fooEventTarget.dispatchEvent(
  new FooCustomEvent('onmessage', { detail: new Error() }),
);
fooEventTarget.dispatchEvent(new FooCustomEvent('oninstall'));

152 B のトリック

実装をよく見てもらえると分かるのですが、ライブラリの大部分は型情報で、JS にトランスパイルされた時に残る部分は createSTEventTarget の実装部分だけです。createSTEventTarget も本当に素朴なことしかしていなくて、ネイティブの CustomEvent / EventTarget をただ strictly-typed な CustomEvent / EventTarget へと type assertion しているだけです。ズルいですね。

export function createSTEventTarget<EventMap extends {}>() {
  const STCustomEvent = (CustomEvent as unknown) as STCustomEvent<EventMap>;
  const STEventTarget = EventTarget as STEventTarget<EventMap>;
  return [STCustomEvent, STEventTarget] as const;
}

そしてこれをトランスパイルし、ES Modules 形式で出力したものが以下になります。これで丁度 152 B です。

export function createSTEventTarget() {
    var STCustomEvent = CustomEvent;
    var STEventTarget = EventTarget;
    return [STCustomEvent, STEventTarget];
}
//# sourceMappingURL=index.js.map

ちなみにより踏み込んだ話をするなら、minify すればもっと小さくなりますし、何なら以下のように型定義だけ使えば、Tree-Shaking が効いて 0 B になります。まあたった 152 B を嫌って長ったらしい型注釈を書きたい人は居ないはずなので、ただの面白テクニックという感じです。

import {
  STCustomEvent,
  STEventTarget,
} from '@mizdra/strictly-typed-event-target';

interface FooEventMap {
  onmessage: string;
  onerror: Error;
  oninstall: undefined;
}
const FooCustomEvent = (CustomEvent as unknown) as STCustomEvent<FooEventMap>;
const FooEventTarget = EventTarget as STEventTarget<FooEventMap>;

変更履歴

  • 2020/09/21
    • 本ライブラリが他のライブラリと比較して高速であるという旨の説明をしていましたが、実際には割と遅いほうだということが判明したため、速度に関する表記を削除しました (参考1, 参考2)
    • 当初既存の EventTarget が型安全ではないという説明がなされていましたが、これは誤りでした。実際には EventTarget は型安全であり、本ライブラリはそれをより強く型付けしたライブラリという位置づけです。そこでそれを反映すべくライブラリを strictly-typed-event-target から strictly-typed-event-target に rename しました。
    • サイズがgzip後のものを指していたため、gzip前のサイズになるよう本文や記事タイトルを書き換えました (151 B => 158 B)
    • 合わせて関数名の rename を行った結果、サイズが少し減りました (158 B => 152 B)

*1:2020/09 現在ではまだ一般ユーザに公開されていない、internal で実験的な機能です。

*2:IE11及びSafariでは一部のAPIがサポートされていません。

*3:むしろ一切バンドル不要なので、実質 0 B

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

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