<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Subterranean Flower Blog]]></title><description><![CDATA[フロントエンドとかいろいろ]]></description><link>https://sbfl.net</link><generator>GatsbyJS</generator><lastBuildDate>Thu, 02 Mar 2023 11:12:08 GMT</lastBuildDate><item><title><![CDATA[Twitterアカウントの乗っ取りの手口と防ぐためにやっておきたいこと]]></title><description><![CDATA[ちょっといつもと毛色の違う記事書きますね。
Twitterアカウントの乗っ取りは昔から問題にはなっているのですが、いまいち纏まった対策法が書いているところが少ないなーと思っていました。最近になってまた…]]></description><link>https://sbfl.net/blog/2021/11/25/protect-your-twitter-account/</link><guid isPermaLink="false">https://sbfl.net/blog/2021/11/25/protect-your-twitter-account/</guid><pubDate>Thu, 25 Nov 2021 09:08:00 GMT</pubDate><content:encoded>&lt;p&gt;ちょっといつもと毛色の違う記事書きますね。&lt;/p&gt;
&lt;p&gt;Twitterアカウントの乗っ取りは昔から問題にはなっているのですが、いまいち纏まった対策法が書いているところが少ないなーと思っていました。最近になってまた増えてきている気がするので、なんとかしたいという気持ちがあります。&lt;/p&gt;
&lt;p&gt;私は主にウェブシステムの設計・開発を仕事にしていて、アカウント管理のセキュリティ周りの話もよくします。チェック自体は専門家に任せるのですが、やりとりをスムーズに行うために自身も最低限の知識は学ぶようにしています。&lt;/p&gt;
&lt;p&gt;普段から否応なしにセキュリティを意識せざるを得ない立場から、Twitter乗っ取りの考えうる手口や、それらに対して私達ができる対策など、簡単にまとめておきたいと思います。&lt;/p&gt;
&lt;h2&gt;長い！読みたくねえ！短くまとめろ！&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;乗っ取りの経路はパスワードと連携アプリの2種類ある&lt;/li&gt;
&lt;li&gt;パスワードを使い回すな&lt;/li&gt;
&lt;li&gt;パスワードは大文字/小文字/数字/記号を全部混ぜて20文字以上にしろ&lt;/li&gt;
&lt;li&gt;二要素認証を有効にしろ&lt;/li&gt;
&lt;li&gt;連携アプリからの乗っ取りはパスワード貫通してくる&lt;/li&gt;
&lt;li&gt;連携アプリを不用意に許可するな&lt;/li&gt;
&lt;li&gt;使ってない連携アプリは解除しろ&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;全体的な傾向&lt;/h2&gt;
&lt;p&gt;まずは攻撃者の傾向について説明します。&lt;/p&gt;
&lt;p&gt;サービス自体の不備を突くというのは現在でも有効な攻撃方法ですが、業界全体のセキュリティ意識も上がっており、特に大手サービスを突破するのは基本的に不可能です。&lt;/p&gt;
&lt;p&gt;パスワードがあまりにも弱い場合など、個人アカウントを乗っ取れることはあるので各個人による基礎的な対策は必要にはなります。ですが「Twitterそのものを攻め落とすぞ」のようなことは現実的ではありません。&lt;/p&gt;
&lt;p&gt;ではどうするのかと言うと、ターゲットの周辺を狙います。Twitterなら連携サービスを狙うとかそういうやつです。&lt;/p&gt;
&lt;p&gt;あるいは既に突破された全く関係のないサービスを利用します。サービスとしては関係なくても、もし同じアカウント名で同じパスワードを使っていれば……あとはわかりますね。&lt;/p&gt;
&lt;p&gt;このように弱い部分から段階的に侵食してく攻撃がメジャーかつ有効になってきています。マンガやアニメで見かける「敵のボスの尻尾を掴むためにその配下に情報を吐かせる」シーンと同じですね。&lt;/p&gt;
&lt;p&gt;よって最低でも全く異なる2種類の侵入経路が考えられます。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;パスワードによる乗っ取り&lt;/li&gt;
&lt;li&gt;連携アプリによる間接的な操作乗っ取り&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;この2種類の侵入経路は全く別物なので、別々に対処します。なおパスワードによる侵入をうけた場合はアカウントの支配権自体が危ないですが、連携アプリからの乗っ取りではツイートやプロフィール、フォローなどの操作しかできないので最悪でも人間関係が破滅する程度で済みます。&lt;/p&gt;
&lt;h2&gt;パスワードをわかりにくくする&lt;/h2&gt;
&lt;p&gt;パスワードをわかりにくくすることで、単純なログイン試行を防ぐことができます。大文字/小文字/数字/記号をすべて含んだ20桁以上のランダムなパスワードにしていれば、一般的には安全だと思われます。私はたいてい28桁から32桁ほどにしています。&lt;/p&gt;
&lt;p&gt;Twitterも単なるウェブサービスのひとつなので、こういった基礎的な対処は効果が大きいです。&lt;/p&gt;
&lt;p&gt;パスワードは推測困難なものにしよう！というのは一般にもよく言われますね。桁数を増やしたりランダムにするなど、できるだけ他人から推測できないようにしましょう。パスワードに英数字だけでなく記号を加えるのも有効です。&lt;/p&gt;
&lt;p&gt;手作業で複雑なパスワードを作って覚えるのは難しいので、後述のパスワード管理ツールを使いましょう。&lt;/p&gt;
&lt;p&gt;パスワードを長く複雑にすることで「とにかくパスワードを入力してログインを試みる」という古典的な手法への耐性が上がります。&lt;/p&gt;
&lt;p&gt;Twitterでは連続でログインに失敗するとアカウントに1時間ほどロックがかかる仕組みになっています。よって無限に試せるわけではないので、一定以上の複雑なパスワードを使えば直接的に突破される可能性はほぼゼロになります。&lt;/p&gt;
&lt;p&gt;また、仮にパスワードそのものが流出した場合の最も効果的な対策にもなります。流出してるのに対策も何もないのではと思うかもしれませんが、意外とこういう地味なところが効いてきます。詳しい原理は記事の末尾に付録として書きました。&lt;/p&gt;
&lt;h2&gt;サービスごとにパスワードを変える&lt;/h2&gt;
&lt;p&gt;サービスごとにパスワードを変えましょう。たとえばGoogleとPixivとTwitterではすべて違うパスワードを使うと比較的安全になります。&lt;/p&gt;
&lt;p&gt;Twitterそのものからパスワードが漏れることはほとんどなく、実際は他のサービスから流出したパスワードをそのまま使ってログインしようとすることが多いはずです。こうなると「長くて複雑なパスワード」も意味をなしません。一発で突破されるはめになるので、本人のログインと区別がつかないでしょう。&lt;/p&gt;
&lt;p&gt;サービスごとに異なった長く複雑なパスワードを使うことで、他のサービスでの流出事故に巻き込まれずに済みます。&lt;/p&gt;
&lt;p&gt;とはいえパスワードをたくさん考えたり覚えるのは難しいですよね。そういうときはパスワード管理ツールを使うのが良いでしょう。PC/スマホでのパスワードの自動生成や自動入力機能などを備えています。&lt;/p&gt;
&lt;p&gt;パスワード管理ツールでは&lt;a href=&quot;https://bitwarden.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Bitwarden&lt;/a&gt;などが有名ですね。個人なら無料で使えます。最近になって評価が上がってきていますね。&lt;/p&gt;
&lt;p&gt;私は&lt;a href=&quot;https://1password.com/jp/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;1Password&lt;/a&gt;を使っています。有料ですが、ずっと使っていて慣れているので。&lt;/p&gt;
&lt;p&gt;Chromeなどのブラウザにもパスワード管理機能はついています。ですが様々な機械やアプリで使うなら専用のアプリを入れることをおすすめします。&lt;/p&gt;
&lt;h2&gt;パスワードのリセットを保護する&lt;/h2&gt;
&lt;p&gt;パスワードリセット時に電話番号かメールアドレスが必要なように設定しておきましょう。パスワードのリセットから他人のパスワードを書き換えて乗っ取るのもよくある手法です。&lt;/p&gt;
&lt;p&gt;Twitterの設定画面から「セキュリティとアカウントアクセス」→「セキュリティ」と選ぶと下段に「パスワードリセットの保護」とあるのでチェックを入れるだけです。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;//images.ctfassets.net/7hyenra16sli/6DZHTY3J4bqJcxyC85oXyq/5283058ff1cb7d756bcfc8669262f6e4/twitter_sec_account.jpg&quot; alt=&quot;twitter_sec_account&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;//images.ctfassets.net/7hyenra16sli/78BwyS0V0SyPjIKCVaaOb3/139ba3b40973af49ebeb4e935856e3c3/twitter_sec.jpg&quot; alt=&quot;twitter_sec&quot;&gt;&lt;/p&gt;
&lt;h2&gt;二要素認証を有効化する&lt;/h2&gt;
&lt;p&gt;二要素認証を有効化するのは大きな効果があり、もはや必須レベルの対策になります。二要素認証というのは、パスワードを打ち込んだあとにSMSで送られてきた番号を打ち込んだりするアレです。&lt;/p&gt;
&lt;p&gt;パスワードの問題点として、そもそも誰が所有しているのか証明できないという難点があります。ただの文字なので当たり前といえば当たり前なのですが、盗まれると悪用され放題になる性質があるとも言えます。&lt;/p&gt;
&lt;p&gt;なのでパスワードに加え、おそらく本人しか所有してないであろう機械を使って追加の認証を行うというのが二要素認証です。現代だとスマホが認証機器としてはメジャーでしょう。&lt;/p&gt;
&lt;p&gt;Twitterの設定画面から「セキュリティとアカウントアクセス」→「セキュリティ」→「2要素認証」と進んでいきます。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;//images.ctfassets.net/7hyenra16sli/78BwyS0V0SyPjIKCVaaOb3/139ba3b40973af49ebeb4e935856e3c3/twitter_sec.jpg&quot; alt=&quot;twitter_sec&quot;&gt;&lt;/p&gt;
&lt;p&gt;2要素認証の設定画面になります。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;//images.ctfassets.net/7hyenra16sli/40WtMp18PEmYyETnwsiU1g/d4e3313a26bcc9448bb4bc1b62260bac/twitter_2fa.jpg&quot; alt=&quot;twitter_2fa&quot;&gt;&lt;/p&gt;
&lt;p&gt;テキストメッセージはスマホのSMSで認証コードが届く仕組みです。追加のアプリなどを入れたくない方はこれでいいでしょう。&lt;/p&gt;
&lt;p&gt;認証アプリは認証コードを発行するアプリです。ログインのたびにSMSが飛んでくるのが嫌いな人は認証アプリがいいでしょう。「Twilio Authy」や「Google Authenticator（Google 認証システム）」あたりが有名なのでAppStoreで探してみましょう。&lt;/p&gt;
&lt;p&gt;セキュリティキーはおそらく最も安全な手法で、USBに刺す専用のデバイスで認証を行います。実物がないと認証できないのでインターネット経由での解析は困難でしょう。「FIDO2」という規格に対応している必要があり、ただUSBに刺すだけなら3000円程度で、NFCやBluetooth対応のものは5000円ぐらいで売っています。Amazonなどで「FIDO2」で検索をかければたくさん出てきます。&lt;/p&gt;
&lt;p&gt;セキュリティキーの有名なメーカーとしてはYubicoやGoogleがあります。Yubicoは多種多様なキーを売っています。Googleは「Titan Security Key」というのを売っています。iPhoneで使用するならNFC対応のものが良いでしょう。NFC対応ならiPhoneに近づけるだけで接続できます。&lt;/p&gt;
&lt;h2&gt;連携アプリを最小限に&lt;/h2&gt;
&lt;p&gt;連携アプリの数は最小限にしましょう。また、不用意に連携アプリの許可をしないようにしましょう。パスワードによる侵入とは別の侵入経路になるので、どれだけパスワードを強くしても連携アプリからの乗っ取りは防げません。使用していない連携アプリは解除しておきましょう。&lt;/p&gt;
&lt;p&gt;連携アプリはTwitterの一部操作をあなたの代理として実行できます。あなたのフォローしてる鍵垢の友人のツイート内容を読み取ったり、あなたにかわって変なメンションを送ったりできます。これらの機能は便利に使える一方で、悪用されると大変なことになります。&lt;/p&gt;
&lt;p&gt;連携アプリが悪用されるのは大きく2パターンあります。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;セキュリティの甘い連携アプリを乗っ取り悪用する&lt;/li&gt;
&lt;li&gt;はじめから悪意を持って作られた連携アプリ&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;世の中には多くの個性的な連携アプリが溢れています。「xx診断」とか「xx占い」のようなアプリを連携した経験、一度はあるのではないでしょうか。連携アプリの制作は今ではかなりハードルが下がっており、制作者がクリエイティブな能力を発揮しやすくなっています。しかしその裏でセキュリティの脇が甘いものも多くあります。「多くの利用者がいる」「セキュリティが甘い」という条件は、悪意ある者にとって最高の環境になります。大した手間をかけずに多くのアカウントに影響を及ぼせるのですから。&lt;/p&gt;
&lt;p&gt;また、はじめから悪意を持って作られた連携アプリもあるかもしれません。「あなたの性格を診断します」のような連携アプリをとりあえずバズらせればいろんな人に連携してもらうことができます。ある程度広まってから例えば「自動でランダムなフォロワーをブロックする」なんてされたら、どうします？&lt;/p&gt;
&lt;p&gt;このあたりはTwitter側にも問題があり、連携アプリを作るときに「読み取り可能」か「読み取りと操作が可能」か「読み取りと操作とDMが可能」の3種類の中からしか選べません。全部見えるか全部操作できるかDMまで明け渡すかの究極の選択を迫られます。正気か？&lt;/p&gt;
&lt;p&gt;単に占い結果をツイートするだけのアプリでも「読み取りと操作が可能」の権限を与えねばならず、完全に気が狂った仕様です。読み取りというのは本当になんでもかんでも見えるので注意してください。なおDMの権限要求してくるやつは普通に怪しいので疑って大丈夫です。DM権限が必要なのは有志のツイッタークライアントぐらいだと思います。&lt;/p&gt;
&lt;p&gt;連携を解除するには、Twitterの設定画面から「セキュリティとアカウントアクセス」→「連携しているアプリ」を選ぶと連携アプリの一覧が出てきます。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;//images.ctfassets.net/7hyenra16sli/1CNwrRtkNcK19poo47GQfw/2569e880f1bb511637d0fc723560c243/twitter_app.jpg&quot; alt=&quot;twitter app&quot;&gt;&lt;/p&gt;
&lt;p&gt;今は使ってないアプリをどんどん連携解除していきましょう。使うときになったらまだ連携すればいいだけなので躊躇する必要はありません。「アプリの許可を取り消す」を押せば解除できます。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;//images.ctfassets.net/7hyenra16sli/22FFJU8OSSUw8eoY6qfrOf/56ad0c73995d017f21faccbb5692cbd0/twitter_app_disconnect.jpg&quot; alt=&quot;twitter app disconnect&quot;&gt;&lt;/p&gt;
&lt;h2&gt;セッションの整理&lt;/h2&gt;
&lt;p&gt;不要になったセッションも解除しておきましょう。セッションは「Twitterのログイン情報」ぐらいの意味と思っておいてください。セッションデータが漏れたときの被害はおそらくパスワードが漏れたときと同じ程度ありますが、セッション自体がサービス固有のものなので、例えばPixivのセッションが漏れてもTwitterのセッションは乗っ取れません。&lt;/p&gt;
&lt;p&gt;Twitterのセッションは無期限のようなので、接続する端末やブラウザが増えるとそれだけ増殖していきます。使ってなさそうなものは消していきましょう。&lt;/p&gt;
&lt;p&gt;Twitterの設定画面から「セキュリティとアカウントアクセス」→「セッション」を選ぶとセッション一覧が出てきます。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;//images.ctfassets.net/7hyenra16sli/1dq7hPx84c5916BTDx5MxB/e17c92f693be7e905a1fa7cdc6f03ff5/twitter_session.jpg&quot; alt=&quot;twitter_session&quot;&gt;&lt;/p&gt;
&lt;p&gt;セッションは個別に解除できますし、「他のすべてのセッションからログアウト」で全解除もできます。数が多い場合は全解除してから今使ってる端末だけでもう1回ログインしなおすのが楽です。&lt;/p&gt;
&lt;p&gt;ここでもし全く身に覚えのない怪しいセッションがあったら、即刻そのセッションを切断してパスワードを変更しましょう。&lt;/p&gt;
&lt;h2&gt;まとめ&lt;/h2&gt;
&lt;p&gt;基本的なところをしっかりおさえよう！という月並みな言葉になってしまいますね。特に最近ではTwitterのアカウントを使ってビジネス的なやり取りをする方も多いので、面倒がらずにしっかりやっていきましょう。&lt;/p&gt;
&lt;p&gt;長い人ではTwitterアカウント作成から10年以上経っていると思います。それまで積み上げてきた人間関係やくだらないやりとりが、変な宣伝アカウントに乗っ取られて終わるというのは悲しいはずです。自分のアカウント、大事にしていきましょう。&lt;/p&gt;
&lt;h2&gt;おまけ：パスワードの解析&lt;/h2&gt;
&lt;p&gt;初めの方で「大手サービスを直接攻撃するのは現実的ではない」と書きましたが、関係者を狙った攻撃や内部犯などは防ぎきれないこともあります。パスワード流出の可能性は現実的にあり得るわけです。ではそうなった場合、流出したパスワードというのはどの程度安全なのでしょうか。&lt;/p&gt;
&lt;p&gt;結論だけ言うと、流出時は公式からアナウンスがあるはずなので速やかにパスワードを変えましょう。こういうものは流出する前提で仕組みが作られているのですぐさま乗っ取られることはないのですが、数週間放置していると危ないと思います。本当に危険な場合はおそらく公式から全員強制パスワードリセットがかかります。&lt;/p&gt;
&lt;p&gt;一般にパスワードは「ハッシュ」という形式で保存されます。ハッシュというのは元のデータをめちゃめちゃに切り刻んで原型を残さないように破壊された形式です。「暗号化」がデータを後で元に戻すことを目的としているのに対して「ハッシュ化」は破壊のための破壊として行われる破滅的行為です。つまりハッシュを見てもパスワードはわかりません。ツイッターの社員が見てもわかりません。誰にもわからないんです。&lt;/p&gt;
&lt;p&gt;一見役に立たなさそうですが、「ハッシュ化」の手法を定めておくことで、「同じ入力なら必ず同じハッシュになる」という性質が得られます。これを活用するとパスワードを保存せずにパスワードの照合が可能になります。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;パスワード登録時/変更時にパスワードのハッシュだけ保存しておく&lt;/li&gt;
&lt;li&gt;ログイン時にパスワードの入力をハッシュ化する&lt;/li&gt;
&lt;li&gt;保存していたハッシュと入力パスワードのハッシュが同一か確認する&lt;/li&gt;
&lt;li&gt;同一ならば登録されたパスワードと同じことが保証できる&lt;/li&gt;
&lt;li&gt;ログイン成功&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;頭がいいですね。ちなみにデメリットとしてそもそもパスワードが誰にもわからないというのがあり、パスワードを忘れたときにリセットするしかないサービスが多いのはこのためです。逆にいうとパスワードを忘れたときにパスワードがそのまま送られてくるサービスは危険です。&lt;/p&gt;
&lt;p&gt;つまりパスワードの流出というのはハッシュの流出です。じゃあハッシュが流出してもパスワードわからないし安全じゃん。ハッピーエンド。……というわけにもいきません。&lt;/p&gt;
&lt;p&gt;パスワードを解析するとき、「ハッシュからパスワードに戻せないなら、当たるまで適当なパスワードをハッシュ化繰り返せばいい」というゴリ押しベースの手法が用いられます。あまり賢くなさそうな方法ですが、今時の家庭用グラフィックボードの進歩により、秒間で数千万回ほどなら簡単に試せるようになりました。&lt;/p&gt;
&lt;p&gt;辞書攻撃という手法もあります。これはゴリ押しするときにアルファベットの総当たりではなくよく使用される単語を組み合わせて行うものです。総当たりより圧倒的に効率がいいので、英単語の組み合わせパスワードはすぐに突破されます。&lt;/p&gt;
&lt;p&gt;また、有名なハッシュ化手法については、よくあるパスワードをハッシュ化したリストを事前に作っておけば、同一のハッシュを持つユーザを探すだけで簡単にパスワードを割り出せます。このやり方はレインボーテーブルと呼ばれます。&lt;/p&gt;
&lt;p&gt;あまり安全ではなさそうですね。もちろんこれらの弱点にも様々な対策を施します。&lt;/p&gt;
&lt;p&gt;一般的な対策として「ソルト」というものがあります。ソルトはユーザごとに付与するランダムな文字列です。ハッシュ化する際にパスワードにソルトをくっつけてから実行します。パスワードが「rhodes」でソルトが「wzuduwr」なら、「rhodeswzuduwr」にしてからハッシュ化します。&lt;/p&gt;
&lt;p&gt;こうすることで擬似的に非常に長いパスワードになるので少し安全になります。またレインボーテーブルをそのまま適用できなくなるので、一瞬でバレることもなくなります。なおソルト自体はパスワードに近い場所に格納することが多いので一緒に流出する可能性もあります。&lt;/p&gt;
&lt;p&gt;その他にもいろいろな対策をしますが、根本的にはどれも時間稼ぎです。流出が判明してからアナウンスをして、ユーザがパスワード変更するまでは安全なようにしようという作りです。&lt;/p&gt;
&lt;p&gt;つまり、強固なパスワードを使っている限り恐れる必要はありませんが、流出が判明した場合は速やかにパスワードの変更を行いましょう。&lt;/p&gt;
&lt;p&gt;ちなみに過去にはユーザ1億超えてるようなサービスでもこのあたりの対策がガバガバだったこともあるので、あまりネット上のサービスを信用しすぎず自衛できるように心がけましょう。世の中そんなもんです。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[GPD WIN3で「原神」や「PSO2:NGS」は遊べるのか]]></title><description><![CDATA[GPD WIN3を買いました。買った理由はなんとなくです。せっかく買ったので普段やってるゲームがどの程度動くのかなと試してみました。
GPD WIN3って何
小型Windowsパソコンです。ゲーム用途…]]></description><link>https://sbfl.net/blog/2021/07/19/play-games-with-gpd-win3/</link><guid isPermaLink="false">https://sbfl.net/blog/2021/07/19/play-games-with-gpd-win3/</guid><pubDate>Mon, 19 Jul 2021 11:33:00 GMT</pubDate><content:encoded>&lt;p&gt;GPD WIN3を買いました。買った理由はなんとなくです。せっかく買ったので普段やってるゲームがどの程度動くのかなと試してみました。&lt;/p&gt;
&lt;h2&gt;GPD WIN3って何&lt;/h2&gt;
&lt;p&gt;小型Windowsパソコンです。ゲーム用途を想定しているのでボタンとかスティックとかがついてます。モバイル向けCore i7とメモリ16GBという構成。昔からのガジェットオタクには「VAIO type U」といえば通じます。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;//images.ctfassets.net/7hyenra16sli/7pqBybiYd1V1teHNoSXt51/b8f16da525010fe6ea2f4b6429dbc9ee/gpd-win-3.jpg&quot; alt=&quot;gpd-win-3&quot;&gt;&lt;/p&gt;
&lt;p&gt;最新のAAAタイトルとかならともかくちょっと前の重めのゲームなら動くいいやつです。良くも悪くも単なるWindowsマシンなのでSteamとかのゲームも動かし放題です。&lt;/p&gt;
&lt;p&gt;当時VAIO type Uを買えなかった腹いせに購入しました。15年越しの復讐です。&lt;/p&gt;
&lt;h2&gt;どの程度のゲームまで遊べるのか&lt;/h2&gt;
&lt;p&gt;GPD WIN3には消費電力の設定があり、消費電力を上げると当然ながら性能も上がります。私はスペックバカなので何も考えず最高の28Wにしてます。なのでこの記事で出てくる話はすべて28W設定です。小さな筐体の中で冷却ファンが轟音立てて回ってるときがいちばん生を実感できますよね。&lt;/p&gt;
&lt;p&gt;とりあえずこの前出てたFF14のベンチマークを動かしてみます。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;//images.contentful.com/7hyenra16sli/6mATi5Werq1mAXWTsLI70y/08a0a6de89e5a5519bfa48a69a4de73d/gpd-win-3-ff14.jpg&quot; alt=&quot;gpd-win-3-ff14&quot;&gt;&lt;/p&gt;
&lt;p&gt;高品質（ノートPC）設定では平均65fpsの最低23fpsと良好な感じでした。高品質（デスクトップ）にすると平均50fpsの最低18fpsまで落ちます。最高品質だと平均47fpsの最低17fps。&lt;/p&gt;
&lt;p&gt;高品質から最高品質にしても意外とフレームレート下がらないなという感想です。これ動くならたいがいのゲーム動くんじゃない？と思いました。&lt;/p&gt;
&lt;h2&gt;原神&lt;/h2&gt;
&lt;p&gt;原神やってみました。最初はグラフィック設定は中でいけるのではないかと思ってましたが、中設定だとモンド城下町やモンド城下町前（ティミーとかがいるところ）で45fps〜52fpsぐらいをうろちょろしてたので、ちょっとカスタムして下げました。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;//images.contentful.com/7hyenra16sli/iAxseMzYPqMPX7f8ot0R1/08bb63ffde2a15a88ca0e288ab453fe3/gpd-win-3-genshin-graphics.jpg&quot; alt=&quot;gpd-win-3-genshin-graphics&quot;&gt;&lt;/p&gt;
&lt;p&gt;影品質を低に、アンチエイリアスをTAAに、サブサーフェススキャタリングを中にしました。これで58fps〜60fpsぐらいになったので戦闘系のデイリークエやってみる。&lt;/p&gt;
&lt;p&gt;&lt;video controls width=&quot;480&quot; src=&quot;//videos.contentful.com/7hyenra16sli/3a9Zw0jJE664YMyugDEtSl/d242bdefb8081b9eea0eb97d0a38a1df/gpd-win-3-genshin.mp4&quot;&gt;&lt;/video&gt;&lt;/p&gt;
&lt;p&gt;ええやん。MSIのAfterburner使って左上にfps表示してます。だいたい60fps維持してますね。螺旋とかはもしかしたらキツい可能性ありますけど、通常プレイの範疇で困ることなさそうですね。&lt;/p&gt;
&lt;h2&gt;PSO2:NGS&lt;/h2&gt;
&lt;p&gt;令和の問題児くんNGSですわよ。詳しくは省くのですが設定2まで落とさないとキツかったです。30fpsで我慢できるなら高設定ぐらいまでは行けそう。&lt;/p&gt;
&lt;p&gt;設定2でまずは過密ブロックのセントラルシティ走ってみたけど40fpsぐらいしか出てないです。&lt;/p&gt;
&lt;p&gt;&lt;video controls width=&quot;480&quot; src=&quot;//videos.contentful.com/7hyenra16sli/qhmr8IAkMJrvLmICXqCBe/73f6fe21a909e6f64d8900b6ae08202b/gpd-win-3-ngs-city.mp4&quot;&gt;&lt;/video&gt;&lt;/p&gt;
&lt;p&gt;次にマグナス山（8人マップ）に行ってみました。ちょうどPSEバーストする場面だったので適当にゾンデ連打します。50fpsぐらいでした。バーストしてないときの普通の雑魚集団ぐらいなら60fps出ます。&lt;/p&gt;
&lt;p&gt;&lt;video controls width=&quot;480&quot; src=&quot;//videos.contentful.com/7hyenra16sli/2N3UirPRBUQ5V10RMupLHT/f7ffd46273b5e45af644f2faa3d942ee/gpd-win-3-ngs-magnus.mp4&quot;&gt;&lt;/video&gt;&lt;/p&gt;
&lt;p&gt;意外と遊べるなーという印象です。設定1なら60fps行くんじゃなかろうか。&lt;/p&gt;
&lt;h2&gt;重量について&lt;/h2&gt;
&lt;p&gt;ところでGPD WIN3なんですが、結構重いです。Nintendo Switchが400gぐらいで、GPD WIN3は560gです。Switchでも重い重い言ってる人多かったので、こっちはかなりの重量級ですね。&lt;/p&gt;
&lt;p&gt;筐体の分厚さもすごくてわりと鈍器めいてます。たぶんゲームやってる最中にゾンビに襲われてもGPD WIN3で殴れば生き残れる。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;//images.ctfassets.net/7hyenra16sli/44uczjfBXXGI3x4V32v8zO/33018dc7fd39a9d478dd069726311e8a/gpd-win-3-up.jpg&quot; alt=&quot;gpd-win-3-up&quot;&gt;&lt;/p&gt;
&lt;p&gt;ちなみに先日発表されたValveのSteam Deckはさらにその上を行く670gです。貯金するより手首鍛えましょう。&lt;/p&gt;
&lt;h2&gt;これ買ったほうがいいですか&lt;/h2&gt;
&lt;p&gt;この記事読んで「原神やNGSが動くんだ！買ったほうがいいかな！？」みたいに考える人もいると思うのですが、もしいま原神やNGSが動くPCを持っていないのであればやめたほうがいいです。&lt;/p&gt;
&lt;p&gt;あくまでサブ機としての立ち位置が強く、これメインでやるのは価格的にも苦しいと思います。もし「2020年頃までのゲームしか遊ばず、これからのゲームは一切動かなくてもいい」という明確な割り切りがあるのであれば止めはしませんが。&lt;/p&gt;
&lt;p&gt;まあ高級おもちゃです高級おもちゃ。&lt;/p&gt;
&lt;h2&gt;Steam Deckどうよ&lt;/h2&gt;
&lt;p&gt;ついこの前に&lt;a href=&quot;https://www.steamdeck.com/ja/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Steam Deck&lt;/a&gt;も発表されましたね。GPD WIN3と同等（もしくはちょっと上）のスペックで価格は半分ぐらいというすごいやつです。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;//images.contentful.com/7hyenra16sli/6WqLbuYk3vE9a4nYf2yIzR/809c827f737f6571d2886519dcbbcf45/steam-deck.jpg&quot; alt=&quot;steam-deck&quot;&gt;&lt;/p&gt;
&lt;p&gt;スペックや価格などみなさんテンション上がるとは思いますが、貧弱ナヨナヨマンの私からひとつ言えることがあります。スペックや価格の前に自分の手首の筋力と相談してください。&lt;/p&gt;
&lt;p&gt;私は400gのNintendo Switchでも結構ギリギリなんですが、560gのGPD WIN3だと「うおおっ！」って感じです。Steam Deckは670gです。手首、鍛えましょう！&lt;/p&gt;</content:encoded></item><item><title><![CDATA[JavaScriptで任意のHTML要素をPicture-in-Pictureする]]></title><description><![CDATA[みなさんはPicture-in-Picture(PiP)という機能を使ったことがありますか。PiPは動画コンテンツなどを浮遊する小窓に表示する機能です。小窓はウィンドウの外側を自由に移動できます。

…]]></description><link>https://sbfl.net/blog/2021/04/30/javascript-html-picture-in-picture/</link><guid isPermaLink="false">https://sbfl.net/blog/2021/04/30/javascript-html-picture-in-picture/</guid><pubDate>Thu, 29 Apr 2021 16:33:00 GMT</pubDate><content:encoded>&lt;p&gt;みなさんはPicture-in-Picture(PiP)という機能を使ったことがありますか。PiPは動画コンテンツなどを浮遊する小窓に表示する機能です。小窓はウィンドウの外側を自由に移動できます。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;//images.ctfassets.net/7hyenra16sli/44f4CtccYNwDQ7YWz3MNr5/973d68bd8911cfbd13da4a7210d5d46e/pip-chrome.png&quot; alt=&quot;pip-chrome&quot;&gt;&lt;/p&gt;
&lt;p&gt;デスクトップが賑やかになりがちなPCでもPiPは大活躍なのですが、特にスマートフォンにおいては数少ない「ウィンドウ」機能になります。Androidはもちろん、iOS14も対応したことで話題になりました。&lt;/p&gt;
&lt;p&gt;これによってスマホ一台あれば、ソシャゲの公式生放送を見ながらソシャゲのイベントを周回する地獄のような行為が可能になりました。&lt;/p&gt;
&lt;p&gt;利用者という視点から見ると非常に便利なのですが、開発者から見ると動画しか表示できないのはなかなか使い所が難しくなります。そこで、この機能を使って好きな情報を表示できないか実験してみました。&lt;/p&gt;
&lt;h2&gt;PiP機能の対応環境&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Chrome 70&lt;/li&gt;
&lt;li&gt;Firefox 71（制限付き）&lt;/li&gt;
&lt;li&gt;macOS/iPad Safari 13.1&lt;/li&gt;
&lt;li&gt;iPhone Safari 14&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;PiP自体はどの環境も対応しています。iOS自体は13.1から対応していたのですがiPadのみ対応で、iPhoneにも機能が解放されたのはiOS14になります。&lt;/p&gt;
&lt;h2&gt;今回の目標と対象外環境&lt;/h2&gt;
&lt;p&gt;今回は好きなHTML要素をPiPしてみたいと思います。曲芸飛行みたいなことするのでちゃんと動かない、対象外の環境が2つあります。&lt;/p&gt;
&lt;p&gt;ひとつはFirefoxです。PiP自体には対応しているのですが制限付きで、JavaScript側から操作できません。なので今回は対象外となります。&lt;/p&gt;
&lt;p&gt;もうひとつはiOS Safariです。今回使う機能がバグってます。数時間格闘したんですが真っ黒な画面しか出ませんでした。&lt;a href=&quot;https://bugs.webkit.org/show_bug.cgi?id=181663&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2018年からずっと報告されてる&lt;/a&gt;んですが特に動きはないようです。&lt;/p&gt;
&lt;h2&gt;実現手順&lt;/h2&gt;
&lt;p&gt;PiPが動画にしか対応していない部分は変わりません。なのでHTMLを動画にしてやれば任意の要素をPiPできるわけですね！&lt;/p&gt;
&lt;p&gt;このような動作イメージです：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;//images.ctfassets.net/7hyenra16sli/rhc0KbJqakoGz3BI5dBaJ/56a312f968b1438bb270276f04c6855a/pip-demo.png&quot; alt=&quot;pip-demo&quot;&gt;&lt;/p&gt;
&lt;p&gt;次の手順で実現します：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;HTMLを画像に落とし込む&lt;/li&gt;
&lt;li&gt;画像化したHTMLをcanvasに書き込む&lt;/li&gt;
&lt;li&gt;canvasから動画ストリームを取得する&lt;/li&gt;
&lt;li&gt;video要素を作成してcanvasの動画ストリームを再生させる&lt;/li&gt;
&lt;li&gt;そのvideo要素をPiPする&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;理屈の上ではできそうですね。それではやっていきましょう。&lt;/p&gt;
&lt;h2&gt;HTMLをcanvasに描画する&lt;/h2&gt;
&lt;p&gt;次のようなHTMLがあるとします：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!DOCTYPE html&gt;
&amp;#x3C;html&gt;
  &amp;#x3C;head&gt;
    &amp;#x3C;meta charset=&quot;utf-8&quot;&gt;
    &amp;#x3C;title&gt;Picture in Picture&amp;#x3C;/title&gt;
    &amp;#x3C;script src=&quot;main.js&quot; defer&gt;&amp;#x3C;/script&gt;
    &amp;#x3C;link rel=&quot;stylesheet&quot; href=&quot;style.css&quot;&gt;
  &amp;#x3C;/head&gt;
  &amp;#x3C;body&gt;
    &amp;#x3C;div id=&quot;pip-source&quot;&gt;
      &amp;#x3C;img src=&quot;https://source.unsplash.com/9UUoGaaHtNE&quot;&gt;
      &amp;#x3C;span class=&quot;message&quot;&gt;Hellooooooooooooo&amp;#x3C;/span&gt;
    &amp;#x3C;/div&gt;
    &amp;#x3C;button id=&quot;pip-button&quot;&gt;PiP them&amp;#x3C;/button&gt;
  &amp;#x3C;/body&gt;
