新サイト「はるらぼ」
開発::web
序
色々とストレスがマッハだったので、気晴らしに新しいドメインを取り、新サイト「はるらぼ」をオープンした。
「はるらぼ」は私のストレス発散に色々なしがらみを無視して好き勝手やるためのサイトである。
私が普段大事にしているアクセシビリティその他を完全に無視し、最新メジャーウェブブラウザでのみ動作する仕様。 コンテンツはJavaScriptで更新する形式のSPA。
今回はその「はるらぼ」の技術解説……だが、実は以外と解説する内容は少ない。
Vanilla JSでのSPA
本節の内容は、バニラJavaScriptに慣れている人には「わざわざそんなことを語って……」となるようなものである。 バニラ慣れしていない人なら「そんなことできるんだぁ」となる、かもしれない。
SPAは、アプリケーション自体はクライアントサイドで完結しているタイプのウェブアプリケーションだ。 サーバーはAPIコールし、データを取得するためにある。 ウェブブラウザでのページロードはクライアントアプリケーションをロードするときに限られるのが特徴となる。
実際はSPAといってもサーバーへの依存度は様々である。 「はるらぼ」の場合、制御はすべてクライアントにあり、サーバーからは単にドキュメントをロードするだけである。
つまり、サーバーアプリケーションは存在しない。
一般的にSPAとサーバー間ではJSONでやりとりするものだが、「はるらぼ」は基本的に単純にウェブサイトでしかないものなので、サーバーからはHTMLドキュメントをロードする。 ドキュメントはPureBuilder Simplyを用いてビルドされており、HTML形式のファイルを静的にサーブする。いわゆるSSGである。
SPAでの肝になるのがJavaScriptであり、一般的にはそれ向きのフレームワークを利用する。 一般的にはReactかAngularを使うようだ。 あまりSPAには向かないが、Vue.jsやNuxt.jsを使うケースもある。 逆に、もっとSPAに特化したライブラリというのもある。
最近はReactも反SPAな傾向にあり、流行からはやや離れつつあるようだ。
SPAを構築する上でフレームワークに頼りたい最大のポイントは、リアルタイムでのコンポーネント更新である。 JavaScriptでのDOM操作はそれなりに面倒で、パフォーマンスを考えると結構難しい。ここにステート管理が絡むとさらに難しい。
こうした面倒なことを考えたくない、というのが、基本的なフレームワークの出発点である。
だが実際のところ、そのフレームワークが余計なことを考えすぎて複雑怪奇になる傾向にあり、「フレームワークを使うことで問題が容易になる」は必ずしも真ではない。 だが、検索するとSPAを構築するにはフレームワークを用いる旨説明しているサイトがひっかかるため、ほとんどの人はその点に疑いを持たずにフレームワークを利用する。
今回、「はるらぼ」はSPAを採用したが、何一つとしてライブラリを採用していない。 その上で明確にしておきたいのは、「モダンウェブブラウザであることを制約とできるのであれば、よほど複雑なことをしない限りライブラリを使わないほうがむしろ簡単である」ということだ。
これを実現する上で最も重要になるのが、async/await
とfetch()
である。
async
/await
はJavaScriptの非同期処理だ。
サーバーサイドJavaScriptや、Babelなどのトランスパイラを用いるフレームワーク(あるいはTypeScript)ではかなり前から馴染み深いが、現在はウェブブラウザでも実装されている。
一時期、JavaScriptはイベント駆動であることがプログラミングにおける難関の全てを解決するかのようにもてはやされたことがあり、それもあってか過剰なまでにイベント駆動型、あるいは「それっぽい」書き方をするように進んでいった。 だが、「それっぽい」書き方を実現するためにかなり無理をしていた面があり、そもそも完全な非同期というのがプログラマにとって扱いやすいものではないという問題に直面していた。
そこでもう少し現実的なアプローチとして登場したのが、「待ち合わせ可能な非同期処理」というわけだ。
「async関数内でしかawaitできない」というのは筋の悪い仕様だとは思うが、使えることでかなり楽になるものではある。
ちなみに、awaitできるのをasync関数内に限っているのは、おそらくトップレベルスレッドを停止させないためだろう。あとは、コンパイラの都合もある。 とはいえ、スレッドをロックさせてしまうのは単なるプログラマの失態でしかなく、大したことではないと思うし、今はトップレベルでawaitできるのだから、同期関数でawaitできないのはまともな理由がないように思われる。 まぁ、実際のところGoogleとしてもMozillaとしても、「awaitできるのはasync関数の中だけという制約のもと美しく実装した機能を、同期関数でもできるように実装し直したくない」という理由が大きいのだろう。
fetch()
はHTTPリクエストを発行する新しい関数だ。
昔からあるXMLHttpRequest
よりずっと扱いやすく、便利。Axiosのようなモダンなライブラリに近い使い心地で、Promise
を返す関数なので、await
で結果を取れる。
このふたつが使えるだけで、SPAとしてやりたいことはだいたい下準備が整っていると言っていい。
Vanilla JSの経験が少ない人にとって難関となるのは、DOM操作だろうか。 DOM操作はもともとJavaScriptのコアとは毛色の違う機能で、ECMAができてからはECMAScriptからは分離されている。 だから、現代のJavaScriptの感覚からすると、まるで別言語のように世界が違うものになっている。
nextSibling
で探索していくことや、「テキストノード」という概念があることが壁になったりするかもしれないが、基本的にはelement.firstChild.replaceWith(child)
の形で要素を置き換えることをゴールとし、document.createElement(tag)
で作った要素にelement.appendChild(child)
で盛っていく感覚に慣れればあとは簡単だ。
また、値が安全かつ完全なHTML文字列であることを信用できるのであれば、そのままinnerHTML
の値として使っても良い。DOM操作をするよりそのほうが高速だったりもする。
document.createElement
で作られたHTML要素は、ユニークなHTML要素である。
HTMLドキュメント上に存在しないHTML要素というのにも少し慣れが必要だが、例えばimg
要素をスクリプト中で作ると、たとえHTMLドキュメントに追加しなくても画像はロードされる。場合によってはこれをプリロードの仕組みに利用したりもする。
クロージャの中に含めるようにすれば、すでに終了したコンテキストの中で作られたHTML要素をHTMLドキュメントに追加しないまま利用することも可能だ。HTMLAudioElement
に関してはそもそもが見えないものなので(見えるようにもできるが)、特にHTMLドキュメントに追加する動機がなく、そのまま利用したりすることも多い。
実際に最も苦労しているのはページのback/forward操作の処理だ。
ヒストリアイテムはwindow.history.pushState()
で追加でき、これによる変更はアドレスバーにも反映される。
一方、ページ内でリンク操作などを行った場合はwindow
のイベントとしてpopstate
が発生する。これにより、pushState()
の第一引数のオブジェクトを取得できる。
一見シンプルそうだが、popstate
イベントが発生するタイミングはたくさんあり、全部をカバーするのはなかなか難しい。変にBackボタンをつけてしまったことも相まって、制御が困難になり、適切に動作しないケースがそれなりにあるし、このステート操作に関わるコードは結構な量がある。
さらに言えばforward操作が動作しなかったりする。 まぁ、このあたりはYouTubeでも結構長い間挙動がおかしいことになっていたりする問題なので、大目に見て欲しい。
時間をかけるタイミングがあれば、ちゃんと実装し直すだろう。
CSS
CSSの新しい構文、新しい機能は互換性の問題を生じやすいため使う機会が少ないが、「はるらぼ」は最新環境のみをターゲットにしているため、CSSに関わる機能を制限せずに使っている。
例えば、フォントサイズは
: min(max(24px, 2.5vw), max(24px, 2.5vh)); font-size
としている。
基本的には2.5vw
または2.5vh
のうち小さいほうが採用されるが、24px
を下回らないようにということだ。2.5vw
と2.5vh
が採用されているのは、ランドスケープかポートレートかの違いを意識している。
以前からvw
あるいはvh
を基準にしたフォントサイズに可能性を感じていたが、単純な指定では期待したように動かない。
今回の場合、こうした組み合わせに加え、メディアクエリを用いてサイズを制御している。
タイトルでのフキダシ部分も
: min(95vw, (100vw - 60px)); width
という形で制御しており、妥協案もあるが、この方法が簡単かつ良い結果になる。
path()
のようにそれがなければ実現しない関数もあるが、calc()
とmin()
,
max()
はシンプルに、CSSにあるべき、欠けたものを満たす。
calc()
はFirefox 16, Chrome 26, Safari
7でサポートされており、かなり安全寄りに「使って良い」に近いものになっている。
ただ、マイナーブラウザでは一般に解釈されないため、プロパティが無効になると致命的なものには使わないほうが安全だろう。
対してmin()
, max()
はFirefox 75, Chrome 79,
Safari
11.1と結構新しめのブラウザでサポートされており、採用はかなりの切り捨てになる。
およそ2020年にサポートされたものだ。
min()
,
max()
は一般的な、というか典型的なケースでは必要にならないが、よりスケーラブルなUIを実現するにはかなり便利。
だが、どうしても採用は切り捨てになるのが難点だ。
CSSメディアクエリの@supports
や、JavaScriptのCSS.supports()
も存在しているが、これら自体がかなり新しめのブラウザでしか動作しない上に、CSS関数をサポートするかどうかの確認には使えない。
その他、「明らかにCSSにあるべきなのになかった機能」としてはvar()
関数が挙げられるだろう。
これは、CSSで変数を使う機能であり、var()
関数はその参照に用いる。
スコープの扱いや定義方法など相当に見栄えが悪く、筋が悪いように見えるが、従来のCSSの文法から逸脱しないようになんとか追加した機能であることからだろう。
実際、カスタムプロパティを正しく解釈しないブラウザであっても、ベンダープレフィックスを理解できるブラウザであればほとんどの場合1、文法のエラーは生じない。
ちなみに、カスタムプロパティはそのセレクターが有効な範囲がスコープになる。
それを利用すると、要素単位で値を異なるものにし、その要素の値をプロパティ値とすることも可能である。
ただ、その場合はカスタムプロパティよりも前からサポートされているattr()
関数を使うほうが穏当だろう。
どちらかと言えば、セレクターが複雑に重なる状況で重複した内容を書かずに済むというほうが大きい。
また、今回はanimation
も使ったが、いつも思うがanimation
は思った通りのものにするためにかなり細かい調整を加えるため、最終的には非常に複雑なものになりやすく、CSSから挙動を読み取るのはかなり難しい。
SPAでのPureBuilder Simply
SPAでロードするためにHTMLボディだけが欲しい。
それを静的にサーブするためにHTMLファイルを用意する方法はいくらでもあるが、私はPureBuilder Simplyを用いた。 それによって、Pandocを使うシェルスクリプトを書くよりはちょっとだけ使いやすくなる。
HTMLボディだけを作るためのテンプレートはこんな感じ。
<h1>$title$</h1>
$body$
PureBuilder Simply自体は静的なHTMLファイルをサーブする形でウェブサイトを構築することを目的としたものだが、プレビルドを目的として利用することができる。 SPAでの利用を推進するためにJSON形式で出力できるようにすることも検討しているが、果たしてそこに需要はあるのだろうか。
メディア素材
今回メディア素材の中で私が作ったのは
- ロゴ
- クリック音
- タイトルコール
の3つ。
ロゴはDynafontのナニカで作った。 Inkscapeでフォント表示がおかしかったのでちゃんと確認はできていない。
クリック音はrev1は音源にはFM8を使用した。 rev2ではMassiveにある”Air Drops”というプリセットをそのまま利用した。
タイトルコールはCeVIO CS7のさとうささらを使用した。
16kbpsのBGM
BGMはフリーBGMを利用したが、なんと16kbpsのOpusである。
昔の64kbpsくらいのMP3よりも全然いい音質、というより、これくらいなら区別がつかないという人もいるだろう。
ひとつの要因として、曲がシンプルな音で始まるため、冒頭は劣化が目立たない。 これにより、音質が気にならないという面もあるだろうが、それにしてもOpusの低ビットレート音質は驚異的と言っていいだろう。
私はkawaii future bassでここまでのクオリティはとても出せないので、自分の曲は使っていない。
デザイン
デザインは、ゲームっぽさと、kawaii cyberopunkっぽさを意識した。
最近、YouTubeでKawaii Future Bass/Future House系の音楽を聴いていて、それらの世界観が本サイトのデザインの基礎となった。 特にChiru-san の Restartのアートワークなどは多大な影響を与えたと言っていいだろう。
rev1では白と紅色基調のデザインとした。
ダークカラーでもっとゴリゴリにしてもよかったけど、アシスタントとの馴染みが悪かったのと、画面がうるさかったのでやめた。
というか、今見るとsweet kawaiiに寄せすぎていて、甘すぎる気がする。 テキストサイトで水色xピンクはかなり目が痛いので、結構難しく、テキストサイトとして無難さを求めてしまった。
紅色は好きな色なのだけど、ウェブサイトではカラーブラインドアクセシビリティ2の問題があって使いにくくほとんどの場合利用を避けているけど、私のものに限らずどうしてもウェブの配色は青になりがちで、紅色で変わった感じを出していきたい気持ちがあった。
rev2では、「まだ振り切れていない」という反省を活かし、よりkawaii underground感を出しにいった。 tn-shi さんの Cyberfantasiaを見て、テキストがネオンカラーに輝くサイトってなかなか斬新なんじゃないかと試してみた。 ネオン感出すとサイバーパンクっぽさは大きく増すだろうし。
結果的には、まぁ読める範囲でかなりがんばれたと思う。 そもそもテキストを大きく出しているという特徴をうまく活かせてもいる。
なお、タイトル画面はrev2にする際にかなり大きな変更を必要とするため、現時点ではrev1のデザインにとどまっている。
スケーラブルUIのウェブサイトについて
かつてCSSは、ピクセルが支配するコンテナにポイント単位を用いた文字サイズを設定するところから始まった。
この時代、コンピュータスクリーンに対する絶対値、実寸に対する絶対値、そして相対値の3種類を使い分けるものであった。 ちなみに、実寸はMacでは72dpi、Windowsでは96dpiであるという計算であったから、実寸に対する絶対値は実質としてピクセルに対する絶対値に換算することが可能であった。
やがて人類はHi-DPIに遭遇する。 小さいの大きいのと文句を言いながら、人々はUIスケーリングを手に入れる。 これにより、pxという単位は絶対値から相対値に成り下がり、世界はピクセルに支配された。
実のところ、かなり昔から「画素密度もディスプレイサイズもあまりにもピンキリなのだから、ラスタを脱してベクターとなり、スケーラブルなUIを実現しよう」という発想そのものはあった。 だが、フォントなどは実際にそうなったが、UIの大部分はそうなっていない。アイコンがSVGになってすらいないレベルだ。
あんまりそうならなかった大きな理由としては、「画面サイズに対してUIサイズの比率を決めてほしいわけではない」という点が挙げられる。 大きいディスプレイなら大きく、小さいディスプレイなら小さく……というのは自然なようだが、小さいディスプレイなら比としては大きくして見やすくしたかったりするし、大きいディスプレイで巨大なUIにするよりは情報量を増やしたいといったこともあったりする。 もちろん、UI部品をスケーラブルにするのは大変という単純な理由もあるが。
とはいえ、スケーラブルなUIを作る環境は少しずつ整ってきていて、特にPCでウィンドウサイズなどで好みに調整できるようなUIを構築したい場合には、かなり狙ったものを作れるようになってきている。
そういった意味で厄介なのはスマートフォンで、スマートフォンのブラウザはだいたいナビゲーションUIがない状態をビューポートのサイズとしていて、実際はナビゲーションUIが出る状況があるため、100vhだとスクロールが発生する。 また、スマートフォンではウィンドウを自由にリサイズするようなものではなく、端末の物理的なサイズに左右されるため、ビューポートに対する比でサイズを決めづらい。
こうした問題はスクリーンの論理ピクセル数ではなくアプリケーションによって決まるものであるため、「モバイル向けレイアウト」をビューポートから決めるのは「関連性がそこそこ高い値による代替」でしかない。
このあたりをもうちょっとうまくやる仕組みが欲しいところ。
とりあえず見てほしい
いずれにせよ、ウェブの世界をひろげるチャレンジをしているところなので、ぜひ一度見て欲しい。