リファクタリングをしていたらCSS Modulesを使っていたにも関わらずCSSが壊れてしまった私達は、CSS ModulesがCSSの特性・我々のプロダクトの構成や開発方針に合わないと判断し「BEMベースのクラス名設計 + PostCSS」へ移行を進める方針を決めました。 これらの背景や、どうやって移行を進めたのか、また移行したあとの評価について話しました。
AbemaTVにおけるCSS is too fragile問題に対する解@kubotashota
View Slide
この内容は当サービスでの解であり他サービスには当てはまらない場合があります
1. 前提2. どうしたか3. 対応後の評価4. 教訓話すこと
前提1
AbemaTVについて”無料で楽しめるインターネットテレビ局 ”として次の特徴を持っている● TVのようにチャンネルを合わせると配信中の番組が流れる● 見逃した番組や様々な作品を見られるビデオオンデマンドサービスの提供
もともとのCSSの構成● css-loaderを使ってクラス名のユニーク化やファイルの結合をしていた● ReactのコンポーネントのインターフェースとしてclassName propsを定義していた○ 上位層のコンポーネントで下位層のコンポーネントに対してclassNameを渡すことでスタイルの上書きを可能にしていた● UIコンポーネントはAtomic Designの語彙を元に分割している
元のCSSの構成: ディレクトリ構造とコード例import React from 'react';import classNames from 'classnames';import styles from './Button.css';export const Button = ({className, disabled, onClick}) => {className = classNames(className, styles.btn);return (className={className}disabled={disabled}onClick={onClick}>{children});};@import "vars";.btn {border-radius: var(--radius);font-size: 100%;padding: .75em 3em;text-decoration: none;}.btn:hover {opacity: var(--opacity);}.btn:disabled {opacity: .2;}src/components├── atoms│ └── Button│ ├── index.js│ ├── Button.jsx│ └── Button.css├── molecules└── organisms
変更前後のディレクトリ構造Before/src├── /container/foo/A├── /actions/foo/A├── /dispatchers/foo/A├── /stores/foo/A│├── /container/bar/B├── /actions/bar/B├── /dispatchers/bar/B└── /stores/bar/BAfter/src├── /foo/container/A├── /foo/actions/A├── /foo/dispatchers/A├── /foo/stores/A│├── /bar/container/B├── /bar/actions/B├── /bar/dispatchers/B└── /bar/stores/B● テレビ機能しかなかった初期構成の負債の返却● 特定の機能に閉じた変更をしやすくする
デグレ: ディレクトリ変更でスタイルが壊れる
デグレ: ディレクトリ変更でスタイルが壊れる● importの記述順序を変えるだけでルールセットの順序が変わりうる○ CSS Modulesのモジュール探索の順番はJSと同様に深さ優先探索● classNameの上書きがこの順序に影響しうる○ モジュールの依存グラフの到達順序によっては最終的なCSSのルールセットの順序は変わる
スタイルが壊れる例AB CD EAB CD EBCD間に記述順序に依存するスタイルがあると崩れる1234514235変更前 変更後
スタイル崩れ その1
スタイル崩れ その2崩れている例正常な例
スタイル崩れ その2崩れている例正常な例スタイルの定義順が入れ替わっていた/* 入力フォーム側のボタンのスタイル定義 */.btn {padding: 12px 24px;}/* ボタン側のスタイル定義 */.btn {padding: .75em 3em;}段差がある段差がない
なぜディレクトリ構造の変更でスタイルが崩れたか● ディレクトリ構造の変更に応じてimportの順序を変えた結果モジュールの探索の順番が変わり、生成されるCSSのルールセットの定義順が変わった● 上位のコンポーネント(Containerなど)で下位コンポーネントのスタイル定義を上書きしているところが多くあった○ 汎用的なコンポーネントを作ろうとした結果、変更に対して開かれすぎたコンポーネントができていた
私たちが欲しかったもの● CSS Modulesベースの設計と似たもの● リファクタリングを阻害しないこと● ライブラリ独自の機能に対する依存が少ない
どうしたか2
BEM + PostCSSへ移行● BEMベースのクラス名の命名規則○ .アプリケーションドメイン -コンポーネント名__コンポーネント内の要素 --修飾子■ 例: .video-PlayerContainer__screen○ grepのためにクラス名を JavaScriptで動的に構築するのは避ける● postcss-importを使ってCSSが展開されるようにした○ コンポーネントのルートディレクトリ内に root.cssを用意してその中でCSSをimportする
BEM + PostCSSへ移行Before: CSS Modulesベース@import "vars";.btn {border-radius:var(--radius);font-size: 100%;padding: .75em 3em;text-decoration: none;}.btn:hover {opacity: var(--opacity);}.btn:disabled {opacity: .2;}After: BEM + PostCSSベース.com-a-Button {border-radius:var(--radius);font-size: 100%;padding: .75em 3em;text-decoration: none;}.com-a-Button:hover {opacity: var(--opacity);}.com-a-Button:disabled {opacity: .2;}root.css@import './vars.css';@import './component/atom.css';@import './component/molecules.css';@import './component/organisms.css';/* template */@import './featA/featA.css';@import './featB/featB.css';/* page */@import './page1/page1.css';@import './page2/page2.css';
対応後の評価3
スタイル適用をしやすくなった● CSS Modulesのようにルールセットの並び順がブラックボックスにならなくなり、CSSのimport順を変えれば意図通りのスタイルが適用されるようになった● クラス名のユニーク化をツールに任せるのではなく開発者側でやるようにしたことでブラウザ拡張などでCSSを上書きしやすくなった○ Stylus(ブラウザ拡張)を使ってデザイナーがプロトタイピングをすることも容易になった
CSS Modulesよりは考えることが増えた● BEM + PostCSSではクラス名の規約を作る必要が増えた● クラス名の規約に沿っているかレビューで見る必要が増えた● 規約をチームメンバーに浸透する必要が増えた● チームメンバーに規約が浸透すればレビューで指摘することはそんなにない● 生成後のCSSを想像しやすくなった利点のほうが大きい
パフォーマンスはどうなのか?● CSSのサイズはデグレ対応の前後で35KB→36KBと誤差の範囲で増加● CSS ModulesからBEM + PostCSSへ移行してページの表示速度が大きく遅くなってはいない● 移行を進めていく上で将来的にCSSのサイズが増えすぎるようだったらデバイスやページ単位でCSSを分けることもできる
他の選択肢と選ばなかった理由
他の選択肢と選ばなかった理由● どうにかしてCSS Modulesを使い続けたりCSS in JSを使ったりする○ 標準のCSSの書き方を保ちやすい、ビルドも複雑にならないという点でBEM + PostCSSのほうがのちのち捨てやすいと判断○ 標準仕様から外れるような PostCSSのプラグインを使わないようにしている● ディレクトリ構造の変更後にモジュールの探索順序を変えないようにするためimport宣言の順序を変えない○ import宣言の並び順に一貫性がなくなる○ それはリファクタリングしたと言えるのか?
教訓4
教訓● CSS ModulesやCSS in JSは使い方を気をつけないとふとしたときに壊れる可能性がある● サービスの段階によって取り入れるものを変えたほうが改善をしやすくなる場合がある
Thank you!Twitter: @kubotashota