ジンジャー研究室

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

CSS in JS(Elm)したら想像以上に良かった

f:id:jinjor:20160530165006p:plain

追記

最新の感想も合わせてご覧ください。

jinjor-labo.hatenablog.com


React界隈では結構前からCSS in JS」と言って、雑に言うと「CSSはイケてないからJSでインラインスタイルを書いてしまえ」という話がある。(ちゃんと知りたい人はこちら

自分も前々からCSSは変数が使えないとか名前が被るとか諸々イケてないのは同意してたんだけど、じゃあJSで書くのが良いかと言われたら「いや流石にロジック汚れるんじゃね?」とか「CSSの便利機能を捨てて平気なの?」とか色々と懐疑的だったんだけど、1~2か月書いてみたら想像以上に良かったので感想を書くことにした。

まず一番に主張したい部分を先に言うと、こう。

(誤解)JSのコードがスタイル記述で汚れる
(正解)JSのコードがスタイル記述から解放される

前提

  • 実際に書いたのはJavaScriptではなくElmなので以下は全てElmコードで書くんだけど、本質は変わらないはずなのでJavaScriptに置き換えて読んでください。
  • デザイナと協業したりプロダクションで実際に使ったわけではないので、その辺のノウハウはないです。
  • パフォーマンスの話はないです。遅くなったら考えようと思ったけど遅くならなかったので。
  • CSSに対する問題意識はある程度共有できていて、React的な書き方をしているWebアプリが対象。

というわけで、ここからが感想というかやってみて分かったこと。

スタイルは今まで通り分離できる

まず誤解されているかもしれないところを解消する。「CSS in JS」と言っても、メインのロジック中にベタベタスタイルを記述していくわけではなくて、スタイルは普通に分離して書くということ。基本的には今までstyle.cssに書いていた内容をstyle.jsに書くだけ。

Elmではこんな感じで「Styles.elm」を記述した。雰囲気を見てもらうのが目的なので詳細な内容は追わなくてOK。

module Styles exposing (..)

zIndex =
  { subView = "600"
  , messageBar = "700"
  , contextMenu = "800"
  }

messageBar =
    [ ("position", "absolute")
    , ("color", "#fff")
    , ("z-index", zIndex.messageBar)
    , ("padding", "5px 10px")
    , ("transition", "height opacity 0.8s linear")
    ]

successBar =
  messageBar ++
    [ ("background-color", "#4c5")
    , ("opacity", "1")
    ]

errorBar =
  messageBar ++
    [ ("background-color", "#d45")
    , ("opacity", "1")
    ]

noneBar =
  messageBar ++
    [ ("opacity", "0")
    , ("pointer-events", "none")
    , ("background-color", "#4c5")
    ]

CSSを書いてるのと何ら変わらない。使用する側のコードは以下。

successView msg =
  div [ style Styles.successBar ] [ text msg ]

errorView message =
  div [ style Styles.errorBar ] [ text message ]

noneView =
  div [ style Styles.noneBar ] [ ]

今までclass属性でスタイル指定していた部分をstyle属性に直すだけ。スタイルはStyle.elmに書いてあるから、使用する側のコードは全く汚れない。今まで通り普通に分離できる。

便利なCSSはそのまま使える

「スタイルをJS」でと聞くと、今までのCSSの知見をすべて捨てて何から何まで自力で座標計算から何からやらなきゃいけないような気がしてくるが、そんなことは全くない。上の例に示したように、positionz-indexpointer-eventsのようなほぼすべてのCSSの便利プロパティは健在だ。Virtual DOMを使っていてもtransitionが動作しないなどのバグも特に無かった。

スタイルの合成が可能

これは「CSS in JS」のそもそもの動機なので言うまでもないのだが、使いまわせるスタイルは変数にしておくことで簡単に合成できる。上の例でいうと、successBarmessageBarに成功時特有のスタイル(緑色とか)を足し合わせることで定義できる。

OOCSS(Bootstrapのような)でも合成は出来るのだが、必要なclassを全て書き連ねる必要があるのでHTMLがどんどん汚れていってしまう(class="message-bar message-bar-success")。スタイル定義側で合成すればHTMLはクリーンに保てるし、「HTMLにはセマンティクスを記述してスタイルはCSS側で~」という昔ながらの価値を取り戻すことが出来る。

スタイルを関心別にまとめて管理

変数を使うことで、よりよいスタイルの管理も可能だ。例えば「色は全部まとめて一つのファイルに定義しておく」とか「z-indexは一か所にまとめて管理する」のようなニーズにも自由自在に対応できる。

動的な値を使った計算が可能

CSS(Sassなどのプリプロセッサを含む)にどうしても出来ないことの一つに「実行時に動的に変化する値を参照」というものがある。「画面サイズ」がその典型例で、「メインコンテンツの高さは画面全体の高さからヘッダの高さを引いた値」とか「コンテクストメニューが画面からはみ出ないように位置を調整する」みたいなことが出来ない。仕方がないので今までそういうロジックは例外的にJavaScript内に書いていたのだが、「CSS in JS」では動的に引数を与えることが出来る。

以下は引数を用いたスタイル定義の例(Elm)。

-- 画面の高さを引数に取る
mainView windowHeight =
  ( flex ++
      [ ("height", px (windowHeight - headerHeight))
      , ("position", "relative")
      ]
  )

数値だけではなく、フラグ的なものを突っ込むこともできる。タブなどを選択したときのactive状態などが典型例。実際のコードからの抜粋。

-- 数値(インデックス)と真偽値の2つを引数に取る
subViewTab index active =
    [ ("position", "absolute")
    , ("top", px (10 + index * 130))
    , ("left", "-30px")
    , ("width", "30px")
    , ("height", "120px")
    , ("padding-left", "6px")
    , ("line-height", "135px")
    , ("background-color", "#eee")
    , ("z-index", zIndex.subView)
    , ("cursor", "pointer")
    , ("border-radius", "8px 0 0 8px")
    , ("box-shadow", if active then "" else "inset -4px 0 4px rgba(0,0,0,0.03)")
    , ("box-sizing", "border-box")
    ]

今まではそれが出来なかったのでHTMLやJavaScriptに書かれていたが、本来このような記述は普通にCSSに書かれるべきもののはずだ。もちろん何でもかんでもロジックを突っ込んでしまうと責務をオーバーしてしまうので、そこは節度を守ってやる。

CSSを使った方が良い場面もある

CSS in JS」だからと言って、別にCSSを使うことを禁止する理由もないので必要ならCSSを使う

まず、インラインスタイルで難しいこととして最初に思い浮かぶのが:hover。流石にこれはCSSでやらないと面倒だろう。JavaScriptでも出来なくはないが、enter/leaveイベントを自力でハンドルする必要があるし、じゃあそのホバーしている状態がロジックとして必要になるケースがあるかと言ったら、まぁない。だから:hoverは諦めて普通にclass属性を使うことにした。

次に、reset系CSS。これもCSSでやっておかないと面倒なことになる。やらない場合「全てのスタイルはこのベースのスタイルを継承してください」のような厄介なルールを作ることになる。嫌な予感しかしないので最初から避けるのが良さそうだ。

弱点に見える部分もほぼ代替手段がある

その他、気付いた点。

  • animationはうまく出来ない。JavaScriptでやる。※未検証
  • ::before/::after疑似要素は普通にJavaScript/HTMLに書けば問題なし。
  • メディアクエリはおそらくフラグで出来る。※未検証
  • ブラウザのツールで同じスタイルを一括変更してプレビューできない。例えば、<li>要素の最初の一つをブラウザ上で変更しても残りには反映されない。JavaScriptのスタイル定義を書き直して画面をリフレッシュする必要がある。
  • BootstrapのようなCSSフレームワークが使えない。それの問題を解決したいので仕方がない。「CSS in JS」用の良いフレームワークが出てくるのを待つか、自分で作る。

自分が納得のできるメリットを見つける

CSSの弱点」とかで調べるとモジュラリティがどうとかいう話がよく出てくるのが、ぶっちゃけよく分からなかった。具体的にどういうコードだとモジュラリティが上がった状態で何が解決するのかよく分からない。

だから「z-indexがー」でも「BEMがめんどくさい」でも「書いてて快感物質が出る」でもなんでもいいけど、理屈はさておき自分の実感できる理由で採用すれば良いんじゃないかなと。逆に実感できないなら主張してもあんまり意味がない。たとえば名前空間の問題も理屈上そうなんだろうけど、自分がそれで困るプロジェクトに関わってなくてよく分からなかったので、ここには書かなかった。あと、CSSではなくてJSのモジュールとして書けるのでエコシステム的にも良いと思っていて、本当はそこも言いたいんだけど試す機会がなかったので誰か知見を持っていれば聞きたい。

まとめ

最初に感じた「スタイルが分離できなくなるんじゃないか」という懸念とは真逆で、むしろ今までJSでゴリゴリ書いていたスタイルに関するロジックが、ちゃんとあるべき場所に収まった

レッツトライ。

自分自身の反省でもあるんだけど、固定観念は良くないと思いました。

以上。