ジンジャー研究室

長めのつぶやき。難しいことは書きません。

Custom Elements を正しく実装するのはとても難しい

f:id:jinjor:20191221055521p:plain

React みたいなコンポーネント作る系フレームワークだと思って Custom Elements を使おうとすると、たちまち死んでしまう。まだ色々試している最中なのでアウトプットはないんだけど、とりあえず今考えてることを書いておく。役立たないし刺されたら困るからポエム宣言しとこうか。ポエムです。

Custom Elements やっていきたい

Custom Elements の良さは特定のフレームワークに依存しないところだと思う。例えば React とか Vue とかだとそれぞれのフレームワークの世界にどっぷり浸かってしまい互換性がないが、 Custom Elements ならば普通の要素の延長線上でどこに持って行っても使える。 npm とか使わなくても script タグで CDN とかから持ってくればすぐに動く。夢のよう。もちろん、データフローはアプリケーション固有のものになるだろうから Custom Elements が力を発揮するのはせいぜい末端要素だろうとは思う。それでもよく使いそうな末端要素を埋めてくれるのは便利。

だけど、実装を始めると途端に難易度が高いことに気づく。もちろん、チュートリアル通りにやればすぐに動くものはできるのだが、「ちゃんと」動くものを作るのはめちゃくちゃハードルが高い。「ちゃんと」というのは言い換えると「普通の DOM らしく動作する」という意味。別に使い方は自由だと思うけど、変な動きをしたら驚くし、せっかくだからちゃんとしたものを作りたいという気持ちがある。

属性とプロパティの二重管理

色々難しい点はあるが、その中でも特に、難易度を劇的に高めているのは「属性とプロパティの二重管理」だと思う。

まず、属性に関しては静的に検査されている訳でもなんでもないので、適当な文字列がどんどん入ってくる。省略される可能性があるので、デフォルト値の定義も必須だ。おかしな値が入っていても例外を投げることもできない。例えば <input type="number" min="10" max="0" value="5"> などと書くことも可能だ。この場合どうなるかというと、最初は 5 が入っているがインクリメントすると値が 10 に飛び、デクリメントすると値が 0 に飛ぶ。0 のまま送信しようとすると「値は 10 以上にする必要があります」と怒られ、10 で送信しようとすると「値は 0 以下にする必要があります」と怒られる。おかしいのだが無難な動きにはなっている。 また、min="hoge" のような明らかに意味のない値を入れると無視される。無視されるが属性が消える訳ではなく、"hoge" という属性はセットされたままになる。

対してプロパティは JS から操作されるためにあり、文字列以外に数値やブール値を許容している。とは言っても、max なら数値かと思えば文字列が返ってくる。MDN によると date-time が入りうるからということらしい(たとえ type="number" でも)。対して maxLength はどうかというと、こちらは数値が返ってくる。属性を指定しない場合は、それぞれ ""-1 が返る。 null ではない。値をセットした時の挙動も属性とは微妙に違って、例えば minLength = 10 の時に maxLength = 0 という値を入れようとすると「 10 より小さくするな」と例外を投げてくる。文字列で "100"を入れると 100 と解釈される。"hoge" を入れると 0true1 。というわけで、型に関しては指定されているものの違っても怒られずなんらかの解釈をするようだ。

次に属性とプロパティの関係について。最初「属性を変更したらプロパティも変更される」という一方通行だと思っていたが、どうも双方向らしい。つまり、どちらかを変更した時にもう片方を呼ぶような実装にしていると無限ループになる。共通部分を切り出す必要がある。だいたい思うように動くが、変な値を入れると妙な動きをする。属性 maxlength"hoge" とすると、属性値は "hoge" に、プロパティは -1 になる。今度はプロパティを "hoge" にすると、属性値は "0" に、プロパティは 0 になる。別にこの通りの動きにする必要はないと思うが。

また、ややこしいことに、value のような属性は双方向ではなく属性には反映されない。頻繁に変更されるものに関しては逐一属性値を書き換えたくないようだ。その辺の細かい作法みたいなのが、以下の "Custom Element Best Practices" に書いてある。

developers.google.com

正直「ベストプラクティス」と書いてはいるが、「お前らちゃんと正しく実装しろよ」ということだと思う。いや、できることならそうしたいけど難しくないっすか。他には例えば「プロパティに値をセットした時にイベントを発火するのは避けるべし」とある。理由は「プログラムから値を変更したんだからどんな値が入ってるかは知ってるでしょ」とのこと。うーん...そう言われればそうな気もするし、そうじゃないような気もする。

