CSS
CSS設計

CSSをRailsとゆるふわにお付き合いさせる話

More than 1 year has passed since last update.

こんにちは🙌、pixivFACTORYでフロントエンドエンジニアをしているラグです😇
ピクシブ株式会社 AdventCalendar 2017 13日目のこちらでは、そこそこページ数の多いRailsアプリケーションでのCSS設計についてお話します:railway_track:

TL;DR

  • namespace classを使おう💪
  • namespace classは「誰が考えても同じクラス名になる」ことが重要
    • Railsなら View のファイルパスで正規化するとよさみがある

どういう問題が起きていたか

pixivFACTORY には、LPやユーザーのパーソナルなページを含めて、100ページ弱くらいのViewがあります。
これらのスタイルはSass上ではURL毎に分割されていましたが、実際のクラスは全てグローバル空間にベタ置きされていました。

Probrem

これによってどういう問題が起きるかというと

  1. 特定ページ専用のコンポーネントに雑なクラス名をつけることが出来ない
    • 適当なクラス名をつけ続けるるとほぼ確実にバッティングを起こす💥
    • サービス全体のページ数に比例して.very-long-long-long-class-nameみたいなクラスを増やすハメになる🙃
    • 後任の首が締まっていくスタイルが避けられない🚨👮🙅🚨
  2. あるクラスにどれくらいの影響範囲があるのかSass上から絞り込めない
    • URL毎にファイルが分かれてても、本当にそのURLでしか使われてないのか分からない🤔

特に 1. の問題は最悪です。クラス名の星空の中、きらびやかに個性輝くクラス名を考えるために多くの時間をかけることになります。考え抜いたキラキラクラス名が他のページで既に使われていたりしたら目も当てられません😩

この問題をいい感じにするには「スタイルの影響範囲を(属人性なく)予測可能にする」ことと「雑なクラス名をつけても将来的な負債になりづらい」ことが必要です。

で、どうする

CSSの設計といえばBEMMOCSSSMACSSなどの手法が有名ですが、これらはそこそこの規模に成長したコードベースへ後から基盤ルールとして適用するのはあまり現実的ではありません😔 サービス中に存在する様々なコンポーネントを正規化したり、それらに対して区別できる名前をつけるなど、デザイナーとエンジニアが結構な時間をかけてデザイン全体を再設計する事になります🤔🤔

それができればまた素敵な世界が待っているかもしれませんが、そこまで気合を入れて頑張らずにもう少しゆるふわな設計を考えます🤗

namespace class