&amp;#x3C;/html&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;CSSはこうです：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;#pip-source {
  background-color: white;
  text-align: center;
  width: 320px;
  padding: 1em;
}

#pip-source img {
  display: inline-block;
  object-position: center center;
  object-fit: contain;
  width: 100%;
}

:picture-in-picture {
  display: none;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;//images.ctfassets.net/7hyenra16sli/1KIUgnl8k22uliarSkpCb9/213c0cb662826fd9f2cc079d827daf72/pip-source.png&quot; alt=&quot;pip-source&quot;&gt;&lt;/p&gt;
&lt;p&gt;このとき &lt;code&gt;#pip-source&lt;/code&gt; の要素をPiPの対象にしたいとします。これを画像化すればよいわけですね。&lt;/p&gt;
&lt;p&gt;画像化にはsvgを使います。svgには &lt;code&gt;foreignObject&lt;/code&gt; という仕組みがあり、その中の描画をブラウザに任せる、ということができます。一方でsvgはブラウザ上では画像として扱うことができ、ブラウザの描画結果を画像として扱うことができるようになります。&lt;/p&gt;
&lt;p&gt;svgを組み立てるところまで一気に行きますね。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const pipSource = document.querySelector(&apos;#pip-source&apos;);
const pipButton = document.querySelector(&apos;#pip-button&apos;);
pipButton.style.opacity = 0.3;

const setup = async() =&gt; {
  // 描画する要素のサイズを取得したい
  // 画像の読み込みが終わるまで待つ
  const waitingImgList = [...pipSource.querySelectorAll(&apos;img&apos;)];
  await Promise.all(waitingImgList.map((el) =&gt; new Promise((resolve) =&gt; {
    el.addEventListener(&apos;load&apos;, () =&gt; resolve());
  })));

  // 描画する要素のサイズを取得する
  // 注意：重い処理なので頻繁に呼び出さないように
  const { width, height } = pipSource.getBoundingClientRect();

  // svgタグを作る
  // SVGはHTMLではないので作り方がちょっと違う
  // 決められたNS(Namespace)を指定して作る
  const ns = &apos;http://www.w3.org/2000/svg&apos;;
  const svg = document.createElementNS(ns, &apos;svg&apos;);
  svg.setAttribute(&apos;width&apos;, width);
  svg.setAttribute(&apos;height&apos;, height);

  // foreignObjectを作る
  // foreignObjectはその中身の描画を
  // SVG自身ではなく外側（ここではブラウザ）に任せる
  const foreignObject = document.createElementNS(ns, &apos;foreignObject&apos;);
  foreignObject.setAttribute(&apos;width&apos;, width);
  foreignObject.setAttribute(&apos;height&apos;, height);

  // 表示したい要素のコピーを作る
  // xmlとしてのNSを追加
  const html = pipSource.cloneNode(true);
  html.style.display = &apos;content&apos;;

  // 画像をすべてDataURLに置き換える
  const imgList = [...html.querySelectorAll(&apos;img&apos;)];
  await Promise.all(imgList.map(async (el) =&gt; {
    const data = await fetch(el.src).then((res) =&gt; res.blob());
    const reader = new FileReader();
    const url = await new Promise((resolve) =&gt; {
      reader.onload = () =&gt; resolve(reader.result);
      reader.readAsDataURL(data);
    });
    el.src = url;
    return el.decode();
  }));

  // 描画に必要そうなものを作っておく
  const style = document.createElement(&apos;style&apos;);
  const css = await fetch(&apos;style.css&apos;).then((res) =&gt; res.text());
  style.textContent = css;

  // 組み立てる
  html.appendChild(style);
  foreignObject.appendChild(html);
  svg.appendChild(foreignObject);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;これ自体は簡単なのですがひとつ注意点があります。それはsvg内の画像やcssは外部ファイルであってはいけないということです。svgを直接HTMLの中に記述するならそれも動くのですが、画像として扱う場合はsvgの中で全て完結する必要があります。今回はスクリプトは使っていませんが、スクリプトを使用するならそれもインライン化が必要です。&lt;/p&gt;
&lt;p&gt;これでDOMとしてのsvgが出来上がりました。img要素に放り込めば画像として取得できるようになるのですが、残念ながらimg要素はDOMを受け付けません。なのでこれをData URLに変換します。文字列化してくっつけるだけです。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const setup = async() =&gt; {
  //
  // ↑ここに先ほどまでの処理がある
  //

  // svgを文字列化してURL化する
  const svgStr = new XMLSerializer().serializeToString(svg);
  const svgUrl = &apos;data:image/svg+xml;charset=utf-8,&apos; + encodeURIComponent(svgStr);

  // svgを画像として読み込む
  const img = new Image(width, height);
  img.src = svgUrl;
  await img.decode();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;あとはこれをcanvasに転写します&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const setup = async() =&gt; {
  //
  // ↑ここに先ほどまでの処理がある
  //

  // canvasを作る
  const canvas = document.createElement(&apos;canvas&apos;);
  const ctx = canvas.getContext(&apos;2d&apos;);
  canvas.width = width;
  canvas.height = height;

  (function render() {
    ctx.clearRect(0, 0, width, height);
    ctx.drawImage(img, 0, 0, img.width, img.height);
    requestAnimationFrame(() =&gt; render());
  })();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;canvasに対して &lt;code&gt;drawImage&lt;/code&gt; するだけですね。ここで &lt;code&gt;requestAnimationFrame&lt;/code&gt; している理由なんですが、drawImageは静止画の転写になるのでこうしないとsvgにCSSアニメーションなどが入っていた場合に反映できないというのと、あとはなぜかこうしないとSafariが描画してくれなかったからですね。&lt;/p&gt;
&lt;p&gt;とりあえずcanvasまではできました。&lt;/p&gt;
&lt;h2&gt;canvasをvideoに流し込む&lt;/h2&gt;
&lt;p&gt;次に先ほど作ったcanvasを動画としてvideoに流し込みます。なんだか難しそうに聞こえますが、標準機能だけでできます。iOS Safariはこの機能がバグってて実現できないんですけどね……。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const setup = async() =&gt; {
  //
  // ↑ここに先ほどまでの処理がある
  //

  // canvasを動画として取得
  const stream = canvas.captureStream(60);

  // canvasを表示するだけのvideo要素を作る
  const video = document.createElement(&apos;video&apos;);
  video.autoplay = true;
  video.muted = true;
  video.playsInline = true;
  video.width = width;
  video.height = height;
  video.srcObject = stream;

  await new Promise((resolve) =&gt; {
    video.ontimeupdate = () =&gt; {
      resolve();
    }
    video.play();
  });

  return video;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;canvasに &lt;code&gt;captureStream&lt;/code&gt; というメソッドが生えてますね。これは指定したフレームレートで動画として書き出してくれる機能です。今回は60fpsです。引数を省略するとcanvas更新時のみ動画も更新され、0に指定すると手動で引っ張ってこないと更新されなくなります。&lt;/p&gt;
&lt;p&gt;また、video要素を勝手に再生するときはmutedとplaysInlineが必須です。ユーザジェスチャーがあればよいのですが、JavaScript側で先行して再生したい時などはこれらをtrueにしておきましょう。&lt;/p&gt;
&lt;p&gt;あとはPromiseで見苦しいことをやっているのですが、こうしないとmacOSのSafariで動きませんでした。macOSのSafariではplay直後に一度suspendしたあとにバッファ溜まってから再生再開という挙動になっていたので、timeupdateを待つと確実だったというわけです。&lt;/p&gt;
&lt;h2&gt;動画をPiPにする&lt;/h2&gt;
&lt;p&gt;あとはPiPにするだけです。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;//
// ↑このあたりにさっきの関数がある
//

(async function main() {
  const video = await setup();
  pipButton.style.opacity = 1;

  video.onenterpictureinpicture = () =&gt; {
    video.style.display = &apos;none&apos;;
  }

  video.onleavepictureinpicture = () =&gt; {
    video.remove();
  }

  pipButton.addEventListener(&apos;click&apos;, (event) =&gt; {
    document.body.appendChild(video);
    video.play();
    video.requestPictureInPicture();
  });
})();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PiPにするにはvideo要素の &lt;code&gt;requestPictureInPicture&lt;/code&gt; というメソッドを呼び出すだけなのですが、呼び出すにはユーザジェスチャー（クリックイベント等）必須なので注意してください。&lt;/p&gt;
&lt;p&gt;video要素をbodyに追加しているのはSafari対策です。一瞬だけでも表示させないとPiPが真っ黒になったので……。このあたりに関してはSafariのほうが挙動的に正しそうな気もしますけどね。&lt;/p&gt;
&lt;h2&gt;実行&lt;/h2&gt;
&lt;p&gt;あとはブラウザで開いて動作確認です。fetchとか使ってるのでファイルから開くのではなく適当にサーバ立ててください。よくわからなかったら適当に &lt;code&gt;python3 -m http.server&lt;/code&gt; とか叩くとサーバ起動します。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;//images.ctfassets.net/7hyenra16sli/rhc0KbJqakoGz3BI5dBaJ/56a312f968b1438bb270276f04c6855a/pip-demo.png&quot; alt=&quot;pip-demo&quot;&gt;&lt;/p&gt;
&lt;p&gt;ボタンを押せばHTML要素がPiPになるはずです！&lt;/p&gt;
&lt;h2&gt;その他&lt;/h2&gt;
&lt;p&gt;この方法ではアニメーションを含むgifやwebpを扱えません。常に最初のフレームが表示されます。アニメーション画像や動画を使いたい場合は、自力でフレームをバラバラにして次々に描画する必要があると思います。一方でCSSアニメーションは大丈夫です。&lt;/p&gt;
&lt;p&gt;また、あくまで要素全体をコピーしているだけなので、ユーザのインタラクション等は再現できません。あくまである程度静的なコンテンツを表示するためのトリックです。&lt;/p&gt;
&lt;h2&gt;プログラム全体&lt;/h2&gt;
&lt;h3&gt;HTML&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!DOCTYPE html&gt;
&amp;#x3C;html&gt;
  &amp;#x3C;head&gt;
    &amp;#x3C;meta charset=&quot;utf-8&quot;&gt;
    &amp;#x3C;title&gt;Picture in Picture&amp;#x3C;/title&gt;
    &amp;#x3C;script src=&quot;main.js&quot; defer&gt;&amp;#x3C;/script&gt;
    &amp;#x3C;link rel=&quot;stylesheet&quot; href=&quot;style.css&quot;&gt;
  &amp;#x3C;/head&gt;
  &amp;#x3C;body&gt;
    &amp;#x3C;div id=&quot;pip-source&quot;&gt;
      &amp;#x3C;img src=&quot;https://source.unsplash.com/9UUoGaaHtNE&quot;&gt;
      &amp;#x3C;span class=&quot;message&quot;&gt;Hellooooooooooooo&amp;#x3C;/span&gt;
    &amp;#x3C;/div&gt;
    &amp;#x3C;button id=&quot;pip-button&quot;&gt;PiP them&amp;#x3C;/button&gt;
  &amp;#x3C;/body&gt;
&amp;#x3C;/html&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;CSS&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;#pip-source {
  background-color: white;
  text-align: center;
  width: 320px;
  padding: 1em;
}

#pip-source img {
  display: inline-block;
  object-position: center center;
  object-fit: contain;
  width: 100%;
}

:picture-in-picture {
  display: none;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;JavaScript&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const pipSource = document.querySelector(&apos;#pip-source&apos;);
const pipButton = document.querySelector(&apos;#pip-button&apos;);
pipButton.style.opacity = 0.3;

const setup = async() =&gt; {
  // 描画する要素のサイズを取得したい
  // 画像の読み込みが終わるまで待つ
  const waitingImgList = [...pipSource.querySelectorAll(&apos;img&apos;)];
  await Promise.all(waitingImgList.map((el) =&gt; new Promise((resolve) =&gt; {
    el.addEventListener(&apos;load&apos;, () =&gt; resolve());
  })));

  // 描画する要素のサイズを取得する
  // 注意：重い処理なので頻繁に呼び出さないように
  const { width, height } = pipSource.getBoundingClientRect();

  // svgタグを作る
  // SVGはHTMLではないので作り方がちょっと違う
  // 決められたNS(Namespace)を指定して作る
  const ns = &apos;http://www.w3.org/2000/svg&apos;;
  const svg = document.createElementNS(ns, &apos;svg&apos;);
  svg.setAttribute(&apos;width&apos;, width);
  svg.setAttribute(&apos;height&apos;, height);

  // foreignObjectを作る
  // foreignObjectはその中身の描画を
  // SVG自身ではなく外側（ここではブラウザ）に任せる
  const foreignObject = document.createElementNS(ns, &apos;foreignObject&apos;);
  foreignObject.setAttribute(&apos;width&apos;, width);
  foreignObject.setAttribute(&apos;height&apos;, height);

  // 表示したい要素のコピーを作る
  // xmlとしてのNSを追加
  const html = pipSource.cloneNode(true);
  html.style.display = &apos;content&apos;;

  // 画像をすべてDataURLに置き換える
  const imgList = [...html.querySelectorAll(&apos;img&apos;)];
  await Promise.all(imgList.map(async (el) =&gt; {
    const data = await fetch(el.src).then((res) =&gt; res.blob());
    const reader = new FileReader();
    const url = await new Promise((resolve) =&gt; {
      reader.onload = () =&gt; resolve(reader.result);
      reader.readAsDataURL(data);
    });
    el.src = url;
    return el.decode();
  }));

  // 描画に必要そうなものを作っておく
  const style = document.createElement(&apos;style&apos;);
  const css = await fetch(&apos;style.css&apos;).then((res) =&gt; res.text());
  style.textContent = css;

  // 組み立てる
  html.appendChild(style);
  foreignObject.appendChild(html);
  svg.appendChild(foreignObject);

  // svgを文字列化してURL化する
  const svgStr = new XMLSerializer().serializeToString(svg);
  const svgUrl = &apos;data:image/svg+xml;charset=utf-8,&apos; + encodeURIComponent(svgStr);

  // svgを画像として読み込む
  const img = new Image(width, height);
  img.src = svgUrl;
  await img.decode();

  // canvasを作る
  const canvas = document.createElement(&apos;canvas&apos;);
  const ctx = canvas.getContext(&apos;2d&apos;);
  canvas.width = width;
  canvas.height = height;

  (function render() {
    ctx.clearRect(0, 0, width, height);
    ctx.drawImage(img, 0, 0, img.width, img.height);
    requestAnimationFrame(() =&gt; render());
  })();

  // canvasを動画として取得
  const stream = canvas.captureStream(60);

  // canvasを表示するだけのvideo要素を作る
  const video = document.createElement(&apos;video&apos;);
  video.autoplay = true;
  video.muted = true;
  video.playsInline = true;
  video.width = width;
  video.height = height;
  video.srcObject = stream;

  await new Promise((resolve) =&gt; {
    video.ontimeupdate = () =&gt; {
      resolve();
    }
    video.play();
  });

  return video;
};

(async function main() {
  const video = await setup();
  pipButton.style.opacity = 1;

  video.onenterpictureinpicture = () =&gt; {
    video.style.display = &apos;none&apos;;
  }

  video.onleavepictureinpicture = () =&gt; {
    video.remove();
  }

  pipButton.addEventListener(&apos;click&apos;, (event) =&gt; {
    document.body.appendChild(video);
    video.play();
    video.requestPictureInPicture();
  });
})();
&lt;/code&gt;&lt;/pre&gt;</content:encoded></item><item><title><![CDATA[JavaScriptのIntl.Segmenterで文章の意味分割を行う]]></title><description><![CDATA[コンピュータ上で文字列を扱う時、データ上は単にコードユニットの羅列でしかなく、そこに単語や文としての意味合いはありません。
しかし我々は人間であり、単なる文字列ではなく文章として処理したい場合がありま…]]></description><link>https://sbfl.net/blog/2020/11/21/javascript-intl-segmenter/</link><guid isPermaLink="false">https://sbfl.net/blog/2020/11/21/javascript-intl-segmenter/</guid><pubDate>Sat, 21 Nov 2020 10:22:00 GMT</pubDate><content:encoded>&lt;p&gt;コンピュータ上で文字列を扱う時、データ上は単にコードユニットの羅列でしかなく、そこに単語や文としての意味合いはありません。&lt;/p&gt;
&lt;p&gt;しかし我々は人間であり、単なる文字列ではなく文章として処理したい場合があります。そんなとき、 &lt;code&gt;Intl.Segmenter&lt;/code&gt; が役に立つかもしれません。&lt;/p&gt;
&lt;h2&gt;文章の分割と仕様の標準化&lt;/h2&gt;
&lt;p&gt;プログラムを書く上で、文字列の処理、というより「文章の処理」をしたくなることがしばしばあります。文章というのは単語や文で構成された、単なる文字コードの連続ではなく、人間的に意味を持つ単位が連続したものとなります。&lt;/p&gt;
&lt;p&gt;今まで文字列の意味的な分割は &lt;code&gt;Intl.v8BreakIterator&lt;/code&gt; を用いて行っていました。これはChromeやNode.jsで使用されているV8エンジンの独自の仕様であり、一般的な物ではありません。なおかつNode.jsでは意図的に無効化されています。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Intl.v8BreakIterator&lt;/code&gt; の代替はしばらく存在しなかったのですが、 &lt;code&gt;Intl.Segmenter&lt;/code&gt; というProposalが上がり、そろそろ正式なものになりそうです。Chrome 87にも実装されましたし、まだ使用はできませんがFirefoxやSafariにも実装自体はされたので、紹介しておきたいと思います。&lt;/p&gt;
&lt;h2&gt;Intl.v8BreakIterator&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Intl.Segmenter&lt;/code&gt; を紹介する前に、歴史のお勉強として &lt;code&gt;Intl.v8BreakIterator&lt;/code&gt; について知っておきましょう。&lt;/p&gt;
&lt;p&gt;V8エンジンに独自実装されていた &lt;code&gt;Intl.v8BreakIterator&lt;/code&gt; は、文字列を単語や文に分割する機能を持つオブジェクトです。&lt;/p&gt;
&lt;p&gt;V8エンジン独自実装ですが、Node.jsではデフォルトでは無効化されているので実質的にChrome専用のメソッドとなります。 &lt;strong&gt;非標準のAPI&lt;/strong&gt; であるため、使用は推奨されません。&lt;/p&gt;
&lt;p&gt;このような感じで使えます：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const text = &apos;メロスは激怒した。&apos;;

const itr = new Intl.v8BreakIterator([&apos;ja-jp&apos;], { type: &apos;word&apos;});
itr.adoptText(text);

let pos = itr.first();

while(pos != -1) {
  const nextPos = itr.next();
  if(nextPos === -1) { break; }
  const str = text.slice(pos, nextPos);
  const type = itr.breakType();
  console.log(str, type);

  pos = nextPos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;見ればわかると思いますが、名前にIteratorとついているのにIterableではありません。&lt;/p&gt;
&lt;p&gt;出力はこうなります：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;メロス ideo
は ideo
激怒 ideo
した ideo
。 none
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Intl.Segmenter&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Intl.Segmenter&lt;/code&gt; は &lt;code&gt;Intl.v8BreakIterator&lt;/code&gt; と似た機能を持つJavaScript標準のAPIです。長らく存在しなかった &lt;code&gt;Intl.v8BreakiTerator&lt;/code&gt; の標準機能版となります。&lt;/p&gt;
&lt;p&gt;使い方は以下のようにします：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Segmenterオブジェクトを作成する
// 日本語を対象とし、単語単位で分割するSegmenter
const segmenter = new Intl.Segmenter(&quot;ja&quot;, {granularity: &quot;word&quot;});

// 文字列をセグメントに分割する
const segments = segmenter.segment(&apos;メロスは激怒した。&apos;);

// segmentsはIterableなのでfor-ofなどが使える
for(const seg of segments) {
  // seg.segmentに単語が入っている
  console.log(`Word: ${seg.segment}`);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;これで以下のように出力されます：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Word: メロス
Word: は
Word: 激怒
Word: した
Word: 。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;まずはSegmenterオブジェクトを作成します。Segmenterオブジェクトには第一引数にロケールを、第二引数にオプションを渡します。オプションには分割単位を指定します。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Segmenterオブジェクトを作成する
// 日本語を対象とし、単語単位で分割するSegmenter
const segmenter = new Intl.Segmenter(&quot;ja&quot;, {granularity: &quot;word&quot;});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第二引数で選べる分割単位 &lt;code&gt;granularity&lt;/code&gt; （粒度）は以下のようになっています：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;grapheme（書記素 ≒ 文字）&lt;/li&gt;
&lt;li&gt;word（単語）&lt;/li&gt;
&lt;li&gt;sentence（文）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;省略した場合は自動的にgraphemeになります。&lt;/p&gt;
&lt;p&gt;次に文字列を渡し、セグメントに分割してもらいます。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// 文字列をセグメントに分割する
const segments = segmenter.segment(&apos;メロスは激怒した。&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;これで文字列を適切に分割した、Iterableな &lt;code&gt;Segments&lt;/code&gt; オブジェクトが返ってきます。&lt;/p&gt;
&lt;p&gt;Iterableなのでfor-ofなどで扱えます。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// segmentsはIterableなのでfor-ofなどが使える
for(const seg of segments) {
  // seg.segmentに単語が入っている
  console.log(`Word: ${seg.segment}`);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使い方はこれだけです。&lt;/p&gt;
&lt;h2&gt;Segmentデータ&lt;/h2&gt;
&lt;p&gt;各々のSegmentデータは以下のようになっています：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;type Segment = {
  segment: string;      // セグメント文字列
  index: number;        // セグメントの入力文字列上の位置
  input: string;        // 入力文字列の全体
  isWordLike?: boolean; // 単語かどうか。word分割のときのみ
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;segment&lt;/code&gt; には文字や単語、文などが入っています。何が入っているかはSegmenterオブジェクトに指定した &lt;code&gt;granularity&lt;/code&gt; によって変わります。 &lt;code&gt;word&lt;/code&gt; を指定していれば単語が入っています。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;index&lt;/code&gt; は文字列全体の中のインデックスです。 &lt;code&gt;input&lt;/code&gt; は入力文字列そのものが入っています。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;isWordLike&lt;/code&gt; は &lt;code&gt;granularity: &apos;word&apos;&lt;/code&gt; のときのみ使用でき、単語なら &lt;code&gt;true&lt;/code&gt; で、それ以外の記号などは &lt;code&gt;false&lt;/code&gt; になります。&lt;/p&gt;
&lt;p&gt;実査に使うときは分割代入などで必要な値だけを取り出すと使いやすいでしょう。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;for(const {segment, isWordLike} of segments) {
  if(isWordLike) {
    console.log(segment);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Segmentsオブジェクト&lt;/h2&gt;
&lt;p&gt;Segmentsオブジェクトには &lt;code&gt;containing&lt;/code&gt; というメソッドがひとつ実装されています。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;containing&lt;/code&gt; メソッドは文字列全体上のインデックスを受け取り、その位置の文字を内包するセングメントを返します。&lt;/p&gt;
&lt;p&gt;例えば以下のように使います：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// Segmenterオブジェクトを作成する
// 日本語を対象とし、単語単位で分割するSegmenter
const segmenter = new Intl.Segmenter(&quot;ja&quot;, {granularity: &quot;word&quot;});

// 文字列をセグメントに分割する
const segments = segmenter.segment(&apos;メロスは激怒した。&apos;);

// &apos;メロスは激怒した。&apos; の 2文字目（indexは1）が含まれるセグメントは？
console.log(segments.containing(1));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;出力は以下のようになります：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;{segment: &quot;メロス&quot;, index: 0, input: &quot;メロスは激怒した。&quot;, isWordLike: true}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;「メロスは激怒した。」の2文字目「ロ」を内包しているセグメントは「メロス」なので、メロスのセグメントが得られます。&lt;/p&gt;
&lt;h2&gt;サポート状況&lt;/h2&gt;
&lt;p&gt;実装は済んでいるけどChrome以外は有効化されていない、という状況です。有効化を待ちましょう。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Chrome&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Chrome87で正式リリース済み&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Firefox&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;実装済み / 未リリース&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Safari&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;実装済み / 未リリース&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Node.js&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;未有効化&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[11月より株式会社stand.fmに入社します]]></title><description><![CDATA[10月末で現職の株式会社トップゲートを退職し、11月からは株式会社stand.fmで働くことになります。
会社としてのstand.fmについて、オファー面談の時に代表に「転職エントリ書きたいんですけど…]]></description><link>https://sbfl.net/blog/2020/10/23/join-stand-fm/</link><guid isPermaLink="false">https://sbfl.net/blog/2020/10/23/join-stand-fm/</guid><pubDate>Thu, 22 Oct 2020 17:08:00 GMT</pubDate><content:encoded>&lt;p&gt;10月末で現職の&lt;a href=&quot;https://www.topgate.co.jp/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;株式会社トップゲート&lt;/a&gt;を退職し、11月からは&lt;a href=&quot;https://stand.fm/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;株式会社stand.fm&lt;/a&gt;で働くことになります。&lt;/p&gt;
&lt;p&gt;会社としてのstand.fmについて、オファー面談の時に代表に「転職エントリ書きたいんですけど、なんか宣伝しときたいこととかありますか」って聞いたら「自分が持ってる印象を正直に書けばいいと思う！率直に書いてくれるのが一番だよ」って言われたので、自分なりに頑張って書きます。なんか間違ってる部分あったらごめんなさい。&lt;/p&gt;
&lt;h2&gt;株式会社stand.fmについて&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://stand.fm/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;株式会社stand.fm&lt;/a&gt;は、音声配信プラットフォームであるstand.fmを開発・運営しているスタートアップです。あんまり聞いたことないと思います。僕も名前ぐらいしか知りませんでした。今年（2020年）の4月に会社ができたらしいです。まだ社員数10名ちょっとみたいですが、これから採用を頑張っていくらしいです。&lt;/p&gt;
&lt;p&gt;今年の中頃、米国でClubhouseという音声SNSが流行していることが国内でも話題になりましたよね。音声での交流は動画での交流よりもハードルが低く、かつテキストの交流よりも温度を感じやすい特性があります。今では様々な会社が音声SNSの提供を始めており、群雄割拠の時代となっています。&lt;/p&gt;
&lt;p&gt;stand.fmもノリとしては似たような感じで、番組を収録していつでも聴けるようにしたり、ライブ配信をして双方向のコミュニケーションをとったり、視聴者から「レター」をもらってそれを話題にしてさらに収録をして……といったことができるようになっています。&lt;/p&gt;
&lt;p&gt;中でも面白いのは複数人によるライブ配信です。これは事前に人を集めて配信を始めるわけでなく、ライブ配信中に挙手した人を登壇許可したり、ライブ配信者側から現在の視聴者に登壇リクエストを送ることができます。双方合意すればその場で喋り始めることができ、アドリブ感あふれる配信を楽しむことができます。&lt;/p&gt;
&lt;p&gt;ライブ配信は2020年10月現在、5人まで同時に喋ることができます。そこから生まれるセレンディピティもあり、ある種のカオスも楽しむこともできます。この展開が予想できないジャムセッションが本当に面白く、stand.fmを特徴付けていると思います。&lt;/p&gt;
&lt;h2&gt;stand.fmに入って何をするのか&lt;/h2&gt;
&lt;p&gt;入社後の仕事としては、React NativeやReact Native for Webを使ったアプリケーション開発がメインになると思います。WebSocketやWebRTCといった双方向通信の仕組みをバリバリ使っているようなので、そういうところは楽しそうです。音声配信プラットフォームなので音声周りの処理も横から覗くぐらいはできるかもしれません。&lt;/p&gt;
&lt;p&gt;単なる開発だけでなく、プラットフォームとして育てる部分にも関わっていきたいです。長期的に見れば「競合製品よりすごい」で終わってほしくなくて、ユーザに&quot;Plus Ultra（もっと先へ）&quot;の体験を与えられるようなプラットフォームにできればなあと、ぼんやり考えてます。&lt;/p&gt;
&lt;h2&gt;Twitter転職とstand.fmに決めた理由&lt;/h2&gt;
&lt;p&gt;今回もTwitter経由での転職をしました。適当に経歴まとめたgist書いてツイートしただけです。もちろん様々な会社からの面談お誘いが来てたわけですが、中でも自社プロダクトやってるところ中心に面談受けに行ってました。その中でもstand.fmがひときわ目を引きました。&lt;/p&gt;
&lt;p&gt;stand.fmは単にYouTubeを音声方面にスライドさせただけのサービスではなく、人と人との繋がりを重視するサービスです。SNSといえばフォロワー数稼ぎ合戦が始まったり、Like稼ぐために人生狂わせたりなどが発生しやすい環境です。もちろんそういった数字を軽視しているわけではなく、プラットフォームを構成する重要な要素のひとつではありますが、そればっかりだと疲れますよね。&lt;/p&gt;
&lt;p&gt;だから「繋がろう」とstand.fmは言うわけです。仲良い人を作って、ワイワイやろう、と。再生数よりもひとつのレターの方が心を救うことだってありますし、一度のライブセッションが一生忘れられない思い出になったりするわけです。スマホの向こうにいる人間を感じて、元気を与えてもらおうと。トップ配信者たちが切磋琢磨して人気を稼ぐのも許容するし、仲間内で盛り上がるだけのコミュニティも許容する、懐の深いサービスになっています。&lt;/p&gt;
&lt;p&gt;正直、自分が求めていたサービスのひとつの理想型でした。音声配信プラットフォームという領域にはそこまで興味がありませんでしたが、人々が安心して住うことのできるプラットフォーム作りへのアプローチのひとつが音声であるのならば、そこに手を貸せば自分の漠然と抱いていた夢も叶うと考えました。&lt;/p&gt;
&lt;p&gt;ただ、実際にアプリを使ってみるとわかるのですが、stand.fmにはまだまだ拙い部分もかなり多くあります。本来実現したいことの20%も達成できていないのではないかなと感じています。見ている未来は決して非現実的ではなく、ただおそらく開発リソースが圧倒的に足りていない。ユーザ数の増加に対してどうしても後手にまわってしまっていて、先へ進むためのアクションが取れていません。逆にいうとリソースの制約という現実問題さえ潰せれば、あとはうまくいくんじゃないかと感じました。&lt;/p&gt;
&lt;p&gt;加えて、中の人たちがある程度冷静であることも決め手でした。スタートアップの中にはよくわからない夢を語るだけで、現実的な議論が成立しない人たちもいます。でもstand.fmは何が足りていて何が足りていないかをしっかり考えています。「今あるリソースはこうで、プロダクトとしてのコアバリューはここだから、ここにこれだけ割き、ここは保留する」といったお話ができます。でも全く冷徹なわけではなく、現実的な割り切りをしつつもしっかりとユーザ体験を重視する内熱の高さも持ち合わせています。&lt;/p&gt;
&lt;p&gt;おそらく気持ちよく働けるだろうな、という直感がありました。流行り始めたばかりの分野で、未来が確実にあるわけではないかもしれないけど、自分の時間の一部を差し出す価値はありそうだと判断しました。&lt;/p&gt;
&lt;p&gt;たぶん最初はバタバタして苦労するだろうなーと思いつつ、落ち着いてきたらいろいろプラットフォームとして盛り上げていく提案もしていきたいですね。&lt;/p&gt;
&lt;h2&gt;転職の理由&lt;/h2&gt;
&lt;p&gt;トップゲートでは一定の役割は果たせたのかなと。いろいろ勉強会主催したり開発標準作ったり、社内で使うアプリ作ったり。そんなことをやり続けてるうちに、他のプロジェクトについてアドバイス求められたり、フロントエンドについてご意見番的な立ち回りをするようになったり、社長込みの技術会議に呼ばれるようになったり。「フロントエンドといえば古都ことだな！」という空気が完全に出来上がっていました。&lt;/p&gt;
&lt;p&gt;でも会社が大きくなるにつれて、後から優秀な人たちが入ってきました。そんな中で早期に大阪事業所に入社していろいろ騒いで目立っていただけの僕が、ある種の先行者利益にあぐらかいてさも当然のように「みんなをリードします」みたいな顔してんのは違うよなあって気持ちが膨らんできていました。僕より優れたタレントを持つ人たちが本来活躍すべき場で、僕のような社会人経験3年目のド素人がしたり顔で技術について語ってるのは無責任なんじゃないかと。&lt;/p&gt;
&lt;p&gt;なので方向転換して「挑戦者」として新たな領域に足を踏み入れたいと思うようになりました。でもトップゲートでそれを実現するにはいくつかのステップをクリアする必要があったと思います。じゃあもう転職したほうが早いかなって。&lt;/p&gt;
&lt;p&gt;退職にあたり、非常に多くの方が温かく送り出してくれました。みんな退職を惜しんでくれ、引き止めてもくれました。今まであまり実感はなかったけど、自分のやってきたことはみんなのためになっていたんだなあと初めて痛感できました。&lt;/p&gt;
&lt;p&gt;転職活動中に&lt;a href=&quot;https://www.topgate.co.jp/20200925news&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;社長が亡くなる&lt;/a&gt;大事件もありました。社長とは何度も口論したり技術について語ったり飲みに行ったりしていたので、これを知らされたときは何も言葉が出てきませんでした。本当に転職しても良いのか何度も考えましたが、僕がいなくてもなんとでもなる組織になったと思うので、前に進むことにしました。&lt;/p&gt;
&lt;h2&gt;フルリモート前提の転職&lt;/h2&gt;
&lt;p&gt;stand.fmは東京の会社ですが、僕は大阪住みです。つまり大阪からフルリモートで働くことになります。&lt;/p&gt;
&lt;p&gt;コロナウィルスは世界中に大打撃を与えましたが、世の中がリモート前提になったことで就職の選択肢は広がりました。今まで諦めていたような所在の会社にも入れる可能性が出てきたので、皆さんもいろいろ調べてみると面白いかもしれません。&lt;/p&gt;
&lt;h2&gt;stand.fmでは一緒に働く人を募集しています！&lt;/h2&gt;
&lt;p&gt;まだまだ社員は募集しているようなので、stand.fmの採用情報を貼っときます。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://corp.stand.fm/recruit&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://corp.stand.fm/recruit&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;2020年10月現在、エンジニアやデザイナ、人事などを募集しているので、興味ある方はぜひ。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[ReactのカスタムHooksをカジュアルに使ってコードの見通しを良くしよう]]></title><description><![CDATA[もはやReactにHooksのない生活は考えられず、私たちのReactコードの中には多数のHooksが使われています。
一方でその弊害として、使われているHooksが多すぎてコードが散らかり始めた人も…]]></description><link>https://sbfl.net/blog/2020/08/21/use-react-hooks-easy/</link><guid isPermaLink="false">https://sbfl.net/blog/2020/08/21/use-react-hooks-easy/</guid><pubDate>Fri, 21 Aug 2020 08:25:00 GMT</pubDate><content:encoded>&lt;p&gt;もはやReactにHooksのない生活は考えられず、私たちのReactコードの中には多数のHooksが使われています。&lt;/p&gt;
&lt;p&gt;一方でその弊害として、使われているHooksが多すぎてコードが散らかり始めた人も多いと思います。Hooksは便利ですが粒度は小さく、プログラムの規模によっては多用しなければなりません。&lt;/p&gt;
&lt;p&gt;そこでカスタムHooksの使用を勧めます。カスタムHooksを使うことでコードの見通しを良くすることができます。&lt;/p&gt;
&lt;h2&gt;カスタムHooksをカジュアルに使っていく&lt;/h2&gt;
&lt;p&gt;カスタムHooksというと、どちらかというとReactの中では難しい部類に入ります。主に「使い方がわからない」「公式ドキュメントが不親切」「ネットの解説が難しい」あたりが問題になるでしょう。しかし難しい機能だからと言って難しく使う必要はなく、自分の使える範囲で自由に使えばいいのではないかと思います。&lt;/p&gt;
&lt;p&gt;カスタムHooksは一般にロジックの分離や再利用性の向上、テストの容易化などのために使われますが、実際にはもう少しカジュアルに使っても大丈夫です。難しいこと考えずにまずは「フックが増えすぎたからカスタムHooks導入してみよう」ぐらいの気持ちから始めて行きましょう。&lt;/p&gt;
&lt;p&gt;本記事ではカスタムHooksを用いた簡単なパターンについて、いくつか紹介します。読んでいくうちにその魅力に気づいていただければと思います。&lt;/p&gt;
&lt;h2&gt;カスタムHooksの基本&lt;/h2&gt;
&lt;p&gt;Hooksの基本的な知識については&lt;a href=&quot;https://sbfl.net/blog/2019/11/12/react-hooks-introduction/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;最近Reactを始めた人向けのReact Hooks入門&lt;/a&gt;をご覧ください。&lt;/p&gt;
&lt;p&gt;ReactではカスタムHooksを作ることで、既存のHooksに囚われない自由な処理を取り扱えます。カスタムHooksの作り方は簡単で、ただ関数を定義するだけです。決まり事は名前を &lt;code&gt;useXXX&lt;/code&gt; にする、ただそれだけです。これにも強制力があるわけではないですが、ReactのデフォルトHooksがすべて &lt;code&gt;useXXX&lt;/code&gt; なのでそれに合わせておきましょうというだけの話です。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const useHello = () =&gt; {
  return &apos;Hello&apos;;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;しかし上の定数を返すだけの例は本当にHooksなのかというと怪しいです。普通は他のHooksと組み合わせて使います。カスタムHooksの内部では他のHooksを使うことが可能です。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const useCount = () =&gt; {
  const [count, setCount] = useState(0);
  const countUp = () =&gt; setCount((c) =&gt; c+1);
  
  return [count, countUp];
  // TypeScriptの場合はas constをつける
  // return [count, countUp] as const;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;このuseCountというHooksでは内部でuseStateを使っており、状態値を保持することができます。返却値はcount値そのままと、countを増加させるcountUp関数です。&lt;/p&gt;
&lt;p&gt;使い方は普通のHooksと同じです。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const App = () =&gt; {
  const [count, countUp] = useCount();
  return &amp;#x3C;div onClick={countUp}&gt;{count}&amp;#x3C;/div&gt;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;カスタムHooksのマナー&lt;/h2&gt;
&lt;p&gt;カスタムHooksには制約がありません。自由な使い方をしても動作に支障が出ることはなく、何かしらのルールを守る必要はありません。&lt;/p&gt;
&lt;p&gt;しかし一般的なマナーのようなものが暗黙に存在し、これを破ると使用者側にちょっとびっくりされます。そのマナーというのは「デフォルトのHooksにスタイルを合わせる」です。具体的には以下の2つになります。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;戻り値は無し、1個の値、2個のタプルのいずれかである&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;無しはuseEffect、1個はuseRef、2個のタプルはuseState、が代表的&lt;/li&gt;
&lt;li&gt;4個とか5個とかになってきたら分割を考えてみましょう&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用者側で再評価の制御をしたい場合は依存配列を使う&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;useEffectの第二引数と同じようにする&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;どちらも簡単なマナーなので、少し意識してみると綺麗なカスタムHooksが作れるかもしれません。&lt;/p&gt;
&lt;h2&gt;複数のHooksをまとめる&lt;/h2&gt;
&lt;p&gt;例えば時計アプリを作りたいとします。素直に作れば以下のようになるでしょう：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;export const App = () =&gt; {
  const [date, setDate] = useState(new Date());

  // 毎秒更新する
  useEffect(() =&gt; {
    const timer = setInterval(() =&gt; {
      setDate(new Date());
    }, 1000);

    return () =&gt; clearInterval(timer);
  }, [setDate]);

  return &amp;#x3C;div&gt;{date.toLocaleTimeString()}&amp;#x3C;/div&gt;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;このコードには特に問題はなく、動作もします。しかし少々不格好というか、将来的な機能追加にともなってどんどん膨れ上がっていくことが予想できます。&lt;/p&gt;
&lt;p&gt;問題の根本としては、1つの機能に対して2つのHooksを使っているところにあります。また、内部的なロジックもベタ書きされており、少し「細かいコード」かなという印象を受けます。&lt;/p&gt;
&lt;p&gt;このDateの更新周りのHooksをカスタムHooksに押し込めてしまいます。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const useCurrentDate = (interval) =&gt; {
  const [date, setDate] = useState(new Date());

  useEffect(() =&gt; {
    const timer = setInterval(() =&gt; {
      setDate(new Date());
    }, interval);

    return () =&gt; clearInterval(timer);
  }, [setDate]);

  return date;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;そしてこのHooksをコンポーネント側で使います。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;export const App = () =&gt; {
  const date = useCurrentDate(1000);
  return &amp;#x3C;div&gt;{date.toLocaleTimeString()}&amp;#x3C;/div&gt;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;びっくりするほどスッキリしましたね！このようにカスタムHooksを使うことで、複雑に絡み合ったHooksをひとつのHooksにまとめあげることができます。&lt;/p&gt;
&lt;p&gt;「ひとつのコンポーネントにHooksが10個とか並んでて、しかもどれがどれに依存しているのかわからない…」という方はカスタムHooksを使用してみてはどうでしょう。&lt;/p&gt;
&lt;h2&gt;複雑なイベント制御を隠蔽する&lt;/h2&gt;
&lt;p&gt;Reactのコードはシンプルに保つことこそ美徳です。しかし複雑なイベントを扱う場合はそうも言ってられません。&lt;/p&gt;
&lt;p&gt;例えば「マウスの位置を取得したい。なおかつイベントはthrottlingする」などと考えだすと、コードの量はすさまじいことになります。このとき &lt;code&gt;useMousePosition&lt;/code&gt; というフックを作って、そこに複雑な処理を押し込めれば物事はシンプルになりそうです。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const useMousePosition = (interval = 0) =&gt; {
  const [position, setPosition] = useState({x: 0, y:0});

  useEffect(() =&gt; {
    let lastCalled;

    const handleMouseMove = (event) =&gt; {
      const currentTime = performance.now();
      if(lastCalled &amp;#x26;&amp;#x26; currentTime - lastCalled &amp;#x3C; interval) {
        return;
      }

      setPosition({x: event.pageX, y: event.pageY});
      lastCalled = currentTime;
    };

    document.addEventListener(&apos;mousemove&apos;, handleMouseMove);
    return () =&gt; document.removeEventListener(&apos;mousemove&apos;, handleMouseMove);
  }, [interval, setPosition]);

  return position;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;useMousePoition&lt;/code&gt; フックは引数で受け取ったインターバルでスロットリングされる、マウス座標取得処理です。このフックを使うには、ただ以下のようにするだけです：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;export const App = () =&gt; {
  const {x, y} = useMousePosition(50);
  return &amp;#x3C;div&gt;Pos: {x}, {y}&amp;#x3C;/div&gt;;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;実にシンプルですね。処理が複雑になればなるほどカスタムHooksは効果を発揮します。&lt;/p&gt;
&lt;h2&gt;操作を制限する&lt;/h2&gt;
&lt;p&gt;Hooksは汎用的な機能であり、使用側に任意の操作を許します。しかし時としてそれが邪魔になることがあります。そういった場合は通常のHooksをカスタムHooksを使って隠してやることで操作を制限することができます。&lt;/p&gt;
&lt;p&gt;例えば以下は非同期処理の結果を変数に格納するコードです：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;export const App = () =&gt; {
  const [data, setData] = useState();

  useEffect(() =&gt; {
    const timer = setTimeout(() =&gt; setData(&apos;Hello&apos;), 1000);
    return () =&gt; clearTimeout(timer);
  }, [setData]);

  return &amp;#x3C;div&gt;{data}&amp;#x3C;/div&gt;;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;しかしこのとき &lt;code&gt;setData&lt;/code&gt; はコンポーネント内に無防備に露出しています。つまりデータを変更しようと思えば &lt;code&gt;App&lt;/code&gt; コンポーネント内ならどこでもできてしまいます。問題になることは少ないのですが、可能であれば隠してしまいたい場合もあると思います。&lt;/p&gt;
&lt;p&gt;カスタムHooksを使うことで &lt;code&gt;setData&lt;/code&gt; を隠してしまうことができます。非同期部分を &lt;code&gt;useAsyncData&lt;/code&gt; フックとして切り出してみます。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const useAsyncData = (promise) =&gt; {
  const [data, setData] = useState();

  useEffect(() =&gt; {
    promise.then((result) =&gt; setData(result));
  }, [promise]);

  return data;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;あとは使うだけですね。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const timeout = async (fn, delay) =&gt; {
  return new Promise((resolve, reject) =&gt; {
    setTimeout(() =&gt; resolve(fn()), delay);
  });
};

export const App = () =&gt; {
  const data = useAsyncData(timeout(() =&gt; &apos;Hello&apos;, 1000));

  return &amp;#x3C;div&gt;{data}&amp;#x3C;/div&gt;;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;カスタムHooksを通して利用することで &lt;code&gt;setData&lt;/code&gt; を隠してしまって、関係のない場所からは触れないようにしました。&lt;/p&gt;
&lt;h2&gt;さいごに&lt;/h2&gt;
&lt;p&gt;カスタムHooksというと複雑な事情を備えた高度な機能だと捉えがちです。もちろん結合の緩和であるとか抽象的なインターフェイスの提供などに使うのが一番効果を発揮するのですが、まずはカジュアルに、ちょっとした処理をまとめるところから始めてみませんか？&lt;/p&gt;
&lt;p&gt;慣れきたらより難しい使い方もできるようになりますし、なによりカジュアルな使い方でも効果は大きいです。いきなり100%活用しようとするのではなく、40%ぐらいを目指せばいいと思います。それで十分です。&lt;/p&gt;
&lt;p&gt;Reactには今後も様々な機能が増えると思いますが、怖がらず、軽く触れてみるところから始めてみましょう。きっと何かのためになるはずです。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[シンプルなReactプロジェクトを簡単に構築するだけのツールを作った]]></title><description><![CDATA[単純なReactプロジェクトを簡単に作るためにcreate-xxx-appというツールを作ったので紹介します。
create-xxx-app
create-xxx-app はテンプレートからNode.…]]></description><link>https://sbfl.net/blog/2020/08/11/create-xxx-app/</link><guid isPermaLink="false">https://sbfl.net/blog/2020/08/11/create-xxx-app/</guid><pubDate>Tue, 11 Aug 2020 07:48:00 GMT</pubDate><content:encoded>&lt;p&gt;単純なReactプロジェクトを簡単に作るためにcreate-xxx-appというツールを作ったので紹介します。&lt;/p&gt;
&lt;h2&gt;create-xxx-app&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.npmjs.com/package/create-xxx-app&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;create-xxx-app&lt;/a&gt; はテンプレートからNode.jsプロジェクトを作るためのパッケージです。&lt;/p&gt;
&lt;p&gt;私が個人的に使うために作ったものですが、一応npmにpublishしているので簡単に解説しておきます。xxxという文字の並びにはエロい意味があるらしいですが、気にしないでおきます。&lt;/p&gt;
&lt;p&gt;Node.jsがインストールされた環境で以下のコマンドを叩きます。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-none&quot;&gt;npx create-xxx-app プロジェクト名 --template react
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;これでReact開発環境の整った「プロジェクト名」フォルダが出来上がります。今のところ対応しているテンプレート名は「react」のみです。&lt;/p&gt;
&lt;p&gt;また、「react」テンプレートも暫定状態でpublishしています。今後大きな変更が入る可能性もあります。&lt;/p&gt;
&lt;p&gt;create-xxx-appは個人でとりあえず使うために作ったので、まだエラーハンドリング等もしていません。このあたりは今後の課題になります。macOSでしか動作確認してないので他のOSで動くかもわからないです。&lt;/p&gt;
&lt;h2&gt;create-xxx-appの仕事&lt;/h2&gt;
&lt;p&gt;create-xxx-app（CXA）は以下の動作をします。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;テンプレートファイルをコピーする&lt;/li&gt;
&lt;li&gt;依存関係をnpm installする&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;以上で全てです。&lt;/p&gt;
&lt;h2&gt;なぜcreate-react-appを使わないのか&lt;/h2&gt;
&lt;p&gt;create-react-app（CRA）は素晴らしいツールです。
当時、ネットには様々な「React構築ベストプラクティス」が溢れ煩雑になっていました。
そんな中でCRAはReact開発環境のセットアップを容易にし、多くの人々を導いてきました。&lt;/p&gt;
&lt;p&gt;一方でCRAは、複雑な物事をまるでシンプルに見せかけるために、様々な&quot;おせっかい&quot;が混入しています。
特に &lt;code&gt;react-scripts&lt;/code&gt; 周りはほぼブラックボックスとなっており、プロジェクト自体がここに依存してしまいます。&lt;/p&gt;
&lt;p&gt;ejectすることでフラットなプロジェクトに変換できるという脱出ハッチは用意されてはいるのですが、水面下でもがくために難解な構造をしたスクリプトや設定ファイルを展開されたところで、何をすればいいのかよくわからんというのが正直なところです。&lt;/p&gt;
&lt;p&gt;また、ejectしたところでwebpackやjestに強く依存した部分については変わりません。ひと昔前ならwebpackでもよかったのですが、最近では選択肢も増えてきたので自由に選びたいところです。&lt;/p&gt;
&lt;p&gt;あとは単純な話として、公式のテンプレートの出来が良くありません。親切のために様々な便利コードが書かれていたり、使いもしないアセットも同梱されています。プロジェクトを始めるときに、まずはこれらのお節介をひとつひとつ丁寧に削除していく必要があります。カスタムテンプレートを作るという手もあるのですが、結局CRAの手の中で動くことには変わりありません。&lt;/p&gt;
&lt;p&gt;CRAだけでなく、 &lt;code&gt;create-snowpack-app&lt;/code&gt; や他の &lt;code&gt;create-*-app&lt;/code&gt; 系でも同じです。いろんなところから設定ファイルをextendsで引き摺り回し、裏側は隠蔽され、膨大な親切サンプルファイルが出来上がります。&lt;/p&gt;
&lt;p&gt;Node.jsのプロジェクト構成というのはnode_modulesとソースディレクトリのカオスを除けば、追えないレベルに肥大化することはそうそうなくて、いろんな箇所に依存して継承して隠蔽してとする意味はあまりないです。手元のエディタのみで一覧できなくて死ぬほど困ることの方が多いです。&lt;/p&gt;
&lt;h2&gt;CXAは何を達成したいのか&lt;/h2&gt;
&lt;p&gt;CXAのやっていることは単純なファイルコピーと &lt;code&gt;npm install&lt;/code&gt; のみです。おわり。&lt;/p&gt;
&lt;p&gt;でも私が本当に必要だったものってこれなんです。高度な抽象化とか親切なサンプルコードとかじゃなくて、コピーとインストールがほしかった。単に最初の手間を省くだけでいいのに、ゆりかごから墓場まで面倒見てくれるようなお節介スクリプトを同梱してくれなんて一度も頼んでない。でもどれだけ探してもこれを達成してくれるものが出てこない。じゃあ自分で作っちゃおうという動機です。&lt;/p&gt;
&lt;p&gt;プロジェクト構築ツールはプロジェクト構築を終えたら役目を終えてほしいというのが個人的な思いでした。プロジェクトの根幹にまで食い込むのであれば、それはもはやただの呪いじゃないですか。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;yarn create&lt;/code&gt; でいいじゃんと思うかもしれませんが、それはyarnが標準になったら考えます。&lt;/p&gt;
&lt;h2&gt;cxa-template-react&lt;/h2&gt;
&lt;p&gt;reactテンプレートの構造も単純にしています。まだWIPですが、リポジトリを見ていただければ雰囲気は掴めるかと。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/kotofurumiya/cxa-template-react&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/kotofurumiya/cxa-template-react&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;主な依存パッケージは以下の通りです。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;react&lt;/li&gt;
&lt;li&gt;react-dom&lt;/li&gt;
&lt;li&gt;typescript&lt;/li&gt;
&lt;li&gt;esbuild&lt;/li&gt;
&lt;li&gt;browser-sync&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;令和にもなってbrowser-syncかよって思うかもしれませんが、このあたりはとりあえず突っ込んだだけです。&lt;/p&gt;
&lt;p&gt;ソースコードは以下のもののみです。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;src&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;index.tsx&lt;/li&gt;
&lt;li&gt;App.tsx&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;public&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;index.html&lt;/li&gt;
&lt;li&gt;style.css&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;tools&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;devserver.js&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;中身もシンプルにしてあります。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import React from &apos;react&apos;;

export const App = () =&gt; {
  return &amp;#x3C;div&gt;Hello React&amp;#x3C;/div&gt;;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;正直borwser-syncも過剰だと思っていて、これを削るかもまだ悩んでいます。&lt;/p&gt;
&lt;p&gt;CXAで作られたアプリのカスタマイズは簡単です。自動リロードが不要ならbrowser-syncをアンインストールするだけでいいし、jestが欲しかったらインストールするだけ、prettierがいるなら自分で入れる。それで終わり。CXAで作られたアプリはCXAがなくても独立して生きていけます。ejectも不要です。単なるひとつのNode.jsプロジェクトとして存在します。&lt;/p&gt;
&lt;h2&gt;カスタムテンプレート&lt;/h2&gt;
&lt;p&gt;CXAはReactに限らず一般的なNode.jsプロジェクトの構築に使用することができます。テンプレートを &lt;code&gt;cxa-template-templatename&lt;/code&gt; の命名規則でnpmにpublishすれば、 &lt;code&gt;npx create-xxx-app prjname --template templatename&lt;/code&gt; でプロジェクトを作成できます。&lt;/p&gt;
&lt;p&gt;ファイルの構造とかフォルダの意味とかはドキュメントがまだできていないので、&lt;a href=&quot;https://github.com/kotofurumiya/cxa-template-react&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;cxa-template-react&lt;/a&gt;を見て雰囲気で察してください。&lt;/p&gt;
&lt;h2&gt;これからの話&lt;/h2&gt;
&lt;p&gt;まだ作りが雑なんでちゃんと詰めていこうと思います。普通に使うだけで当然の権利のようにUnhandled Promise Rejectionが出てくる。&lt;/p&gt;
&lt;p&gt;また、テンプレートの置き場はnpm registryにしか対応していませんが、fileやgitにも対応できればいいな程度に考えてます。&lt;/p&gt;
&lt;p&gt;テンプレート側もちゃんと考えていこうと思います。React、シンプルにやりたいですからね。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[WebTransportを用いてブラウザ上からUDP/QUICによるリアルタイム双方向通信を行う]]></title><description><![CDATA[ウェブとリアルタイム通信は、もはや切り離せない関係となりました。
ロングポーリングやそれを応用したComet、チャンク通信を利用したServer-Sent Event、新しくはWebSocketという…]]></description><link>https://sbfl.net/blog/2020/07/31/web-transport-introduction/</link><guid isPermaLink="false">https://sbfl.net/blog/2020/07/31/web-transport-introduction/</guid><pubDate>Thu, 30 Jul 2020 17:54:00 GMT</pubDate><content:encoded>&lt;p&gt;ウェブとリアルタイム通信は、もはや切り離せない関係となりました。
ロングポーリングやそれを応用したComet、チャンク通信を利用したServer-Sent Event、新しくはWebSocketという技術は、
いずれもリアルタイム通信を実現するために先人たちが築き上げてきた礎です。&lt;/p&gt;
&lt;p&gt;それらの技術の中に、次は &lt;code&gt;WebTransport&lt;/code&gt; が加わろうとしています。&lt;/p&gt;
&lt;h2&gt;WebSocketとTCP&lt;/h2&gt;
&lt;p&gt;WebSocketという偉大な発明は、ひとつの時代を作りました。
双方向性のリアルタイム通信がもたらした恩恵は計り知れません。&lt;/p&gt;
&lt;p&gt;しかし一方で問題もありました。
WebSocketはTCP上に構築されたプロトコルであり、当然ながらTCPの特性による制約を受けます。
顕著な例としてTCPの到達保証や順序保証があります。TCPにおいてはパケットが途中でロスした場合は再送制御が行われます。
また、パケットには番号が振られ、送信された順番通りに組み立てなければなりません。
このおかげで「TCPを使えばパケットは必ず届くし、順番も保証できる」という高い信頼性を確保できるのですが、もちろん欠点もあります。
順序保証のため &lt;strong&gt;「ひとつのパケットがロスすると後続のパケットを利用できない」&lt;/strong&gt; という現象が起こります。これを &lt;strong&gt;Head-of-Lineブロッキング&lt;/strong&gt; と呼びます。&lt;/p&gt;
&lt;p&gt;順序保証はTCPの大きなメリットのひとつです。しかしHoLブロッキングの影響で本来送信したかったデータの利用が遅れることは特定の分野で問題になりました。
例えば映像ストリーミングの世界では、到達保証や順序保証は必要ありません。
「現在の映像が、遅延なく届く」ことが重要であり、映像のすべてのフレームが順番通りに必ず連結できることは求めていません。
競争性の高い対戦ゲームにおいてもそうです。現在の対戦相手の座標などをリアルタイムに知ることが重要であり、数フレーム前の情報到達を保証されても、もはや何の役にも立ちません。&lt;/p&gt;
&lt;p&gt;TCPの大きな価値となる部分が、特定の利用領域にとっては害となっていたのです。&lt;/p&gt;
&lt;h2&gt;WebRTCとUDP&lt;/h2&gt;
&lt;p&gt;信頼性に重きを置いたTCPの対照的な存在として、UDPが存在します。&lt;/p&gt;
&lt;p&gt;UDPは「信頼性のない」通信プロトコルであり、 &lt;strong&gt;順序や、到達そのものを保証しない&lt;/strong&gt; 特性があります。
何も保証せずただただ一方的にデータを送りつけるだけのプロトコルなので、仕組みが非常にシンプルで、TCPで発生する各種問題とは無縁です。&lt;/p&gt;
&lt;p&gt;ウェブでUDP通信を行うには現在ではWebRTCを使う必要があります。WebRTCは主にビデオチャットなどに用いられています。
この話だけを聞くとWebRTCとWebSocketは相補の関係にあるように思えます。しかし実際には異なります。
WebSocketがServer-Clientでの接続方式を前提としているのに対し、WebRTCはPeer-to-Peer（サーバを介さずコンピュータ同士を直接つなぐ）方式になっています。
Peer同士の接続のためには複雑な手順を踏まねばならず、UDPを使うためだけにWebRTCをセットアップするのは労力に見合いません。&lt;/p&gt;
&lt;p&gt;現在、ウェブにおいて気軽にServer-Client方式のUDP通信を実現する手段は存在しません。&lt;/p&gt;
&lt;h2&gt;WebTrasnportの登場&lt;/h2&gt;
&lt;p&gt;「WebSocketのように簡単に使える双方向UDP通信の仕組みが欲しい」というのがウェブ開発者の正直な気持ちでした。
そしてそれが実現しようとしています。ついに &lt;strong&gt;WebTransport&lt;/strong&gt; の登場です。&lt;/p&gt;
&lt;p&gt;WebTransportはプロトコルとして主に &lt;strong&gt;QUIC&lt;/strong&gt; を用います。
QUICはUDPの上に構築された柔軟性のあるプロトコルで、UDPでありながらもTCPのように到達の保証をするという芸当もできます。&lt;/p&gt;
&lt;p&gt;WebTransportを使えばServer-Client方式で簡単に双方向接続ができます。信頼性のある通信も信頼性のない通信も両方扱えます。
まさに夢の仕様とも言えるでしょう。&lt;/p&gt;
&lt;p&gt;WebTransportには現在2種類のインターフェイスが存在します。
ひとつは &lt;code&gt;QuicTransport&lt;/code&gt; です。名前の通りQUICを用いたWebTransportを実現します。接続URLには &lt;code&gt;quic-transport://&lt;/code&gt; を用います
もうひとつは &lt;code&gt;Http3Transport&lt;/code&gt; です。こちらは通信にHTTP/3を用いたもので、URLは &lt;code&gt;https://&lt;/code&gt; を用います。&lt;/p&gt;
&lt;h2&gt;実装状況&lt;/h2&gt;
&lt;p&gt;WebTransportはChrome84において &lt;code&gt;QuicTransport&lt;/code&gt; のみ実装されています。
Chrome84時点では利用にはフラグを変更する必要があり、 &lt;code&gt;chrome://flags&lt;/code&gt; より &lt;code&gt;Experimental Web Platform features&lt;/code&gt; を有効化すれば使えるようになります。&lt;/p&gt;
&lt;p&gt;他ブラウザにおいては未実装となっています。&lt;/p&gt;
&lt;h2&gt;仕様について&lt;/h2&gt;
&lt;p&gt;この記事の内容は2020/07/31時点での仕様に基づいています。
WebTransportの仕様は随時更新されていますので、変更点は適宜チェックしてください。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://wicg.github.io/web-transport/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://wicg.github.io/web-transport/&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;QUICサーバを立てる&lt;/h2&gt;
&lt;p&gt;WebTransportの利用には当然QUICに対応したサーバが必要です。
幸運なことにGoogleがQUICのサンプルサーバ実装を公開しているので、それを使いましょう。&lt;/p&gt;
&lt;p&gt;以下のURLからダウンロードできます。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/GoogleChrome/samples/blob/7b9ca12e0a8c7e35837ac32ab03f7f0f0c5ce8bd/quictransport/quic_transport_server.py&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/GoogleChrome/samples/blob/7b9ca12e0a8c7e35837ac32ab03f7f0f0c5ce8bd/quictransport/quic_transport_server.py&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;このサーバの実行にはPython3.6以降と &lt;code&gt;aioquic&lt;/code&gt; というライブラリが必要になります。
&lt;code&gt;aioquic&lt;/code&gt; のインストールにはOpenSSLのヘッダが必要になりますので、以下のREADMEを参考にインストールしてください。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/aiortc/aioquic&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/aiortc/aioquic&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;OpenSSLのヘッダをインストールできたら、 &lt;code&gt;aioquic&lt;/code&gt; をインストールします。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-none&quot;&gt;pip3 install aioquic
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;QUICはTLSが必須となっており、何かしらの証明書がないと動かないので適当に作ります。
&lt;code&gt;openssl&lt;/code&gt; コマンドはバージョン1.1.1以上を使います。 &lt;code&gt;aioquic&lt;/code&gt; インストールの過程でOpenSSLもインストールされているはずなのでコマンドを叩きます。
（※ macOSの場合はbrewでインストールした &lt;code&gt;openssl&lt;/code&gt; コマンドをフルパスで実行します）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-none&quot;&gt;/usr/local/opt/openssl/bin/openssl req -newkey rsa:2048 -nodes -keyout certificate.key \
                   -x509 -out certificate.pem -subj &apos;/CN=Test Certificate&apos; \
                   -addext &quot;subjectAltName = DNS:localhost&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以下のような出力が出ればOKです。&lt;code&gt;certificate.key&lt;/code&gt; と &lt;code&gt;certificate.pem&lt;/code&gt; という鍵ファイルができているはずです。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-none&quot;&gt;Generating a RSA private key
................................+++++
...........................+++++
writing new private key to &apos;certificate.key&apos;
-----
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;これでサーバが起動できます。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-none&quot;&gt;python3 quic_transport_server.py certificate.pem certificate.key
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;ChromeからQuicTransportを使う&lt;/h2&gt;
&lt;p&gt;まずはChrome84以降で &lt;code&gt;chrome://flags&lt;/code&gt; より &lt;code&gt;Experimental Web Platform features&lt;/code&gt; を有効にします。
Chromeの再起動を促されてボタンが出てくるので、ボタンを押すとChromeが再起動します。
これで &lt;code&gt;QuicTransport&lt;/code&gt; が使えるようになります。まだ &lt;code&gt;Http3Transport&lt;/code&gt; は使えません。&lt;/p&gt;
&lt;p&gt;さっき作った適当な鍵を許可するようにChromeを起動します。鍵のダイジェストを取得します。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-none&quot;&gt;openssl x509 -pubkey -noout -in certificate.pem |
                   openssl rsa -pubin -outform der |
                   openssl dgst -sha256 -binary | base64
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以下のような出力が得られます。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-none&quot;&gt;ADGFAqYrog/Dv09f/5FK1H7ZkoBQ3PtL4wYWsWYBEDQ=
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;このダイジェストを用いてChromeを起動します。macOSなら以下のようにします。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-none&quot;&gt;open &quot;/Applications/Google Chrome.app&quot; \
  --args --origin-to-force-quic-on=localhost:4433 \
  --ignore-certificate-errors-spki-list=ADGFAqYrog/Dv09f/5FK1H7ZkoBQ3PtL4wYWsWYBEDQ=
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Windowsの場合はChromeのショートカットを作って、起動コマンドを以下のようにします。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-none&quot;&gt;chrome.exe --origin-to-force-quic-on=localhost:4433 --ignore-certificate-errors-spki-list=ADGFAqYrog/Dv09f/5FK1H7ZkoBQ3PtL4wYWsWYBEDQ=
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;JavaScript側からQuicTransportを利用する&lt;/h2&gt;
&lt;p&gt;やっとJavaScriptの話です！&lt;/p&gt;
&lt;p&gt;HTMLファイルから用意します。とりあえず動けばいいので以下のような感じでしょう：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;!DOCTYPE html&gt;
&amp;#x3C;html&gt;
  &amp;#x3C;head&gt;
    &amp;#x3C;meta charset=&quot;utf-8&quot;&gt;
    &amp;#x3C;title&gt;WebTransport&amp;#x3C;/title&gt;
    &amp;#x3C;script src=&quot;main.js&quot; defer&gt;&amp;#x3C;/script&gt;
  &amp;#x3C;/head&gt;
  &amp;#x3C;body&gt;&amp;#x3C;/body&gt;
&amp;#x3C;/html&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;main.js&lt;/code&gt; ファイルには以下のように記入します：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;(async function main() {
  // QuicTransportをつなぐ
  const transport = new QuicTransport(&apos;quic-transport://localhost:4433/counter&apos;);
  await transport.ready;

  // 信頼性のない送信経路
  // WritableStreamDefaultWriter（ https://developer.mozilla.org/ja/docs/Web/API/WritableStreamDefaultWriter ）
  const writer = transport.sendDatagrams().getWriter();

  // 信頼性のない受信経路
  // ReadableStreamDefaultReader（ https://developer.mozilla.org/ja/docs/Web/API/ReadableStreamDefaultReader ）
  const reader = transport.receiveDatagrams().getReader();

  // stringとTypedArrayとの相互変換に使用する
  const [encoder, decoder] = [new TextEncoder(), new TextDecoder()];

  // メッセージを送信する（到達するとは限らない）
  await writer.write(encoder.encode(&apos;Hello!!&apos;));
  writer.close();

  // メッセージを受信する（受信できるとは限らない）
  const result = (await reader.read()).value;
  console.log(decoder.decode(result));
})();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;これで信頼性のない通信ができます。データの到達は保証されませんが、それゆえに高速に通信することが可能です。
通信にはStreams APIを使用します。読み書きにはそれぞれ &lt;code&gt;ReadableStream&lt;/code&gt; と &lt;code&gt;WritableStream&lt;/code&gt; が割り当てられています。
Streams APIについては私の記事&lt;a href=&quot;https://sbfl.net/blog/2018/05/26/javascript-streams-api/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;「JavaScriptのStreams APIで細切れのデータを読み書きする」&lt;/a&gt;でも触れているので、参考にしてください。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;quic_transport_server.py&lt;/code&gt; は受信した文字列の長さをstringで返すサーバです。例えば &lt;code&gt;&apos;Hello!!&apos;&lt;/code&gt; を渡すと &lt;code&gt;&apos;7&apos;&lt;/code&gt; が返ってきます。
今回はローカルでサーバを動かしているのでほぼ間違いなくデータの送受信ができると思います。&lt;/p&gt;
&lt;p&gt;先ほどのHTMLファイルを、自作鍵の許可をして起動したChromeで開くと正常に実行されるはずです。コンソールに &lt;code&gt;&apos;7&apos;&lt;/code&gt; と表示されたら成功です。&lt;/p&gt;
&lt;h2&gt;信頼性のある通信をする&lt;/h2&gt;
&lt;p&gt;データが確実に届く、信頼性のある通信路を確保するには &lt;code&gt;async createBidirectionalStream()&lt;/code&gt; メソッドを使います。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;(async function main() {
  // QuicTransportをつなぐ
  const transport = new QuicTransport(&apos;quic-transport://localhost:4433/counter&apos;);
  await transport.ready;

  // 信頼性のある双方向ストリーム
  const stream = await transport.createBidirectionalStream();
  const reader = stream.readable.getReader();
  const writer = stream.writable.getWriter();

  // WritableStreamが不要ならReadableStreamだけを直接取得できる
  //   const reader = transport.receiveBidirectionalStreams().getReader();

  // stringとTypedArrayとの相互変換に使用する
  const [encoder, decoder] = [new TextEncoder(), new TextDecoder()];

  // メッセージを送信する
  await writer.write(encoder.encode(&apos;Hello!!&apos;));
  writer.close();

  // メッセージを受信する
  const result = (await reader.read()).value;
  console.log(decoder.decode(result));
})();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;これで読み書きのストリームが手に入ります。&lt;/p&gt;
&lt;p&gt;双方向ではなく書き込みだけの単方向通信が行いたい場合は、 &lt;code&gt;async createSendStream()&lt;/code&gt; または &lt;code&gt;receiveStreams()&lt;/code&gt; メソッドで単方向のストリームを取得できます。&lt;code&gt;async createSendStream()&lt;/code&gt; が送信専用、 &lt;code&gt;receiveStreams()&lt;/code&gt; が受信専用です。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;(async function main() {
  // QuicTransportをつなぐ
  const transport = new QuicTransport(&apos;quic-transport://localhost:4433/counter&apos;);
  await transport.ready;

  // 信頼性のある単方向ストリーム
  const stream = await transport.createSendStream();
  const writer = stream.writable.getWriter();

  // stringとTypedArrayとの相互変換に使用する
  const [encoder, decoder] = [new TextEncoder(), new TextDecoder()];

  // メッセージを送信する
  await writer.write(encoder.encode(&apos;Hello!!&apos;));
  writer.close();
})();
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;その他の細かい使い方&lt;/h2&gt;
&lt;p&gt;接続を閉じる場合は &lt;code&gt;close()&lt;/code&gt; メソッドを呼び出します。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;(async function main() {
  // QuicTransportをつなぐ
  const transport = new QuicTransport(&apos;quic-transport://localhost:4433/counter&apos;);
  await transport.ready;

  // 接続を閉じる
  transport.close();
})();
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;さいごに&lt;/h2&gt;
&lt;p&gt;WebTransportの実装が進めば、より気軽にUDP通信が実現できるようになるでしょう。
TCPでは難しかった、低レイテンシのリアルタイム通信が達成できます。&lt;/p&gt;
&lt;p&gt;WebTransportはUDPだけでなくQUICやHTTP/3を用いた到達保証のある通信もできるので、WebSocketのモダンな置き換えが可能になるでしょう。互換性や企業ファイアウォールのことを考えるとまだまだWebSocketも現役ですが、将来的にはWebTransportも有力な選択肢になります。&lt;/p&gt;
&lt;p&gt;一方でWebRTCに関しては住み分けが大事になってきます。
Server-Clientで通信したい場合はWebTransport、Peer-to-Peerで通信したい場合はWebRTC、という選択肢になります。&lt;/p&gt;
&lt;p&gt;ウェブ技術の移り変わりは目まぐるしく、しばしば驚きを与えてくれます。
最新技術にキャッチアップしつつ、これからどんなことが可能になっていくのか、楽しみに待ちましょう。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Reactの実験的ステート管理ライブラリRecoilの基本的な使い方]]></title><description><![CDATA[Reactにおける状態管理の方法論は、様々な道を辿ってきました。ある人はReduxを使い、またある人はMobXを、またある人はuseContextで物事を解決してきたでしょう。
先日、また新しい選択肢…]]></description><link>https://sbfl.net/blog/2020/05/17/react-experimental-recoil-usage/</link><guid isPermaLink="false">https://sbfl.net/blog/2020/05/17/react-experimental-recoil-usage/</guid><pubDate>Sun, 17 May 2020 07:38:00 GMT</pubDate><content:encoded>&lt;p&gt;Reactにおける状態管理の方法論は、様々な道を辿ってきました。ある人はReduxを使い、またある人はMobXを、またある人はuseContextで物事を解決してきたでしょう。&lt;/p&gt;
&lt;p&gt;先日、また新しい選択肢が増えました。Facebook公式による状態管理ライブラリ&lt;a href=&quot;https://recoiljs.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Recoil&lt;/a&gt;です。
まだExperimental（実験版）なので実際のプロジェクトに導入することは難しいですが、ちょっとつまみ食いをしてみましょう。&lt;/p&gt;
&lt;h2&gt;Recoil&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://recoiljs.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Recoil&lt;/a&gt;はFacebook製のReact状態管理ライブラリです。
小さくシンプルで、Hooksネイティブなライブラリとなっており、非同期処理にも対応している点が特徴です。&lt;/p&gt;
&lt;p&gt;まだExperimental（実験版）ということで仕様は大きく変わるかも知れませんし、もしかしたらプロジェクト自体が凍結になるかもしれません。
しかしそれでも触ってみたくなるのが人間というものです。なので今日はRecoilを触ってみましょう。&lt;/p&gt;
&lt;h2&gt;注意点&lt;/h2&gt;
&lt;p&gt;2020/05/17時点でRecoilはまだ正式リリースされていません。ここに書かれている内容は大幅に変更されている可能性があります。
実際に使用する際は、各自で最新の情報を追うようにしましょう。&lt;/p&gt;
&lt;h2&gt;Recoilの導入&lt;/h2&gt;
&lt;p&gt;まずはプロジェクトにRecoilをインストールします。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-none&quot;&gt;npm install --save recoil
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;現時点で型定義ファイルはありませんが、&lt;a href=&quot;https://github.com/DefinitelyTyped/DefinitelyTyped/pull/44756&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;DefinitelyTypedにPullRequestが出ている&lt;/a&gt;ので近いうちに取り込まれるでしょう。&lt;/p&gt;
&lt;p&gt;あとはRecoilを適用したい範囲を &lt;code&gt;&amp;#x3C;RecoilRoot&gt;&lt;/code&gt; で囲むだけです。一般的にはルートコンポーネントを囲むことになるでしょう。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import React from &apos;react&apos;;
import ReactDOM from &apos;react-dom&apos;;
import { RecoilRoot } from &apos;recoil&apos;;

const App = () =&gt; (
  &amp;#x3C;div&gt;
    Hello!
  &amp;#x3C;/div&gt;
);

ReactDOM.render(
  &amp;#x3C;RecoilRoot&gt;
    &amp;#x3C;App/&gt;
  &amp;#x3C;/RecoilRoot&gt;,
  document.getElementById(&apos;root&apos;)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Recoilによる状態管理&lt;/h2&gt;
&lt;p&gt;Recoilでの状態管理は実にシンプルで、&lt;code&gt;useRecoilState&lt;/code&gt; フックを呼び出すだけです。
&lt;code&gt;useState&lt;/code&gt; フックと違う点は、生の値を直接扱うのではなく、Atomというステートオブジェクトを通して値を管理するところです。&lt;/p&gt;
&lt;p&gt;Atomは &lt;code&gt;atom&lt;/code&gt; 関数にプロジェクト全体でユニークなキーとデフォルト値を渡すだけで作れます。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const countState = atom({
  key: &apos;sample/count&apos;, // 適当なユニークキー
  default: 0           // デフォルト値
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;作成したAtomを &lt;code&gt;useRecoilState&lt;/code&gt; フックに渡すことで状態を取得できます。あとは &lt;code&gt;useState&lt;/code&gt; と同じです。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const Counter = () =&gt; {
  // atomから状態を取り出す
  const [count, setCount] = useRecoilState(countState);

  return &amp;#x3C;div onClick={() =&gt; setCount((c) =&gt; c + 1)}&gt;Clicked: {count}&amp;#x3C;/div&gt;;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以下に例を示します：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import React from &apos;react&apos;;
import ReactDOM from &apos;react-dom&apos;;
import { RecoilRoot, atom, useRecoilState } from &apos;recoil&apos;;

// コンポーネントの外でatomを作成する。
// atomにはユニークなkeyとデフォルト値が含まれる。
const countState = atom({
  key: &apos;sample/count&apos;,
  default: 0
});

const Counter = () =&gt; {
  // atomから状態を取り出す
  const [count, setCount] = useRecoilState(countState);

  return &amp;#x3C;div onClick={() =&gt; setCount((c) =&gt; c + 1)}&gt;Clicked: {count}&amp;#x3C;/div&gt;;
};

const App = () =&gt; (
  &amp;#x3C;Counter/&gt;
);

ReactDOM.render(
  &amp;#x3C;RecoilRoot&gt;
    &amp;#x3C;App/&gt;
  &amp;#x3C;/RecoilRoot&gt;,
  document.getElementById(&apos;root&apos;)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Atomは複数作ることもできますし、ひとつだけにしてReduxのように使うこともできます。
しかし普通は複数のAtomを使うことを想定しているようなので、ある程度は分けるようにしましょう。&lt;/p&gt;
&lt;h2&gt;read-only / write-only&lt;/h2&gt;
&lt;p&gt;setXXX関数が不要な場合、 &lt;code&gt;useRecoilValue&lt;/code&gt; フックを用いることで値のみを取得できます。
これ自体にはあまり大きな意味はないのですが、ユーティリティフックとして覚えておくと良いでしょう。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const CountDisplay = () =&gt; {
  const count = useRecoilValue(countState);
  return &amp;#x3C;div&gt;{count}&amp;#x3C;/div&gt;;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一方、値は読み取らなくてもいいが、書き込みはしたい場合があります。そう言った場合は &lt;code&gt;useSetRecoilState&lt;/code&gt; フックを使うと良いでしょう。&lt;/p&gt;
&lt;p&gt;Reactにおいて値を読み取らない（setだけする）というのは大きな意味を持ち、値が変化した際も再レンダリングが行われないということになります。
これはパフォーマンスを考える上において有利に働きます。値の読み取りが不要な場合は積極的に利用していきましょう。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const CountUpdater = () =&gt; {
  const setCount = useSetRecoilState(countState);
  return &amp;#x3C;div onClick={() =&gt; setCount((c) =&gt; c + 1)}&gt;Increment!&amp;#x3C;/div&gt;;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Selector&lt;/h2&gt;
&lt;p&gt;原始的なAtomを直接扱う方法以外にも、Selectorというインターフェイスを通してのアクセスもできます。
SelectorはAtomの値を加工して取得する、加工して更新するなどの処理が可能になります。Atomに関係ないSelectorを作ることも可能です。&lt;/p&gt;
&lt;p&gt;Selectorを作るには &lt;code&gt;selector&lt;/code&gt; 関数を使います。ユニークなkey、getメソッド、setメソッドを渡します。&lt;/p&gt;
&lt;p&gt;以下のようにします：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const liarCountState = selector({
  key: &apos;sample/liarCount&apos;,
  get: ({get}) =&gt; get(countState) * 3,
  set: ({get, set}, newValue) =&gt; set(countState, newValue)
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;keyについてはユニークなキーを指定してください。&lt;/p&gt;
&lt;p&gt;getメソッドはSelectorから値を得る時の処理です。
この例では「嘘つき」カウントのSelectorとなっているので3倍の値を返します。
Selectorの中でAtomから値を得るには、getメソッドに渡される &lt;code&gt;get&lt;/code&gt; 関数（ややこしい！）を使用します。
このとき、もしgetメソッドの中の &lt;code&gt;get&lt;/code&gt; 関数（紛らわしい！！）でAtomにアクセスしている場合、Atomが更新されると再実行されます。&lt;/p&gt;
&lt;p&gt;setメソッドはオプションです。getメソッドだけでも動くので実装しなくとも良いです。実装するとSelectorを通しての値の更新が可能になります。
setメソッドの第1引数には &lt;code&gt;get&lt;/code&gt; 関数と &lt;code&gt;set&lt;/code&gt; 関数、第2引数には更新したい値が渡されます。
ここでは渡された値をそのままセットしています。&lt;/p&gt;
&lt;p&gt;SelectorはAtomと同等に扱うことができます。つまり各種フック（例えば &lt;code&gt;useRecoilState&lt;/code&gt; など）にそのまま渡してそのまま使えます。&lt;/p&gt;
&lt;p&gt;以下が使用例です：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import React from &apos;react&apos;;
import ReactDOM from &apos;react-dom&apos;;
import {
  RecoilRoot,
  atom,
  selector,
  useRecoilState
} from &apos;recoil&apos;;

const countState = atom({
  key: &apos;sample/count&apos;,
  default: 0
});

const liarCountState = selector({
  key: &apos;sample/liarCount&apos;,
  get: ({get}) =&gt; get(countState) * 3,
  set: ({get, set}, newValue) =&gt; set(countState, newValue)
});

const Counter = () =&gt; {
  const [count, setCount] = useRecoilState(liarCountState);
  return &amp;#x3C;div onClick={() =&gt; setCount((c) =&gt; c + 1)}&gt;Clicked: {count}&amp;#x3C;/div&gt;;
};

const App = () =&gt; (
  &amp;#x3C;Counter/&gt;
);

ReactDOM.render(
  &amp;#x3C;RecoilRoot&gt;
    &amp;#x3C;App/&gt;
  &amp;#x3C;/RecoilRoot&gt;,
  document.getElementById(&apos;root&apos;)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;非同期処理&lt;/h2&gt;
&lt;p&gt;Recoilでは非同期処理も扱うことができます。やり方はSelectorのgetメソッドをasync関数にするだけです。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const userDataState = selector({
  key: &apos;sample/userData&apos;,
  get: async ({get}) =&gt; {
    return wait(1000).then(() =&gt; ({name: &apos;John&apos;}));
  }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;こうすると、getメソッドが返したPromiseが解決されるまでsuspendされるので、あとは &lt;code&gt;&amp;#x3C;Suspense&gt;&lt;/code&gt; で受けるだけです。
Suspenseについては当ブログの &lt;a href=&quot;https://sbfl.net/blog/2020/02/10/react-suspense-async/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ReactのSuspenseで非同期処理を乗りこなす&lt;/a&gt; でも解説しています。&lt;/p&gt;
&lt;p&gt;注意点として、&lt;code&gt;get&lt;/code&gt; 関数でAtomにアクセスしている場合に、Atom更新時に再計算されるのは同期処理と同じなので、
HTTPリクエストを飛ばす場合は予想外のリクエストが飛ばないように気をつけましょう。&lt;/p&gt;
&lt;p&gt;以下がサンプルになります：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import React, { Suspense } from &apos;react&apos;;
import ReactDOM from &apos;react-dom&apos;;
import {
  RecoilRoot,
  selector,
  useRecoilValue
} from &apos;recoil&apos;;

const wait = async (millisec) =&gt; {
  return new Promise((resolve, reject) =&gt; {
    setTimeout(() =&gt; resolve(), millisec);
  });
};

const userDataState = selector({
  key: &apos;sample/userData&apos;,
  get: async ({get}) =&gt; {
    return wait(1000).then(() =&gt; ({name: &apos;John&apos;}));
  }
});

const UserDataDisplay = () =&gt; {
  const userData = useRecoilValue(userDataState);
  return &amp;#x3C;div&gt;{userData.name}&amp;#x3C;/div&gt;;
};

const App = () =&gt; (
  &amp;#x3C;Suspense fallback={&amp;#x3C;div&gt;Loading...&amp;#x3C;/div&gt;}&gt;
    &amp;#x3C;UserDataDisplay/&gt;
  &amp;#x3C;/Suspense&gt;
);

ReactDOM.render(
  &amp;#x3C;RecoilRoot&gt;
    &amp;#x3C;App/&gt;
  &amp;#x3C;/RecoilRoot&gt;,
  document.getElementById(&apos;root&apos;)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;動的なAtom/Selector&lt;/h2&gt;
&lt;p&gt;今までの例では、Atomは手で書き並べなければなりませんでした。
Recoilでは同的にAtomやSelectorを生成する方法が用意されています。&lt;/p&gt;
&lt;p&gt;Atomのファクトリを作成するには &lt;code&gt;atomFamily&lt;/code&gt; を使います。このとき &lt;code&gt;atomFamily&lt;/code&gt; に渡す引数は &lt;code&gt;atom&lt;/code&gt; とほぼ同じです。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const itemStateFamily = atomFamily({
  key: &apos;sample/item&apos;,
  default: 0
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;このときFamilyは関数なので、適当な識別子を渡してやれば、その識別子に応じたAtomを作成・取得してくれます。
あとはただのAtomなのでフックで値を取得・更新できます。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const atom = itemStateFamily(&apos;My ID&apos;); // &apos;My ID&apos;に対応するAtomを取得
const [itemCount, setItemCount] = useRecoilState(atom);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;これを用いることで、Atomの数を動的に増やすことができます。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import React from &apos;react&apos;;
import ReactDOM from &apos;react-dom&apos;;
import {
  RecoilRoot,
  atomFamily,
  useRecoilState
} from &apos;recoil&apos;;

const itemStateFamily = atomFamily({
  key: &apos;sample/item&apos;,
  default: 0
});

const Item = ({name}) =&gt; {
  const [itemCount, setItemCount] = useRecoilState(itemStateFamily(name));
  return &amp;#x3C;div onClick={() =&gt; setItemCount((c) =&gt; c + 1)}&gt;{name}: {itemCount}&amp;#x3C;/div&gt;;
};

const App = () =&gt; (
  &amp;#x3C;div&gt;
    &amp;#x3C;Item name=&quot;Apple&quot;/&gt;
    &amp;#x3C;Item name=&quot;Banana&quot;/&gt;
  &amp;#x3C;/div&gt;
);

ReactDOM.render(
  &amp;#x3C;RecoilRoot&gt;
    &amp;#x3C;App/&gt;
  &amp;#x3C;/RecoilRoot&gt;,
  document.getElementById(&apos;root&apos;)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;atomFamily&lt;/code&gt; がAtomと違う大きな点のひとつとして、 &lt;code&gt;default&lt;/code&gt; を関数にできることです。
関数を &lt;code&gt;default&lt;/code&gt; にすることによって、引数に応じたデフォルト値を設定できます。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const itemStateFamily = atomFamily({
  key: &apos;sample/item&apos;,
  default: (arg) =&gt; arg * 10;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Selectorについても同様で、 &lt;code&gt;selectorFamily&lt;/code&gt; が用意されています。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const LiarItemStateFamily = selectorFamily({
  key: &apos;sample/liarItem&apos;,
  get: (arg) =&gt; ({get}) =&gt; get(itemStateFamily(arg)) * 5,
  set: (arg) =&gt; ({get, set}, newValue) =&gt; set(itemStateFamily(arg), newValue)
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;これも &lt;code&gt;selector&lt;/code&gt; とほぼ同じで、 &lt;code&gt;get&lt;/code&gt; と &lt;code&gt;set&lt;/code&gt; がそれぞれ関数を返すメソッドとなっているだけです。
&lt;code&gt;set&lt;/code&gt; は任意なので、無くても動きます。&lt;/p&gt;
&lt;p&gt;例えば以下のように使用します：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import React from &apos;react&apos;;
import ReactDOM from &apos;react-dom&apos;;
import {
  RecoilRoot,
  atomFamily,
  selectorFamily,
  useRecoilState
} from &apos;recoil&apos;;

const itemStateFamily = atomFamily({
  key: &apos;sample/item&apos;,
  default: 0
});

const LiarItemStateFamily = selectorFamily({
  key: &apos;sample/liarItem&apos;,
  get: (arg) =&gt; ({get}) =&gt; get(itemStateFamily(arg)) * 5,
  set: (arg) =&gt; ({get, set}, newValue) =&gt; set(itemStateFamily(arg), newValue)
});

const Item = ({name}) =&gt; {
  const [itemCount, setItemCount] = useRecoilState(LiarItemStateFamily(name));
  return &amp;#x3C;div onClick={() =&gt; setItemCount((c) =&gt; c + 1)}&gt;{name}: {itemCount}&amp;#x3C;/div&gt;;
};

const App = () =&gt; (
  &amp;#x3C;div&gt;
    &amp;#x3C;Item name=&quot;Apple&quot;/&gt;
    &amp;#x3C;Item name=&quot;Banana&quot;/&gt;
  &amp;#x3C;/div&gt;
);

ReactDOM.render(
  &amp;#x3C;RecoilRoot&gt;
    &amp;#x3C;App/&gt;
  &amp;#x3C;/RecoilRoot&gt;,
  document.getElementById(&apos;root&apos;)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;また、 &lt;code&gt;atomFamily&lt;/code&gt; と &lt;code&gt;selectorFamily&lt;/code&gt; を組み合わせて、 &lt;code&gt;atomFamily&lt;/code&gt; のデフォルト値を &lt;code&gt;selectorFamily&lt;/code&gt; にすることも可能です。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const itemStateFamily = atomFamily({
  key: &apos;sample/item&apos;,
  default: selectorFamily({
    key: &apos;sample/item/liar&apos;,
    get: (arg) =&gt; ({get}) =&gt; arg + 10
  });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Atomと結び付けずにAtomの値を読み出す&lt;/h2&gt;
&lt;p&gt;稀なケースだとは思いますが、コンポーネントをAtomと結び付けずにAtomの値を読み出したい場合があります。
その場合は &lt;code&gt;useRecoilCallback&lt;/code&gt; フックを使って関数を作ることで実現できます。
通常のAtomの読み出し方だとAtomの更新ごとにコンポーネントも更新されますが、 &lt;code&gt;useRecoilCallback&lt;/code&gt; を使えば任意のタイミングでのみ取得できます。&lt;/p&gt;
&lt;p&gt;名前からわかる通り、React本体の &lt;code&gt;useCallback&lt;/code&gt; とほぼ同じです。以下が例となります：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const getUserData = useRecoilCallback(async ({getPromise}) =&gt; {
  const {name} = await getPromise(userDataState);
  console.log(name);
}, []);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;useRecoilCallback&lt;/code&gt; には引数に &lt;code&gt;getPromise&lt;/code&gt; 関数を受け取る関数を渡します。第2引数（ここでは空配列）は依存関係の配列です。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;get&lt;/code&gt; ではなく &lt;code&gt;getPromise&lt;/code&gt; となっているのは、もしSelectorからの値の取得の場合はPromiseが返ってくる可能性があるからです。
Selectorは非同期処理を扱えるのでこのような形になっています。&lt;/p&gt;
&lt;p&gt;使用例としては以下のようになります：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;import React from &apos;react&apos;;
import ReactDOM from &apos;react-dom&apos;;
import {
  RecoilRoot,
  atom,
  useRecoilCallback
} from &apos;recoil&apos;;

const userDataState = atom({
  key: &apos;sample/userData&apos;,
  default: {name: &apos;John&apos;}
})

const UserDataLogger = () =&gt; {
  const getUserData = useRecoilCallback(async ({getPromise}) =&gt; {
    const {name} = await getPromise(userDataState);
    console.log(name);
  }, []);

  return &amp;#x3C;div onClick={getUserData}&gt;Click to Log&amp;#x3C;/div&gt;;
};

const App = () =&gt; (
  &amp;#x3C;UserDataLogger/&gt;
);

ReactDOM.render(
  &amp;#x3C;RecoilRoot&gt;
    &amp;#x3C;App/&gt;
  &amp;#x3C;/RecoilRoot&gt;,
  document.getElementById(&apos;root&apos;)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;さいごに&lt;/h2&gt;
&lt;p&gt;Recoilについて大まかに眺めてみました。ずいぶん綺麗で、シンプルに纏まっているライブラリに感じます。
また実験的なライブラリであるので現実での利用は難しいですが、少し触ってみるのも楽しいかも知れません。&lt;/p&gt;
&lt;p&gt;Recoilはこれからも更新/変更がされていくはずなので、最新情報は&lt;a href=&quot;https://recoiljs.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;公式サイト&lt;/a&gt;でチェックしてみてください。&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Trusted Typesを利用してJavaScriptからのDOM操作をセキュアに行う]]></title><description><![CDATA[ウェブアプリケーションの高度化に伴い、セキュリティに対する関心も年々高まりつつあります。特にXSS（クロスサイトスクリプティング）と呼ばれる脆弱性は簡単ながらも大きな被害をもたらします。アプリケーショ…]]></description><link>https://sbfl.net/blog/2020/04/19/trusted-types/</link><guid isPermaLink="false">https://sbfl.net/blog/2020/04/19/trusted-types/</guid><pubDate>Sun, 19 Apr 2020 13:15:00 GMT</pubDate><content:encoded>&lt;p&gt;ウェブアプリケーションの高度化に伴い、セキュリティに対する関心も年々高まりつつあります。特にXSS（クロスサイトスクリプティング）と呼ばれる脆弱性は簡単ながらも大きな被害をもたらします。アプリケーションの開発者は当然セキュリティを意識した開発を行うべきですが、人間の注意は万能ではなく、時に不注意から脆弱なアプリケーションを作成してしまいます。&lt;/p&gt;
&lt;p&gt;こういった状況を改善するために、Trusted Typesという提案がなされています。Trusted Typesはよりセキュアなウェブアプリケーションを作る手段を提供し、安全性を高める補助をしてくれます。&lt;/p&gt;
&lt;h2&gt;Trusted Types&lt;/h2&gt;
&lt;p&gt;HTMLやJavaScriptは非常に柔軟な仕組みを有しており、要素を動的に組み立てることが可能です。例えば以下の例を見てみましょう：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const { username, email } = await api.getUser();
const userInfo = document.createElement(&apos;div&apos;);
userInfo.innerHTML = `${username}, ${email}`;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;このコードはAPIからユーザ情報を取得し、div要素に取得した情報を追加しようとしています。&lt;/p&gt;
&lt;p&gt;ここで &lt;code&gt;innerHTML&lt;/code&gt; は代入された文字列をHTMLとして解釈します。すると大きな問題が起こります。もし何らかの原因でユーザ名やメールアドレスに悪意ある値、例えばスクリプトなど、が埋め込まれていた場合、ブラウザは何の疑いもなくそのスクリプトを実行します。これがXSSです。&lt;/p&gt;
&lt;p&gt;XSSが発生する原因の多くは、主にDOMが任意の文字列を許容するところにあります。任意の値を受け取るということは、悪意あるコードが一切の検査を通らずに実行されてしまうことを意味します。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Trusted Types&lt;/strong&gt; はDOMのプロパティなどが &lt;strong&gt;任意を文字列を受け取ることを禁止&lt;/strong&gt; し、特定の関数を通過した &lt;strong&gt;検査済み文字列のみを許容&lt;/strong&gt; するようにする機能です。&lt;/p&gt;
&lt;p&gt;Trusted Typesを有効活用することで、より安全なアプリケーションが開発できます。&lt;/p&gt;
&lt;h2&gt;ブラウザ対応状況&lt;/h2&gt;
&lt;p&gt;Trusted TypesはChrome 83より利用可能になります。他のブラウザにおける対応は未定です。&lt;/p&gt;
&lt;h2&gt;Trusted Typesの有効化&lt;/h2&gt;
&lt;p&gt;Trusted Typesを利用するにはhttps環境またはlocalhostでの実行である必要があります。&lt;/p&gt;
&lt;p&gt;Trusted Typesはデフォルトで無効となっており、利用のためには有効化する必要があります。有効化のためにはHTTPの &lt;code&gt;Content-Securiy-Policy&lt;/code&gt; ヘッダで &lt;code&gt;require-trusted-types-for &apos;script&apos;&lt;/code&gt; と &lt;code&gt;trusted-types&lt;/code&gt;を指定します。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-none&quot;&gt;Content-Security-Policy: require-trusted-types-for &apos;script&apos;; trusted-types
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;HTTPヘッダの他にも &lt;code&gt;&amp;#x3C;meta&gt;&lt;/code&gt; 要素でも指定できるので、簡単なテストの際にはこちらでもよいでしょう。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;meta http-equiv=&quot;Content-Security-Policy&quot; content=&quot;require-trusted-types-for &apos;script&apos;; trusted-types&quot;&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;これでTrusted Typesが有効化されます。試しに以下のようなコードを実行してみます：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const elem = document.createElement(&apos;div&apos;);
elem.innerHTML = &apos;Hello!&apos;;
document.body.appendChild(elem);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;すると次のエラーが出ます：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-none&quot;&gt;Uncaught TypeError: Failed to set the &apos;innerHTML&apos; property on &apos;Element&apos;: This document requires &apos;TrustedHTML&apos; assignment.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Trusted Typesが有効化されたため、もう &lt;code&gt;innerHTML&lt;/code&gt; は単なる文字列を受け付けません。文字列を代入しようとするとエラーとなります。エラー文の通り、文字列ではなく &lt;code&gt;TrustedHTML&lt;/code&gt; 型の値を代入しなければいけません。&lt;/p&gt;
&lt;p&gt;Trusted Typesの対象となるのは以下のものです：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;document&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;write&lt;/code&gt; メソッド&lt;/li&gt;
&lt;li&gt;&lt;code&gt;writeln&lt;/code&gt; メソッド&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;&amp;#x3C;script&gt;&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;innerText&lt;/code&gt; プロパティ&lt;/li&gt;
&lt;li&gt;&lt;code&gt;textContent&lt;/code&gt; プロパティ&lt;/li&gt;
&lt;li&gt;&lt;code&gt;src&lt;/code&gt; プロパティ&lt;/li&gt;
&lt;li&gt;&lt;code&gt;text&lt;/code&gt; プロパティ&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;&amp;#x3C;iframe&gt;&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;srcdoc&lt;/code&gt; プロパティ&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;&amp;#x3C;embed&gt;&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;src&lt;/code&gt; プロパティ&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;&amp;#x3C;object&gt;&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;data&lt;/code&gt; プロパティ&lt;/li&gt;
&lt;li&gt;&lt;code&gt;codeBase&lt;/code&gt; プロパティ&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;SVG&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;href&lt;/code&gt; プロパティ&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;全てのHTML要素&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;outerHTML&lt;/code&gt; プロパティ&lt;/li&gt;
&lt;li&gt;&lt;code&gt;insertAdjacentHTML&lt;/code&gt; メソッド&lt;/li&gt;
&lt;li&gt;&lt;code&gt;innerHTML&lt;/code&gt; プロパティ&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Range&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;createContextualFragment&lt;/code&gt; メソッド&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;DOMParser&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;parseFromString&lt;/code&gt; メソッド&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Timer系関数&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;setTimeout&lt;/code&gt; 関数&lt;/li&gt;
&lt;li&gt;&lt;code&gt;setInterval&lt;/code&gt; 関数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Web Workers&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;new Worker()&lt;/code&gt; コンストラクタ&lt;/li&gt;
&lt;li&gt;&lt;code&gt;new SharedWorker()&lt;/code&gt; コンストラクタ&lt;/li&gt;
&lt;li&gt;&lt;code&gt;importScripts&lt;/code&gt; 関数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Service Worker&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;register&lt;/code&gt; メソッド&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;ポリシの作成&lt;/h2&gt;
&lt;p&gt;Trusted Typesは有効化しましたが、当然ながらこのままでは何も操作できません。何かしらの方法で &lt;code&gt;TrustedHTML&lt;/code&gt; 型の値を生成する必要があります。2通りの方法が考えられます：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/cure53/DOMPurify&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;DOMPurify&lt;/a&gt;のようなTrusted Types対応のライブラリを使用する&lt;/li&gt;
&lt;li&gt;Trustedな値を生成するポリシを自作する&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;今回はポリシの自作をしてみましょう。ポリシの作成には、グローバルに &lt;code&gt;trustedTypes&lt;/code&gt; というオブジェクトができているので、それの &lt;code&gt;createPolicy&lt;/code&gt; メソッドを利用します。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const myPolicy = trustedTypes.createPolicy(&apos;my-policy&apos;, {
  createHTML: (unsafeValue) =&gt; {
    return unsafeValue.replace(/&amp;#x3C;/g, &apos;&amp;#x26;lt;&apos;).replace(/&gt;/g, &apos;&amp;#x26;gt;&apos;);
  }
});

const elem = document.createElement(&apos;div&apos;);
elem.innerHTML = myPolicy.createHTML(&apos;&amp;#x3C;div&gt;Hello!&amp;#x3C;/div&gt;&apos;);
document.body.appendChild(elem);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上の例では &lt;code&gt;my-policy&lt;/code&gt; という名前のポリシを作成しています。ポリシ名は任意ですが、わかりやすい名前をつけておきましょう。&lt;/p&gt;
&lt;p&gt;次に &lt;code&gt;Content-Security-Policy&lt;/code&gt; の &lt;code&gt;truted-types&lt;/code&gt; ディレクティブでポリシを許可します。例えばポリシ名が &lt;code&gt;my-policy&lt;/code&gt; であれば、&lt;code&gt;trusted-types my-policy&lt;/code&gt; と記述します。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;meta http-equiv=&quot;Content-Security-Policy&quot; content=&quot;require-trusted-types-for &apos;script&apos;; trusted-types my-policy&quot;&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ポリシが複数存在する場合は &lt;code&gt;trusted-types my-policy my-policy2 my-policy3&lt;/code&gt; のようにします。&lt;/p&gt;
&lt;p&gt;ポリシが持つことができるメソッドは以下の3つです。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;createHTML&lt;/code&gt; メソッド: &lt;code&gt;TrustedHTML&lt;/code&gt; 型を返す&lt;/li&gt;
&lt;li&gt;&lt;code&gt;createScript&lt;/code&gt; メソッド: &lt;code&gt;TrustedScript&lt;/code&gt; 型を返す&lt;/li&gt;
&lt;li&gt;&lt;code&gt;createScriptURL&lt;/code&gt; メソッド: &lt;code&gt;TrustedScriptURL&lt;/code&gt; 型を返す&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;これらのメソッドを通した値はTrusted（信頼できる）な型になります。それぞれのメソッドを通すことで、信頼できるHTML文字列として扱える &lt;code&gt;TrustedHTML&lt;/code&gt; 型や、信頼できるURLである &lt;code&gt;TrustedScriptURL&lt;/code&gt; 型を得ることができます。&lt;/p&gt;
&lt;p&gt;今回は &lt;code&gt;innerHTML&lt;/code&gt; に代入する &lt;code&gt;TrustedHTML&lt;/code&gt; が欲しいので、 &lt;code&gt;createHTML&lt;/code&gt; のみを利用しています。必要に応じて他のメソッドも実装してください。例えば &lt;code&gt;&amp;#x3C;script&gt;&lt;/code&gt; 要素の &lt;code&gt;src&lt;/code&gt; プロパティに代入できる &lt;code&gt;TrsutedScriptURL&lt;/code&gt; が欲しい場合は &lt;code&gt;createScriptURL&lt;/code&gt; メソッドを実装します。&lt;/p&gt;
&lt;p&gt;作成したポリシのメソッド（先ほどの例では &lt;code&gt;myPolicy.createHTML&lt;/code&gt; メソッド）を呼び出すことで実際にTrustedな値を得られます。&lt;/p&gt;
&lt;h2&gt;デフォルトポリシ&lt;/h2&gt;
&lt;p&gt;ポリシ名として &lt;code&gt;default&lt;/code&gt; を指定すると特殊な挙動をします。これをデフォルトポリシと言います。&lt;/p&gt;
&lt;p&gt;デフォルトポリシはTrustedではない普通の文字列が代入された時に、自動的に適用する処理を記述します。デフォルトポリシが存在しない場合はエラーになりましたが、デフォルトポリシが定義されていると自動変換してくれるのでエラーにはなりません。&lt;/p&gt;
&lt;p&gt;ポリシの作り方やポリシの許可の仕方は同じです。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;trustedTypes.createPolicy(&apos;default&apos;, {
  createHTML: (unsafeValue) =&gt; {
    return unsafeValue.replace(/&amp;#x3C;/g, &apos;&amp;#x26;lt;&apos;).replace(/&gt;/g, &apos;&amp;#x26;gt;&apos;);
  }
});

const elem = document.createElement(&apos;div&apos;);
elem.innerHTML = &apos;&amp;#x3C;div&gt;Hello!&amp;#x3C;/div&gt;&apos;;
document.body.appendChild(elem);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;もちろん &lt;code&gt;default&lt;/code&gt; という名前のポリシに対しての明示的な許可が必要です。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;meta http-equiv=&quot;Content-Security-Policy&quot; content=&quot;require-trusted-types-for &apos;script&apos;; trusted-types default&quot;&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;全てのポリシの許可&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Content-Security-Policy&lt;/code&gt; から &lt;code&gt;trusted-types&lt;/code&gt; ディレクティブを取り除くことで全てのポリシを許可することができます。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;meta http-equiv=&quot;Content-Security-Policy&quot; content=&quot;require-trusted-types-for &apos;script&apos;&quot;&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ただしXSSで勝手にポリシを作られる可能性も考慮すると、この選択は賢いとは言えません。なので &lt;code&gt;trusted-types&lt;/code&gt; ディレクティブはできるだけ指定するようにしましょう。&lt;/p&gt;
&lt;h2&gt;Trusted Typesの安全性&lt;/h2&gt;
&lt;p&gt;注意点として、Trusted Typesそれ自体はセキュアではないというところです。あくまで開発者に安全のための補助機構を提供するだけであり、自動的にサニタイズしてはくれませんし、全くの安全を保証してくれるわけではありません。&lt;/p&gt;
&lt;p&gt;例えば以下のような危険なポリシを作成できます：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const myPolicy = trustedTypes.createPolicy(&apos;my-policy&apos;, {
  createHTML: (unsafeValue) =&gt; unsafeValue
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;このポリシは文字列をそのまま &lt;code&gt;TrustedHTML&lt;/code&gt; として返しているだけであり、一切の無害化を行っておりません。よって悪意ある文字列を含んだ可能性のある値がそのままTrustedな値として扱われます。&lt;/p&gt;
&lt;p&gt;Trusted Typesを使用する時は、値の扱いに気をつけましょう。&lt;/p&gt;
&lt;h2&gt;Content-Security-Policy-Report-Only での使用&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Content-Security-Policy&lt;/code&gt; と同様に、当然ながら &lt;code&gt;Content-Security-Policy-Report-Only&lt;/code&gt; でもTrusted Typesは使用可能です。完全なエラーにしたくはない場合はこちらを使いましょう。&lt;/p&gt;</content:encoded></item></channel></rss>