感想

とりあえず、「 Custom Elements を使えばアプリがスイスイ作れるぜ」みたいな生易しいものじゃないので万人に勧められないし、開発チームに丸投げしたらコンポーネントがホイホイ上がってくるようなものでもなさそう。いや、上がってくるかもしれないが。本当に一部の実力者みたいな人がちまちま作るか、上のベストプラクティスをうまいことまとめたツールキットなりテストなり、なんらかの仕組みが必要そう。 Custom Elements のチュートリアルに「ユーザーはどんな使い方をするか分からないからね!」とあるんだけど、ほとんどの開発現場では世界中の人に使ってもらうわけではなく、限られた使い方だけを網羅すればいいはずなので。そして限られた使い方しかしないのであれば、まあ React とかでいいよねという話に戻ってしまう。その辺はなんか立場の違いというか、今まで実現できなかったことをボトムアップに構築していく集団と、今までできてたことをいかに高効率でやってくかっていう集団があって、それは両方あっていいと思う。ただ、効率厨が軽い気持ちで触ると難しさに音を上げるという話。覚悟を持って挑もう。覚悟と気合い。

よいお年を。

.ogg ファイルのメタデータを読んでみた

.ogg ファイルのメタデータを確認する作業が発生したので、ブラウザでディレクトリ内の .ogg ファイルのリストを読めるようにしてみた(TypeScript)。

背景

友達のゲームに BGM を提供しました。

ゲームに使用する ogg ファイルにループ位置指定のためのメタデータを埋め込むんだけど、どのファイルにメタデータを入れたのかが分からなくなるのと、変数名のタイポが不安だったので一覧でさっと確認したくなった。というのが経緯。

ちなみに、仕様は RPG ツクールのを踏襲しているらしいので、多分同じことで困っている人は多いはず。まあ欲を言えば音量レベルが揃ってるか等も知りたいけど、時間がなかったので今回はメタデータのみ読めれば OK ということに。

作ったもの

コード

GitHub - jinjor/ogg-vorbis-experiment

こんな風に読める。

f:id:jinjor:20191215230025p:plain

ディレクトリを読みたい

Native File System API が必要で Chromium でフラグを立てたりしないといけないんだけど、今回の趣旨ではないので省略。

Ogg Vorbis を読みたい

公式に vorbis-tools というツール(コマンドは vorbiscomment)が提供されているので、 CLI で済ませたい場合はこれで出来る(Mac なら brew インストールできる)。

GitHub - xiph/vorbis-tools: Command-line tools for creating and playing Ogg Vorbis files.

npm にも .ogg 読むパッケージあるよね絶対。うん、あった。 でもそれじゃつまらないので、折角だから仕様を理解してバイナリを読むことにする。

Xiph.org: Ogg

仕様はここから読めるので、サクッと Google 翻訳して読む。最近は翻訳の精度が良いので下手に自力で読むよりも母国語の方が速く読める(英語が得意なら別)。たまにテクニカルタームが翻訳されてしまうので、そこだけ原文を確認する。

Ogg コンテナを剥がしたい

実は Ogg はパケットを伝送するためのコンテナなので、Vorbis がコーデック本体。なので Vorbis 以外にも Opus とか別のフォーマットのデータが入ってるかもしれないし、音声じゃなくてもストリーミングしたいものならなんでも可能とのこと。

というわけで、 Vorbis を読むにはまず Ogg コンテナを剥がす必要がある。

以下に現時点での浅い理解を図示してみる。(間違ってるかもしれないけど、何も情報ないよりは取っ掛かりとして良いはず。ちゃんと知りたい人は公式ドキュメントを読んでください。思想とかも書いてあるので。)

f:id:jinjor:20191220004908p:plain

Page 単位でパケット列が送られる。パケットは内部的にいくつかのセグメントに分けられる(255 バイトずつ+最後は余り)。1つのパケットは複数のページにまたがることもある。多重化も考慮されていてページごとにストリームの番号が振られている。

コンテナを剥がすと上のレイヤー(上の図の2番目の帯)では、単にパケット列として読めばいい。カプセル化されていて、ちょうど TCP と HTTP みたいな関係。

Vorbis を読みたい

1~3 パケット目がヘッダーで、 1 パケット目にチャンネル数やビットレート、 2 パケット目にコメントの類が入ってる。今回読みたかったのは、 2 パケット目にある任意のユーザーコメント(LOOPSTART, LOOPLENGTH)です。まあここは単にバイナリを読むだけなので、特に図とか解説とかは必要なさそう。

