このポストはCSS Modules — Solving the challenges of CSS at scaleをそれとなく意訳したものです。何か間違いなどありましたらご指摘いただけると幸いです。
(以下、意訳)
1996年にCSS1が公表された時、仕様の意図するところはドキュメントの内容から、ドキュメントの見た目を分離させることでした。仕様の中で最も重要となる単語は"ドキュメント"でした。当時のWebは、まだ主流ではなくコンテンツはドキュメントベースでした。スタイルシートの存在は今日に比べたらまだ小さく、いかなる保守的な負担もスタイルの分離による莫大なメリットによって、容易に相殺されました。
モダンなWebにおいては、私たちはドキュメントという文脈で内容を考えることはほとんどありません。モダンなWebは、高度に動的で派手なWebアプリケーションによって占拠されてしまいました。100個ものセレクタや1000行ものコードなんて、今のスタイルシートでは当たり前です。
大規模サイトのCSSを構築、保守することは、開発者チームにとって、ユニークな課題を投げかけます。これらの課題を克服するには、規約、ツールそしてフレームワークのバランスが要求されます。
大規模サイトにおけるCSSの課題
UniversalMindでは、何年にも渡って、大規模Webアプリケーションを設計・構築してきました。なので、開発・本番における大規模サイトでのCSSのユニークな課題について、とてもよく知っています。
グローバルスコープ
CSSは1つの暗黙なスコープを持っており、そのスコープはグローバルです。
開発者を教育する際、私たちはJavaScriptでのグローバルスコープの危険性を説きます。それなのに、CSSでのグローバルスコープの危険性については見逃してしまいます。
どんなフレームワークやプリプロセッサ、ビルドシステムを用いるにせよ、アプリケーションのCSSはランタイムに1つのグローバルスコープにて実行されます。その結果、セレクタの衝突や意図しない上書きが発生します。それらは開発の失速や、クロスプラットフォームでのUIレンダリングの問題の原因となります。さらには、開発者たちは既存のスタイルシートを変更することに不安を覚えることでしょう。
深くネストされた過剰なセレクタ
グローバルスコープの問題を緩和しようとして、開発者たちは過剰なセレクタで偽りのスコープを作ろうとするでしょう。これはうまくいきません。
.widget table row cell .content .header .title {
padding: 10px 20px;
font-weight: bold;
font-size: 2rem;
}
過剰なセレクタは、いくつかの問題を含んでいます。
- パフォーマンスにひどい悪影響を与えます。上記の小さな例では、レンダリングの前に、DOMに対して7回ものフェッチを行うようブラウザに要求します。上記のように1つのセレクタだけなら問題にはなりませんが、コードベースのいたるところに複数の過剰セレクタがあるならばページのレンダリングが遅くなってしまうでしょう。
- サイトを不要に重くします。バイト単位の重さでさえ、データスピードの制限されたモバイルには影響が出ます。
- 再利用性を落とします。意図しないセレクタの上書きがなくなる代わりに、再利用性が落ち、コピペコードが増えます。
リファクタリング
アプリケーションのデプロイ後、あなたはどうやってスタイルシートにインタラクティブな変更を加えますか?どのセレクタがどのコンポーネントに影響するのか、どうやって判断すれば良いのでしょうか?デッドコードはどうやって判定すれば良いのでしょうか?CSS設計をする際には、カプセル化を推し進め、保守性の高いコードを高めるべきなのです。
正しい進路に踏み込む
今やこういった課題に立ち向かうためのフレームワークや手法が存在します。しかしそれらは皆、課題を解決することはなく、それぞれがそれぞれの課題を抱えています。
プリプロセッサ(Sass, Less..等)
CSSプリプロセッサは、ここ10年の半分以上を占めてきた。それらはCSS設計においてとても価値があり、全てではないにしろ、大規模サイトでのCSSの課題に対処しました。
- import, 変数, mixinによって、コードの保守性が劇的に向上した。
- リファクタリングが怖くなくなり、その影響結果が予想しやすくなった。というのも、モジュール単位のスタイルは、関連ファイルを対象としてカプセル化されているからだ。
プリプロセッサは多くの利便性を持ち、開発環境の向上に大きく貢献したものの、実行時の最大の問題を和らげはしない。そう、 __グローバルスコープ__である。
通常のSassのルートファイルは以下のようになる。
// root.scss
@import 'reset';
@import 'global-values';
@import 'header';
@import 'item-list';
@import 'footer';
プリプロセッサは、圧縮前に、importによってモジュールを再構成する。だから、最終的にはすべてのセレクタをグローバルスペースに押し込めることになるのだ。これは、グローバルスコープの課題を解決していない。
BEM(Block Element Modifier)
BEMという手法はセレクタの命名規約によって、CSSにおけるモジュール性を提唱した。
[block]__[element]--[modifier] {
padding: 10px 20px;
font-weight: bold;
font-size: 2rem;
}
- Block―それ自体で意味をもつ、単独の要素。(header, container, menu, input等)
- Element―Blockのパーツであり、それ自体では意味を持たない。これらは意味的にブロックに結びつく。(menu item, list item, checkbox caption, header title)
- Modifier―BlockやElementに付くフラッグ。見た目や振舞を変更するために使う。(disabled, active, checked, big, red, error等)
モジュール性と再利用性とあり、構造化されたCSSを作成するためには、BEMは健全な手法となりえます。BEMはグローバルスコープの課題を解決できるものであり、Sassのようなプリプロセッサと組み合わせることで、見事に大規模サイトでのCSSの課題を対処することができます。しかしながら、BEM自体に問題がないわけではありません。
- 深すぎるネストは、手に負えないセレクタ名へと直結するし、そのセレクタ名が何を表しているのかを頑張って把握しなければいけない。
- BEMをうまく機能させるには、命名規則に際して首尾一貫してなければならない。それに、大規模チームにとっては、BEMのルールを守らせることは困難かもしれない。
実はもっと良い方法があります…
CSSモジュール
CSSモジュールは両者の世界にとって、ベストな解決策となります。CSSモジュールはCSSについて私たちが知り、愛している全てを順守しつつ、コンポーネント設計のパラダイムにおけるレイヤーも順守します。
CSSモジュールは複雑な規約なしに、ローカルスコープ化されたクラス名を生成します。
CSSモジュールを生成することは、他のCSSファイルを生成することと変わりません。CSSモジュールを使うことで、グローバルスコープの問題を恐れること無く、好きなクラス名を命名できます。もうBEMの複雑な名前は不要です。CSSの記法は不変なので、既にお使いの全てのツールは今までどおり動作するはずです。
CSSモジュールの例を示します。
/* components/demo/ScopedSelectors.css */
.root {
border-width: 2px;
border-style: solid;
border-color: #777;
padding: 0 20px;
margin: 0 6px;
max-width: 400px;
}
.text {
color: #777;
font-size: 24px;
font-family: helvetica, arial, sans-serif;
font-weight: 600;
}
CSSモジュールをローカルスコープにロードすることは、JavaScriptにおける__require__や__import__と同じくらいシンプルです。
/* components/demo/ScopedSelectors.js */
import styles from './ScopedSelectors.css';
いえ、待ってください!CSSを__require__できるんですか?
CSSモジュールをロードすれば、CSSのクラス名を他のプロパティ同様に参照することができます。
Reactでのシンプルな例を示します。
import React, { Component } from 'react';
import styles from './ScopedSelectors.css';
export default class ScopedSelectors extends Component {
render() {
return (
<div className={styles.root}>
<p className={styles.text}>Scoped Selectors</p>
</div>
);
}
};
コードの中で、ドットを用いてモジュールから"active"なクラスを参照していることに注目してください。ロードされたCSSモジュールを参照することは、あらゆるJavaScriptオブジェクトを参照することと同様です。
これらの全ては、CSSモジュールローダーによって可能となります。CSSモジュールローダーは、CSSをコンパイルしてローカルスコープに落としこむためにrequireやimportを用いています。そして、最終的にCSSのクラス名はグローバルにおいてユニークなものとなります。
上記のReactの例では、レンダーされた出力結果は以下のような見た目になるでしょう。
<div class=”ScopedSelectors__root___16yOh”>
<p class=”ScopedSelectors__text___1hOhe”>Scoped Selectors</p></div>
モジュールローダーが、{styles.root}と{styles.text}をグローバルにおいてユニークなクラス名へと変化させたのです。スタイルの結果は、ユニークでありコンポーネントに限定されたローカルなものとなります。生成されたクラス名は、コンポーネント名とクラス名と一意なハッシュから為るBEMの記法を使っており、たとえ同名のクラス名が存在していたとしても、それらが別のモジュールに存在していれば、グローバルではユニークとなります。
コンポジションを通じて、再利用を推進する
クラスを効率的に再利用することで、重複を最小化しつつ、アプリケーションにおける一貫したUIを維持します。CSSモジュールはコンポジションを通して、クラスの再利用を促進します。1つのクラスは複数のクラスを"構成"できるのです。
先述の例を思い出してください。共通のレイアウトやタイポグラフィはカンタンに抽象化することができますし、それによってアプリケーション内部の他のコンポーネントとの再利用を促進します。
/* components/demo/ScopedSelectors.css */
.root {
composes: box from "shared/styles/layout.css";
border-color: red;
}
.text {
composes: heading from "shared/styles/typography.css";
color: red;
}
レイアウトファイルは、また別のCSSモジュールになります。
/* shared/styles/layout.css */
.box {
border-width: 2px;
border-style: solid;
padding: 0 20px;
margin: 0 6px;
max-width: 400px;
}
タイポグラフィも同様です。
/* shared/styles/typography.css */
.heading {
font-size: 24px;
font-family: helvetica, arial, sans-serif;
font-weight: 600;
}
プリプロセッサとの共存
CSSモジュールはプリプロセッサを排除するわけではありません。Webpackのようなツールを使っているならば、Sassを使ってCSSモジュールを実現することはとてもシンプルです。
CSSモジュールを導入するべきか?
CSSモジュールは全てのプロジェクトに適合するわけではないです。Railsではなく、ReactやAngular2のようなコンポーネントベースでのプロジェクトではないとダメです。JavaScriptのアプリケーションでのみ導入できると言えるでしょう。さらに言えば、BEMを既にお使いならば、CSSモジュールに切り替える意味はないかもしれません。
次のプロジェクトでは、CSSモジュールをぜひお使いください。現時点では、Webpack, Browserify, JSPMにてローダーを使うことができます。また、RailsではWIPというプラグインが存在します。
まとめ
大規模サイトにおけるCSSの3つの課題について振り返ってみましょう。また、CSSモジュールがそれら全ての課題を解決し、プロジェクトに必要とされる構造化をもたらすことも、忘れないで下さい。
- グローバルスコープ CSSモジュールはグローバルスコープにおける名前の衝突を排除します。これはBEMの記法を応用することで、ユニークなクラス名を生成しているためです。
- 過剰なセレクタ CSSモジュールはコンポーネントレベルに存在するので、深くネストされたセレクタを記述する必要はありません。クラス名はシンプルでコンポーネントに関係する名前であることが約束されます。
- リファクタリング コンポーネントレベルで作業を行うため、どのスタイルがどのコンポーネントに影響するかをカンタンに知ることができます。その結果リファクタリングがシンプルになります。
参考レポジトリと参考記事
- CSS Modules-Welcome to the future CSSモジュールチームメンバーによるブログ記事
- CSS Modules GitHub repo
- CSS Modules — Webpack demo
- CSS Modules — Browserify demo
- CSS Modules — JSPM demo
- CSS Modules specification(IGSS)
(追記)
scivolaさん、typo修正ありがとうございました!