このポストは以下の記事の意訳です。
The End of Global CSS
何か間違いなどありましたら、ご指摘いただけると幸いです。
(以下、訳)
グローバルCSSの終焉
CSSセレクタはすべて同じグローバル空間に存在しています。
CSSに触れた人は皆、以下のような見解に至っていることでしょう。
CSSの考えはドキュメントの時代に設計されたもので、今ではモダンなウェブアプリケーションのためのマトモな開発環境を提供することが困難であると。
全てのセレクタには意図しない副作用を起こす可能性があり、期待と違う要素にスタイルが適用されてしまったり、他のセレクタと競合してしまったりします。さらに驚くべきことに、セレクタはグローバル空間で詳細度の競争に負けることもあり、ページのデザインに全く影響を持たなくなることだってあり得るのです。
CSSファイルを変更する時はいつでも、スタイルが適用されるグローバル空間について慎重に考慮する必要があります。CSSの保守性を担保するためには多くの規約が必要なのです。
しかし、もはや状況は変わりました。
グローバルなスタイルシートは過去の時代となりました。
ローカルCSSを導入する時が来たのです。
グローバル環境を変更することには非常に慎重になるべきである、これは他の言語では当たり前のことです。
JavaScriptのコミュニティにおいては、BrowserifyやWebpack、JSPMのおかげで、我々のコードが小さく分割されることが求められるようになりました。それぞれのモジュールは依存性を明示しつつ、小さなAPIをexportします。
しかし、CSSはいまだに野放しじゃないでしょうか。
私達の多くは、私を含め、最近まで次のような状態にありました。長いことCSSに触れてしまったため、ブラウザベンダの助けを借りることなく解決できる問題として、ローカルスコープの欠如に気づきませんでした。そうでなくとも、ShadowDOMをきちんとサポートしているブラウザをユーザが使い始めるまで待たなければいけません。
こういったグローバルスコープの回避策として、OOCSSやSMACSS、BEMやSUITのような名前付けの規約がありました。それらは名前空間の衝突を防いだり、ちゃんとしたスコープのルールをエミュレートするための方法です。
確かにCSSを手懐けるまで大きな一歩であったとはいえ、これらの手法はスタイルシートの本当の問題に対処するものではありませんでした。どんな手法をとったとしても、グローバルセレクタからは逃れられないのです。
しかし2015年4月22日、全てが変わりました。
以前の記事で説明したように、Webpackを使うことでJavaScriptモジュール内でCSSをimportすることができます。もしこの手法に馴染みがないとしたら、この記事を読むことをオススメします。今回の論旨を逃すといけないので。
Webpackのcss-loaderを使えば、CSSのimportは以下のようになります。
require('./MyComponent.css');
これは一見、奇妙です。JavaScriptではなくCSSをimportしている!ということを抜きにしても。
概してrequire
での読み込みは、ローカルスコープに何ものかを持ち込んでしまうものなはずです。そうでなくとも、グローバルの副作用がありうるということの表れで、貧弱な設計の徴候です。
しかし、これがCSSなんだ・・グローバルでの副作用は避けようのない弊害なんだ。
そう思っていたのです。
2015年4月22日、Tobias Koppersはcss-loaderの新機能として最初のイテレーションを付与しました。これは当時 _placeholders_と呼ばれていましたが、今ではローカルスコープと呼ばれています。
この機能を使うことで、CSSのクラス名をJavaScriptのコードへとexportすることができます。
つまり、以下のように書く代わりに、
require('./MyComponent.css');
このように書くことができます。
import styles from './MyComponent.css';
この例では、 _styles_は何を評価しているのでしょうか?
CSSから何がexportされたかを確認するために、そもそもどのようなスタイルだったのかを見てみましょう。
:local(.foo) {
color: red;
}
:local(.bar) {
color: blue;
}
この場合、css-loaderのカスタムである _:local(:identifier)_シンタックスを使いました。これでfoo
とbar
の2つの識別子をexportすることができます。
これらの識別子は、JavaScriptファイルで利用できるクラス名へとマップされます。以下は、Reactの例です。
import styles from './MyComponent.css';
import React, { Component } from 'react';
export default class MyComponent extends Component {
render() {
return (
<div>
<div className={styles.foo}>Foo</div>
<div className={styles.bar}>Bar</div>
</div>
);
}
}
クラス名にマップされたこれらの識別子はグローバルなコンテキストにおいてユニークである、ということが重要です。
もはや、スコープを実現するために長ったらしい接頭辞をセレクタにつける必要はないのです。より多くのコンポーネントが、名前衝突のない識別子を定義できます。かつてのグローバルセレクタとは大違いですね。
CSSの世界に大変動が起こっているのだとお気づきになりましたか?
いまや副作なしにCSSに変更を加える事ができます。CSSに正常なスコープを導入することができたのです。
グローバルCSSの利点は、新しいCSSモデルでも実現できます。例えば、ユーティリティクラスを使って、コンポーネント間でスタイルを再利用できます。重要なポイントは、他の技術と同様に、必要なクラスを明示的にimportする必要があるということです。コードそのものは、グローバル環境を推し知ることができないからです。
保守性の高いCSSを書くことは、いまや推奨要件です。そのためには、命名規則にこだわるのではなく、スタイルをカプセル化しながら開発するのです。
上記のようなスコープモデルの結果、Webpackのおかげで _本当の_クラス名をコントロールできました。幸運なことに、これはconfigで設定できることです。
しかし、デフォルトでは、css-loaderは識別子をハッシュに変換します。
例えば以下のとおり。
:local(.foo) { … }
これは以下のようにコンパイルされます。
._1rJwx92-gmbvaLiDdzgXiJ { … }
デバッグという観点から見れば、これは何の助けにもならないでしょう。クラスをもっと有用にするためには、クラスのフォーマットをWebpackのconfigで指定しましょう。その際に、css-loaderのパラメータとしてクラスフォーマットを設定します。
loaders: [
...
{
test: /\.css$/,
loader: 'css?localIdentName=[name]__[local]___[hash:base64:5]'
}
]
上記のように設定することで、 _foo_というクラスの識別子は、以下のようにコンパイルされます。
.MyComponent__foo___1rJwx { … }
これで、識別子の名前がよくわかりますし、どのコンポーネントから来たのかも明確です。
_node_env_という環境変数を使うことで、開発と本番でクラスのパターンを分けて設定することができます。
loader: 'css?localIdentName=' + (
process.env.NODE_ENV === 'development' ?
'[name]__[local]___[hash:base64:5]' :
'[hash:base64:5]'
)
いまや、Webpackはクラス名をコントロールできます。なので、私たちはカンタンに、圧縮されたクラス名を本番環境で利用することができます。
この発見をするやいなや、私たちはすぐにプロジェクトのスタイルをローカル化しました。私たちは既に、BEMを利用してコンポーネントベースでCSSをスコープ化していましたので、Webpackでのスタイル管理が馴染みました。
すると、面白いことにとあるパターンが確認されました。私達のCSSをファイルのほとんどが、ローカル識別子のみを含むようになったのです。
:local(.backdrop) { … }
:local(.root_isCollapsed .backdrop) { … }
:local(.field) { … }
:local(.field):focus { … }
etc…
グローバルセレクタは、アプリケーションのわずかな部分でだけrequireされました。このことは、とても大切な問いかけへと繋がりました。
特別なシンタックスを使う必要なんて無い。セレクタはデフォルトでローカルであり、グローバルセレクタが例外的なものなのだ。そう考えられるのではないか、と。
こんな風に書けたとしたらどうでしょうか?
.backdrop { … }
.root_isCollapsed .backdrop { … }
.field { … }
.field:focus { … }
通常、これらのセレクタは曖昧すぎます。しかしcss-loaderを噛ませることで、この問題は解消され、セレクタはモジュールのスコープ内に収まります。
グローバルを避けられないような局面では、 _:global_という特殊なシンタックスを明示的に付与します。
例えば、ReactCSSTransitionGroupによって生成されるスコープ化されていないクラスをスタイルする時は、以下のようにします。
.panel :global .transition-active-enter { … }
ローカルの _panel_識別子をモジュール内にスコープ化しているだけでなく、手に負えないグローバルクラスのスタイリングもしています。
デフォルトがローカルスタイルであるようなこういったクラス構文の導入は、それほど難しくはないだろうと気づきました。
私たちはそのためにPostCSSを利用しました。PostCSSは、カスタムCSSの変換を可能とする素晴らしいツールです。有名なCSSビルドツールの1つであるAutoprefixerは、実際にはPostCSSのプラグインです。
ローカルCSSを広めていくために私は、postcss-local-scopeという名のPostCSSのプラグインをオープンソース化しました。postcss-local-scopeはまだまだ開発の真っ最中なので、自己責任で導入してください。
もしあなたがWebpackをお使いならば、理想的なCSSビルド環境まであと一歩です。postcss-loaderやpostcss-local-scopeを導入するのは、比較的カンタンであると言えるでしょう。ただし、ここで説明するよりも、以下のサンプルレポジトリを参照するのが良いでしょう。
postcss-local-scope-example
ローカルCSSの導入はほんの始まりに過ぎません。
ビルドツールのクラス名生成は、おおきな示唆を含んでいます。長いスパンで見れば、私たちは手動コンパイルからコンピュータによる最適化へと移行することができるでしょう。
将来的には、コンポーネント間の共有クラスを自動生成し、スタイルの再利用をコンパイル時の最適化として扱うことができるでしょう。
ひとたびローカルCSSに触れれば、後戻りはできません。クロスブラウザでの真のローカルスコープは、カンタンに無視できるものではありません。
ローカルスコープの導入は、私達のCSS体験にとって非常に大きな波及効果がありました。命名規約、再利用パターン、そしてパッケージごとに分離されたスタイル。これらは全てCSSのローカル化によって大きく影響を受けました。そして私たちはいま、ローカルCSSの新時代の入り口に立ったばかりです。
ローカルCSS化の流れがどのように派生していくかについては、私達もまだ調査しているところです。
この流れに乗るためには、ぜひともpostcss-local-scope-exampleをチェックしてみてください。
実際に上記のレポジトリを見てみてば、私が述べてきたことは誇張でもなんでもないのだと、きっと同意してくださると思います。グローバルCSSの時代は終わりを告げつつあります。CSSの未来はローカルCSSなのです。
注:コンポーネント間でのスタイル再利用の自動最適化は、とても素晴らしい1歩ではあります。しかし、私よりもはるかに賢い人の助けが必要でしょう。どうかそんな方が現れますように。
追記
2015年5月24日:postcss-local-scopeで提唱した思想は、Tobias KoppersによってWebpackへと取り込まれました。なので、元のプロジェクトは廃止状態となっています。CSSモジュールのサポートは、css-loaderでmoduleフラッグを設定することで利用可能です。試しにcss-loaderを使ってCSSモジュールの使用例を作りました。この例では、コンポーネント間でのスタイル共有を行うためのクラス継承も行っています。