今回の要件はここまでで十分なので、 3 パケット目以降はまだ読んでません。

感想など

バイナリを読むのは楽しい。

そのうちコーデックにも手を出したい。ツールを使うとクオリティ 0 ~ 1 みたいなざっくりした表示しかないので何をしてるのか良く分からないし、再エンコードした時に無駄に音質劣化していないか不安で眠れない。あと Vorbis よりも Opus の方が音質が良さそうなので、そっちも使う機会があるといいなぁという感じ。

Nim v1.0 で簡単なツールを作ってみた

Nim

今朝たまたまこんなツイートが流れてきて、聞いたことある言語だったので気になって触ってみた。 見たところ、静的型付けで文法も結構親しみやすい感じ。その上パフォーマンスが良いとか。ひょっとしてこれから来る言語なのでは。知らんけど。解説は先駆者の記事とか読んでください。最近の言語かと思ったら初登場が 2008 とからしくて意外と歴史を積み重ねてる。

お題を考える

やるからには何か意味のあるものを作らないとつまらないので、お題を考える。

良さそうな題材が浮かんだのでこれで。

インストール

Mac なので brew install nim でサクッとインストール。パッケージマネージャの nimble も付いてくる。

チュートリアル

ここで簡単な文法を学びつつ、コンパイルの方法 num c -r greetings.nim を覚えた。

Part I から Part III まであるので一通り目を通した。と言っても3つ目は主にマクロの話なので、1〜2を読んでおけば大体なんとかなる。とは言え想像よりもボリュームがあるので頑張りが必要。なんかもっとチャラい言語を想像してたんだけど、結構ガチ系っぽい。

言語仕様は結構センス良い感じだと思った。普段やってる TS とかと比べると若干コンパイル時の処理とかメモリの場所とかを気にするようになっているけど、普通のプログラムを書いている分には明示的なメモリ確保とかポインタ操作とかはしなくて良さそう。

早速プログラムを作り始める

チュートリアルは基礎文法中心でアプリの作り方という感じではなかったので、まず何から取り掛かればいいのか分からなかったのだが、まあ nimble init とかしたらプロジェクトの雛形が出来るんでしょと思ったらそうだった。ただプロジェクトの中にもう一個ディレクトリが出来るという謎の動きをしたので、一枚皮を剥がした。プロジェクトのタイプとしては、ライブラリとバイナリとハイブリッドの3つから選べるようだ。

nimble run で確認するサイクルを回そうと思ったけど、なんか上手く動かなかったので nimble build して出てきたものを実行した。

ひたすら API ドキュメントを読みながら進める

ここからはとにかく API ドキュメントとにらめっこしながら進めていくしかない。 JS とかと違って StackOverflow とかによくある書き方みたいな情報が全然ないし。だけど標準ライブラリはかなり充実していて、不親切ということはない。自力で進むコツを掴みさえばなんとかなる(はず)。

まず grep を実行するために child process の実行の仕方(osproc)と正規表現の使い方(re)を覚える。次に HTTP (httpclient)の使い方と JSON のパースの仕方(json)を覚える。アウトプットに色もつける(terminal)。最後に、引数もちゃんとパース(parseopt)して CLI として使えるようにする。色々と一通り触れる良いお題だったっぽい。

ちょっと詰まったのが HTTPS を実行しようとすると実行時に SSL support is not available. Cannot connect over SSL. [HttpRequestError] と怒られる。コンパイル時に -d:ssl を指定しないといけないらしい。nim コマンドはそれでいいんだけど nimble 経由で渡すにはどうすればいいのよって探し回ったら、 nimble build にそのままフラグを渡せるよって閉じられた issue に書いてあった。

あとは stdout.write の後に flushFile し忘れると何も出ないとか、 seq に sort がないとか、seq から配列に変換する簡単な方法がないとか、正規表現バグってないかとか、細かいつまづきポイントはあったものの、全体としては「こんな感じかな、えい」ってやると大体動く。文法が直感的で良い。あ、でも全体的にかなり手続き的な書き方をするので、関数型フリークは満足しないかも。

完成・感想

というわけで完成しました。

標準ライブラリが充実しているおかげでプロジェクトがかなりスッキリしてますね。 TypeScript だと tscargvchalknode-fetch を入れて tsconfig も書いて...となることを考えると、最初から揃ってるのはかなり嬉しい。あとはもう少し情報とコミュニティ規模が〜という感じはするけど、時間が解決してくれることを期待してます。