上に上げたような CSS 設計手法や、現状のフロントエンドの問題点や状態を考慮して、「namespace class」で全体を設計することにしました。(僕が勝手にそう呼んでいるだけなのでググってもC#とかC++系の話しかヒットしません、あしからず…)

「namespace class」は、1つViewに対して一意のルートクラスを設定する手法です、愚直ですね。Wordpressで似たような仕組みを見た方もいるかと思います。

RailsではViewがapp/views/users/show.htmlのように配置されていますので、ここから.view-users-showのようなクラス名にすることで、1つのViewに一意なクラス名を付与することが出来ます。規約的な名前付けなので、チームにちゃんと共有すれば属人性もかなり抑える事ができます。Rails-wayに相乗りです。

このクラス名で重要なのは以下の2点です

  • プレフィックスがnamespace用に予約されていること
    (他の用途のクラス名にそのプレフィックスが使われない事)
  • URLを基に正規化しないこと
    (/users/:id:idの正規化で頭を抱えることになる)

サイト全体のリファクタリングの際も、Viewのルートとスタイルにnamespace classを付与していって、壊れたら直すを繰り返していくだけだったので、そこまで高くないコストで安全性を手に入れられました。

現在のpixivFACTORYはnamespace class + BEM + いくらかのコーディング規約を組み合わせることで特定のページに対してのスタイリングがかなり高速に行えるようになっています。
業務的にはLPのコーディングの機会が多いのでこのリファクタリングはやっておいてよかったという感じです。(脳内が虚無でもコーディングが終わるようになった)

users/show.sass
.view-users-show
  // 特定ページ限定のコンポーネントは `._`からクラス名を始める
  ._Head
    position: relative
    margin: 32px 0
    font-size: 32px
    line-height: 1

  ._Head_Menu
    position: absolute
    right: 16px

users/show.html
<div class="view-users-show"><!-- 全てのビューはこの形式のルートクラスから始まる -->
  <div class="_Head">
    ステータス

    <div class="_Head_Menu">
      ...
    </div>
  </div>
</div>

namespace classの問題点

パーシャルの再利用問題

pixivFACTORYでは割とよくpartialを利用しています。使い方としては特定のモデルに共通のViewを使うために、コンポーネントとしてpartialを使っているような感じです。

このpartialに対してもnamespace classを付与していくと.view-*の中に.view-*が登場してしまい、以下の問題が発生します。

  • partial は他のViewに埋め込まれる関係上、marginなどのプロパティが他のView上から変更される可能性がある
    • もしこれを .view-a .view-b のようなセレクタで行うと、namespaceの独立性が保証できなくなる
  • 呼び出し元のViewとpartial内要素でクラス名がバッティングしないことを保証できない
    • View内要素 .view-orders-show ._Orderとpartial内要素 .view-orders-order ._Order があった場合
      .view-orders-show ._Order セレクタが .view-orders-order ._Order へ影響を与えてしまう

ちょっと恣意的な例ですがコード的には以下のようなことが起きるということです。

例えばこのようなViewがある.html
<div class="view-orders-index">
  <ul class="_OrderList">
    <li class="_Order">
      <%= render 'order' # partial view %>
    </li>
  </ul>
</div>
そのViewにこのようなスタイルを当てる.sass
.view-orders-show
  ._Order
    width: 80%
これらをレンダリングするとなんとこうなる.html
<div class="view-orders-show">
  <ul class="_OrderList">
    <li class="_Order"> <!-- → ここだけを`width: 80%`にしたかった😎  -->
      <div class="view-orders-order">
        <div class="_Order"> <!-- → ここも`width: 80%`になっとるやないかい😇 -->
          <h1>手帳型iPhoneケース</h1>
        </div>
      </div>
    </li>
  </ul>
</div>
order.html
<!-- ._Order が含まれることがパッと予期できただろうか? -->
<div class="view-orders-order">
  <div class="_Order">
    <h1>手帳型iPhoneケース</h1>
  </div>
</div>

残念ながら現状この問題に対するよさげなアプローチ方法は思いついていません…

可能であればコンポーネントとして、.Componentをpartialのルートクラスにしてしまえばよいかと思います。ゆるくやるなら「被ってないか気をつけような」運用も許されると思います。

pixivFACTORYでは、現状以下のような方法を取っています。(真似しないで)

template-order-componentsクラス以下に複数のコンポーネントのスタイルを書いておいて…
.template-order-components
  ._OrderItem
    display: flex
    flex-flow: column

  ._PriceTable
    text-align: right
template-order-componentsクラスをつける事でそのViewで利用可能にする
<!-- app/views/cart/show.html.erb -->
<div class="view-cart-show template-order-components">
  <div class="_OrderItem">
    <h1>#{@order.items.first.name}</h1>
  </div>
  <div class="_PriceTable"></div>
</div>

<!-- app/views/order/new.html.erb -->
<div class="view-order-new template-order-components">
  <div class="_OrderItem">
    <h1>#{@order.items.first.name}</h1>
  </div>
  <div class="_PriceTable"></div>
</div>

しかしこの方法は、CSSの影響範囲の予測性を下げるのと!important地獄を誘発するのでオススメしません…🙅🙅🙅(グローバルにベタ置きよりは良いかなという具合です)

CSSサイズの肥大化

CSSプリプロセッサを使ったことがある方は気になっていると思いますが、この手法を使うとクライアントへ配信されるCSSのサイズは大きくなります😳

サーバー側でgzipを使えるようにするとそれなりにサイズが削れるので、有効化されてない方はまずはそちらを検討しましょう🗜️(開発環境のminify済みで555KBあるCSSが78KBくらいまで圧縮できてます)
あとはページが表示されるまでの時間と開発効率のどっちを取るかという話になってきます。

HTTP2が使える環境ならページ毎に出力されるCSSを分割してみるのも手かもしれません(assetsの設定が少し手間ですが…)

Sassでハッシュ関数が使えたなら、namespace classをハッシュ化して先頭数文字を取り出す、というようなことをすればもうちょっと切り詰められそうですが、Sassにはそのような関数がビルトインされていないようなので理想上の話です🙄

まとめ

コンポーネント単位のCSS設計については多くの言及があるものの、クラシカルな中大規模Webサービス向けのCSS設計についてあまり言及が見当たらなかったので考えをまとめてみました😊
今日日はSPAが流行しているので、そういう文脈なら「CSS Modules使え💢」で片付けられる問題ですが、そういう設計ではないサービスでCSSの秩序にお困りの方の参考になればと思います🙏🙏🙏

余談ですが、記事中のHTMLはSlimとSassがパッと見区別つけづらいので書き分けています。プロダクションコードではガッツリSlim使っているので「erb… うっ頭が…」っと思った方はご安心下さい☺️🔪


ピクシブ株式会社では、フロントエンドの秩序絶対守るマンとして自信のあるエンジニアを募集しています。

明日は @ik-fib が何かお話するそうなのでお楽しみに☺️