おしまい。

Netlify Functions + FaunaDB 使ってみた

個人開発でサクッと何か作りたいとき、 Heroku みたいに手軽に「git push はいリリース」なノリのやつがあると便利なんだけど Heroku は無料だと半日寝てるし東京に居ないしアドオンも少しまともに使うと値段が跳ね上がる。ので、なんか良い代替がないかなと探していたら、 Netlify Functions というものを見つけた。静的コンテンツなら Netlify が便利なのは言うまでもないとして、サーバーサイドも何か書きたいときにはこれを使って AWS Lambda を動かせると言うことらしい。従量課金でお金も節約できそう?というわけで、やってみた。

以下、「何も情報がないよりはマシだろう」程度のメモ。日記なので技術記事だと思って読まないほうがいいです。

Netlify Functions + FaunaDB

Netlify Functions 自体についての解説はググれば Qiita やら何やら出てくるので、そっちを当たってください。説明するの面倒臭いです。

せっかくなので裏に DB を置きたいんけど、 Lambda だからって裏を DynamoDB にすると結局 AWS に浸かることになってしまい面白くないので、もう少し調べてみるとこういう記事が出てきた。 www.netlify.com

FaunaDB というのは初めて聞いたけど、スケーラブルなドキュメント DB らしく「World's best serverless database, now with native GraphQL」っていう売り文句で、ちょっと面白いのでやってみるかと(結局 GraphQL 使ってないけど)。

作ったもの

github.com

REST API (Webhook) のエンドポイントを動的に追加して気軽にテストできるやつです(前回 Heroku で作っていたやつのマイグレーション)。メールのテストに使える MailSlurp というサービスがあって、それのパクリ。

感想・ハマったポイントなど

正直 Netlify でこんなにハマるとは予想してなかったというか、分かったら簡単なんだけど上手くレールに乗らないと死ぬ。ローカルテストとかスムーズに開発するために netlify-lambda というツールを使うんだけど、 TypeScript でやろうとすると結構ハマりポイントが多い(願わくば TypeScript デフォルトにして欲しい)。

babel 設定

ここ に書いてある通り .babelrc が必要。 TypeScript on Node で babel が必要なのかは疑問だが、内部的に使っているようなので仕方がない。で、 async/await を何の気なしに使っていると babel がエラーを吐くので、フロントエンドだと @babel/polifill を入れて解決するところだけど、今回は Node.js 8 以降で動かすのでターゲットの方を変える

webpack 設定

ここで webpack が登場するのは、 Lambda に zip を送信する都合上、必要な依存だけを node_module からかき集めるのが都合が良いという話だと思う。いつものフロントエンドじゃないか助けてくれ。

デバッグしてたらいきなり i is not a function と出るので(sourcemap どうすれば利くの)ここ に従って webpack.config を書くことでまずは minify を解除。すると require is not a function になるのでどうしたものかと調べると同じハマり方をしてる人がいて、早い話がこの Issue の UPDATE2 を書くと解決する。

Netlify Dev

netlify-lambda の後発として netlify dev という netlify-cli のコマンドがあって、バンドルはしないけど必要な依存は解決してかき集めてくれるらしい。というのを後から知って「じゃあ netlify-lambda もう要らないのか」と思ったんだけど、どうも TypeScript とかが必要な時は結局 netlify-lambda が必要らしきことが README に書いてある。うーん、なんかもうちょっと綺麗にまとめてくれないものだろうかと思うものの、 netlify dev はまだベータなので改善を待ちたい。

Express

実は Express が使える。FaaS と言えば基本的に1エンドポイントにつき1関数だと思うけど、全てのリクエストを1関数に集約すると、その中でルーティングできるようになる。それでいいのかよという気もするが、そんなに細かいチューニングがしたいわけでもないし、 Express でサクサク書きたい気持ちが上回ってしまうのだから仕方がない。

詳しくはこの記事に書いてある通り、 serverless-http というライブラリを使うとサクッと Lambda と express の辻褄を合わせてくれる。

Decoder

デコーダーと言えば Elm のあれなんだけど、 TypeScript でも同じようなことができるのでやってみた。何かと言うと any をバリデーションして型をつけてくれる。例えば下のように Decoder<User> を作って userDecoder.run(value) すると、成功時には anyUser になって返ってくるし、失敗時には例外が発生する。あとは express と組み合わせて 400 を返せば OK 。

export const userDecoder: Decoder<User> = object({
  age: number,
  name: string
});

使い方はこのリポジトリがわかりやすいと思う。まだ PoC なので実装は適当だけど、最低限は使えるはず。実務だと Swagger から JSON Schema を引っ張り出して ajv とかでバリデーションするんだけど、まあ軽い用途だとこのくらいで良いよねという感じ。

FaunaDB

https://fauna.com/ からサインアップしてドキュメントを読むと大体雰囲気がわかる。ダッシュボードはかなり使いやすい方だと思う。 GraphQL も使えるけど機能に制限があるらしいので、今回は FQL (Fauna Query Language) というものを使った。FQL はかなり癖が強くとっつきにくいが、慣れてしまえば API ドキュメントを読み読みしながら書けるようになる。以下は TypeScript の SDK を使って書いてみた様子。

q.Select(
  "data",
  q.Filter(
    q.Map(
      q.Paginate(q.Match("results_by_key_order_by_requestedAt", key), {
        size: 100000
      }),
      q.Lambda(["_", "ref"], q.Select("data", q.Get(q.Var("ref"))))
    ),
    q.Lambda("x", q.GTE(q.Select("requestedAt", q.Var("x")), from || 0))
  )
)

正直これ AST を直書きしてるのと変わらないので、SQL みたいに文字で書きたくなってくる。とは言え、これはこれで composable なので上手くヘルパーを使えば楽になるかもしれない。一応、型もついてる。

あと、情報源がほぼ公式のドキュメントしかなくて、そこで理解できないとたちまち詰む。まあでもマイナー技術ってそんなものというか苦労するからこそ達成感を感じたりするし、作者も高確率でエゴサしてるので Twitter でちょっと呟くと飛びついてくるのが面白い(前例: Elm, ArangoDB, MailSlurp, etc.)。

全体としての感想

  • TypeScript で書こうとすると webpack や babel が絡んで難易度が上がる。TypeScript の開発がもう少しスムーズになるようにサポートして欲しい。その分だけ Heroku より手軽ではないが、 AWS を直に触るよりはかなり楽だと感じる。
  • FaunaDB は今後どうなるか未知数だが、 Netlify Functions と親和性は良さそうなので個人的にはもう少し使いたい。今のところ無料だし。
  • 手軽デプロイ系だと他には aws-cdk とか Firebase とかも気になってる。あと Netlify Edge 。

GW 進捗

割と捗った方だと思います。

4/27

  • DreamNotes
    • ScriptProcessorNode でノイズを再実装
    • 矩形選択を実装
    • ピアノロールのルーラーを実装
  • Starry Sky (Cupsule)

4/28

4/29

  • DreamNotes
    • プレイヤー実装
    • 「時の回廊」公開
  • リアル脱出ゲーム
    • 6/9 部屋
  • 海外旅行の計画
  • ゆるゆり 16 巻

4/30

  • DreamNotes
    • プリセットに organ1, organ2, base3, wind 追加
    • Octave パラメータ追加
  • 「FMシンセの新しいトリセツ」読了

5/1

  • DreamNotes
    • OSC / Fine 実装
    • FM / Detune 実装
    • プレーヤーの iOS / Safari 対応

5/2

  • DreamNotes
    • ノートのドラッグ実装
    • ノートのリサイズ実装
  • 複雑GUI

5/3

  • DreamNotes
    • アナライザー実装
    • パフォーマンス改善

5/4

5/5

5/6

  • DreamNotes
    • インサーションエフェクトの追加・削除
    • Distortion 実装
  • ドラム体験レッスン
  • シンセサイザーがわかる本」読了

GitHub Actions で Puppeteer をインストールして実行

分かってみれば全く大したことはない...けど誰かの役にたつかもしれないので足跡を残しておく。

.github/main.workflow

ワークフロー定義。カスタム定義の Action を使う(作り方はこの辺)。 UI では以下が上から串刺しになって見える。

workflow "Build and Test" {
  on = "push"
  resolves = ["Test"]
}

action "Install" {
  uses = "./puppeteer"
  runs = "npm"
  args = "install"
}

action "Test" {
  uses = "./puppeteer"
  needs = ["Install"]
  runs = "npm"
  args = "test"
}

コンテナは Action ごとに作られるけど、前の結果は引き継がれているように見える。

puppeteer/Dockerfile

Puppeteer を docker で使うためのテンプレの前半をコピペ。 同じディレクトリに entrypoint.sh を定義してもいいけど任意っぽい。

# https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md#running-puppeteer-in-docker
FROM node:10

RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
    && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
    && apt-get update \
    && apt-get install -y google-chrome-unstable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst ttf-freefont \
    --no-install-recommends \
    && rm -rf /var/lib/apt/lists/* \
    && rm -rf /src/*.deb

Puppeteer を root で使う時に --no-sandbox フラグが必要になるので、元ネタだとこの続きで USER を使っているんだけど、 GitHub Actions の制約で USER が使えないのでその部分は諦める。

index.js

仕方がないから CI の時だけ --no-sandbox つけようかということで、適当な環境変数を探す。

// https://developer.github.com/actions/creating-github-actions/accessing-the-runtime-environment/#environment-variables
const ci = !!process.env.GITHUB_ACTION;

// https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md#setting-up-chrome-linux-sandbox
const options = ci
  ? { args: ["--no-sandbox", "--disable-setuid-sandbox"] }
  : {};
const browser = await puppeteer.launch(options);

おしまい。

CSS フレームワークを使いたくない

CSS フレームワークが辛い。

ここでいう CSS フレームワークとは Bootstrap とか Bulma とかそういうやつのことである。昔から自分はこういうのが苦手で、一定の便利さは感じつつもどうしても馴染めないという状態が続いていて、それでも「それは使い方が悪いだけで、ちゃんと使いこなせばペイするんだろう」と思って今までズルズル使ってきてしまったのだが、やっぱりそれでもどうしても辛くなり脱フレームワークしようと思う。

もちろん使いこなせる人には使いこなせるんだろうし「使うべきでない!」という主張をするつもりはない。頭のいい人には使えるんだろう。昔は「今すぐ〜すべき 10 の理由」みたいなことを適当に書いてたんだけど、どうせ自分がやってることは「 Web 系」のメインストリームからは外れてるんだろうし、合わせるつもりもなければ合わせさせるつもりでもない。使う理由も使わない理由も人それぞれだけど、少なくとも自分には無理でしたというだけの話。

前置きが長くなった。以下、個人的に CSS フレームワークをもう使いたくないなと思った理由。

CSS を書く方が楽

そもそもこれ。 CSS はモジュール化しにくいなどの欠点はあれど、生で書いてもそれなりに使いやすい。だからフレームワークで間に合わなかったり気に入らないスタイルがあるとすぐに CSS を書きたくなってしまう。しかし独自にスタイルを書いていくとフレームワークの世界観を壊してしまうので、なるべくフレームワークの文法とシステムを使って書こうと思うわけだが、これが全然思うように行かなくて歯がゆい。「ここを目的のスタイルにするためには、あのクラスとこのクラスを組み合わせて...」というのがとにかく難しくて「こんなの CSS 書けば一瞬で解決するじゃないか」という気持ちと戦いつつ、頑張ってフレームワークの文法を使って書いてみるも、結局思った通りにいかず最終的にぎこちなさの残ったスタイルになってしまう。

学習コストが高い

フレームワークを使うんだから楽がしたいのだが、学習コストがとにかく高い。もちろん上手くレスポンシブをやってくれたり、その辺はその道の達人が作ってるはずだから、払ったコストに対してペイするものだと思ってやっている。が、それでも難しい。なんどもなんども忘れて公式サイトを確認しにいくがそれでも忘れてしまう。 CSS もなんども調べなおすんだけど、こっちは Web 標準だからまだ良くて、フレームワークは本当にその場限りの知識になってしまうからガッツリ時間をかけて学習する気にもならない。

サンプルからルールが読み取れない

「こう言う感じのヘッダが作りたかったらこういう風に書くんだよ」というサンプルコードと絵が対になって紹介されているドキュメントが良くある。サンプルそのままの見た目でいいならそれをそのままコピペでいいんだけど、細かいところがちょっと予定と違っていたりして、その部分だけを変えようとすると途端にわからない。要は、 ABCDEFG という7種類のクラスが 30 行くらいの中にわっと書いてあって、そこからどういうルールでスタイルが適用されているのかを推測するゲームになる。

運が良ければ要素ごとに分解して書いてあるけど、それでも「こんな感じ」とサンプルがどーんと乗っかっていてかなり曖昧。わからないから「このクラスとこのクラスをネストさせるとこういう風になるんだろうな」と適当に想像したり、もっと知りたいときは「検証」で開発者ツールを開いて調べるんだけど、サンプル用に特別に当てられたスタイルとかもあって、自分のアプリ上に持っていくとその通りのスタイルにならなかったりする。で、開発中のアプリとドキュメントを行ったり来たりして「ここを変えたらここが変わったからつまり...」と何度かあれこれ試しながら見た目を整える作業がひたすら辛い。で、完成しても結局ルールはわからないまま。

意図のわからないルール

ルールを理解しようと思ったらコードを読めばいいんだが、 .foo > .bar:not(.is-hoge) .fuga みたいなセレクタを見て「あーそうですか」と理解できるならそもそもこんなフレームワーク使わないし、理解したとしてもその上でそのスタイルと整合性の合うように別のスタイルを組み合わせたりするのはさらに難しい。そもそも汎用的なコードというのはあらゆる組み合わせの可能性を考慮して書かれているので、普通のコードよりも複雑になりがちである。

で、コードを読んだ上でそれでも意図がわからないルールがあったりする。親要素がネガティブマージンになっていて子要素のスタイルと組み合わせることでプラマイ0になるようなやつとか、そんなトリッキーなことをして何がしたいのかが分からない。ドキュメントを探しても書いてないし、 StackOverflow には同じところで詰まってる人がいる。結局「よく分からないけど親要素にボーダーをつけたら上にはみ出るからダメなんだ」と納得しないまま進むしかなく、とても気持ち悪い。

HTML がどんどん汚くなる

本当はこれを一番に書いても良かったんだけど、話の流れから結果的にここになった。でもこれは本当に言いたい。そもそも CSS はドキュメントの構造とスタイルを分離しようという思想なんで、HTML にスタイル指定をモリモリ書いていくのはおかしいという感覚は前からあって、それでも「いや頭のいい人たちが OOCSS とか SMACCS とか色々考えた結果こうなったのだから、これも必要悪か」と渋々従っていたのだが、それでも読みにくいものは読みにくい。じゃあ古き良き CSS というか HTML にはスタイルを全く書かずに意味だけを記述してやっていくかというと、それはそれで今度は CSS の方が大変になって SASS を使ってやれ継承だという話になりそうなので、それも避けたい。

思うに、汎用性の高さゆえにクラスの記述が爆発している。例えば、開発中のアプリではカラムの幅の持たせ方は1種類しかないにも関わらずフレームワークは4種類をサポートしていると、そのうちのどれかを選ぶためにクラスの記述が必要になる。それがパラメータの数だけ掛け算になって <div class="columns is-x"><div class="column is-a is-b is-c"> ...</div></div> のように 5, 6 種類のクラスの組み合わせでようやく思ったのに近いスタイルになるが、 CSS を自分で書いていいのであれば多分1種類でいい。100 種類のボタンが用意されていても実際にアプリで使うのが 3 種類なら、残りの 97 はノイズなのだから綺麗に視界から消えて欲しい。

クラスも多いがネストも多い。例えば次のように foo-childbar を同居させて書きたい時に

<div class="foo">
  <div class="foo-child bar">
    <div class="bar-child">
    </div>
  </div>
</div>

これが何故かできなくて、次のようにネストさせないとスタイルが合わなかったりする。

<div class="foo">
  <div class="foo-child">
    <div class="bar">
      <div class="bar-bar">
      </div>
    </div>
  </div>
</div>

これが積もり積もって、頭の中では「2段にすればいいかなー」と思っていたら蓋を開けたら5段、6段になっていてゲッソリする。詳細は知らないが、これも汎用化するというフレームワークの指名ゆえに生まれた無駄だと思っている。

直感的でないレスポンシブの挙動

直感的かどうかというのは、主観的な話なのでその人のバックグラウンドによるんだとは思うが、ウインドウ幅を縮めた瞬間にガラッと見た目が変わってしまうのでやたらびっくりしてしまう。 Bulma なんかは、ちゃんとトップに「モバイルファースト」と大きく書いてあるので、それをあんまり気にせずにデスクトップファーストで作り始めた自分に非があるのは認める。だけど今までデスクトップファーストでしか作ったことないし、いきなり言われてもねというか、ブログとか簡単なサービスはいいとして高機能な UI を小さい画面で表現するのはかなりのデザイン力を要求されるし、なんかフレームワークを導入したから自動的にモバイル対応できましたにはならないと思う。

というわけで、ただでさえ設計力がいるんだけど、それはどの画面でどの UI を使うにしても常に考えていなければいけなくて、例えば「横に並べたいときはこのクラスあてりゃいいんだな」って思ってデスクトップサイズで開発してて、あとでモバイルサイズにしてみたらガッと縦になって「アイコン1個のために1行使うとかアホかー」になる。いやそれでも流石にレスポンシブフレームワークを謳うだけはあって、予想を裏切ってもそれなりに整った見た目にはなる。なるけどコレジャナイ感は拭えないし、何より自分で描いた覚えのない画面が急に出てくるのが気持ち悪いので最悪デスクトップと同じでもいいんじゃないかと思えてくる。

結局必要になるカスタム CSS

確かにフレームワークの方でカスタマイズできるように変数を用意してくれているので、ある程度はなんとかなる。けど、絶対にそれだけじゃ足りなくて必ず自前でなんらかの CSS は結局書くことになる。で、一度書き出すと「めっちゃ楽だ」ということに気づいてしまい、すぐに割れ窓になる。「こんなん横並びなんだから flex で済むだろ」でやってくと 100% 思い通りになる。で、気づくとレスポンシブがいつの間にか壊れている。既存のスタイルの何かと競合して壊れたんだろうが、面倒なので別のスタイルを上書きして直す。

あと大きいのが、デザイナーさんが居てくれる場合はほぼ 100% 自前のスタイルを書くことになるので、フレームワークがもともと用意したスタイルは最終的に影も形もなくなる。それは覚悟してるんだけどじゃあなんでフレームワークを使うかというと、苦手意識のあるレスポンシブをなんかよろしくやってくれるだろう期待があったりするんだけど、その期待は上で言った通り砕かれてしまった。変数も最初は「$danger がこの色で...」と当てはめていくだけなんだけど、そのうちに「薄い灰色よりももっと薄い灰色」とかが出てきて、最初は /* この部分は独自拡張 */ みたいにしていたけど、そのうちに独自拡張だらけになる。で、自分で CSS 書くを前提になると既存のスタイルがどんどん邪魔になってきて「なぜかスタイルが当たらない」と思うとだいたい優先度で負けていて !important をつけていくことになる。

この段階になると「半分くらいのスタイルがフレームワーク由来、残りはそれを上書きして拡張したものか完全に独自のスタイル」という状態になり、フレームワークと自前 CSS の境界がとても曖昧になる。HTML もフレームワーク由来のクラスと独自クラスが入り乱れて、非常に混乱してくる。

コードをすぐに参照できる場所に置いておきたい

フレームワークのコードは node_modules の奥深くに埋もれていていてなかなか目にする機会がないし、膨大なコードの中からアプリケーションで実際に使われているわずかな情報を抽出するのも難しい。100% 自分が意図して必要な分だけスタイルを記述して src 以下に置いておくのが、やっぱり一番コンパクトで良いのではないかと思う。「それができりゃ苦労しねえ!」って話なんだろうけど、トータルで見るとそのほうが辛くない気がする。

CSS の進化

CSS フレームワークの概念自体が Bootstrap で広がったんだと思ってるんだけど、その頃から比べたら CSS はかなり進化しているし、少なくとも「何かしらの hack をしないと使い物にならない CSS 」ではなくなったと思う。 IE11 以上でよければ Grid も使える。ブラウザの差異を吸収するのは post css とかもやってくれるし、まあそもそもあまりマニアックな機能は使わないように気をつけていれば崩れないと思う。というわけで、臭い CSS に蓋をする役割としてはどんどん価値が減っていると思う。

フレームワークをやめたら綺麗になった

ともかく辛さから逃れたい一心でフレームワークを削っていったら、HTML のネストはどんどん減るし、クラスも激減してコードも見やすくなった。それだけではなく、今まで微妙だなと思いながら放置していたスタイル崩れがどんどん解消していった。「フレームワークの流儀で書かなくてはいけない」という縛りプレイが悪い方向に働いて思うように書けなかったのと、HTML が複雑化してスタイルを直すのも困難だったことが原因だったと思われる。

何がいけなかったのか?

反省点としては、「とりあえず CSS フレームワークは導入しておけば楽になるだろ」という思考停止、「自分の書く CSS なんてたかが知れてるだろう」という自信の無さ、「自分が気づいていない何かをフレームワークがカバーしてくれるんじゃないか」という淡い期待を抱いてしまったことだろうと思う。確かにメディアクエリの知識とかは足りないんだけど、それはやりながら覚えればいい話。

自前で書くのは果たして楽なのか?

そんなことはないと思う。あと当たり前だけど CSS のスキルとコツが要る。ここではフレームワークを dis ってるんだけど、考え方は盗んだ方がいい。という意味では、やったこと自体は無駄にはなってないんだけど、ちょっと遠回りしすぎたねという感じ。地道に CSS スキルをつけていこうと思う。関係ないけど Grid 最高。