CSSは、勉強してないのをごまかすためにイキって強がる人の風評被害を受けやすく、実際よりも低く評価されてしまうことがよくあります。
でも現代のCSSは意外に良いものですし、依然として残る課題に対しても「名前空間がないからCSSマジつかえねぇわ〜」と知ったかぶりして書かない理由を探すのではなく、やはり生産的に解決したいものです。
そこで、この記事ではCSS modulesを使って お手軽に CSSに名前空間を擬似的に導入します。
また、ParcelでCSS modulesを使う時に遭遇するバグの回避方法もあわせてお見せします。
CSSに名前空間がないことで起きる問題
CSSには名前空間がないため、たとえばこれからご紹介する例のように複数のCSSファイルを読み込んだ際に意図せぬ見た目になってしまうことがあります。
まず、以下の yagi.css
だけを読み込んでみましょう。
.yagi {
width: 10em;
}
.big {
width: 50em;
}
.yagi::after {
content: '';
display: block;
background-image: url('https://user-images.githubusercontent.com/1481749/56465716-251ebf00-643f-11e9-8c66-8d0de8953663.jpg');
background-size: contain;
background-repeat: no-repeat;
padding-top: calc(100% * 225 / 400);
width: 100%;
}
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="yagi.css">
</head>
<body>
<div class="yagi"></div>
<div class="yagi big"></div>
</body>
</html>
yagi.css
に設定した .big
クラスの設定が効いて $\huge{大きなヤギ}$ の画像になっています。
次に、以下の button.css
だけを読み込んでみましょう。
.button {
padding: 0.6em;
font-size: 1em;
}
.big {
font-size: 3em;
}
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="button.css">
</head>
<body>
<button type="button" class="button">Button</button>
<button type="button" class="button big">Big Button</button>
</body>
</html>
button.css
に設定した .big
クラスが効いてボタンが大きくなっています。
では、yagi.css
と button.css
を両方読み込むとどうなるでしょうか。
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="yagi.css">
<link rel="stylesheet" href="button.css">
</head>
<body>
<button type="button" class="button">Button</button>
<button type="button" class="button big">Big Button</button>
<div class="yagi"></div>
<div class="yagi big"></div>
</body>
</html>
ぶっこわれました。
ヤギ画像にもボタンにも以下の 両方の CSSが適用されてしまったためです。
.big {
width: 50em;
}
.big {
font-size: 3em;
}
このように、一般的過ぎるクラス名を使うと、意図しない要素にそのCSSが影響してしまいます。
BEM記法
技術ではなく運用でこの問題を解決しようとするのがBEM記法です。
先ほどの例では、以下のようにクラス名をつけなおします。
.yagi {
width: 10em;
}
.yagi--big {
width: 50em;
}
.yagi::after {
content: '';
display: block;
background-image: url('https://user-images.githubusercontent.com/1481749/56465716-251ebf00-643f-11e9-8c66-8d0de8953663.jpg');
background-size: contain;
background-repeat: no-repeat;
padding-top: calc(100% * 225 / 400);
width: 100%;
}
.button {
padding: 0.6em;
font-size: 1em;
}
.button--big {
font-size: 3em;
}
手作業でクラス名の頭に識別子をちまちまくっつける感じですね!
悪くはないんですが、識別子を付けないやつがチームにいると困ったことになります。
また、いくらベラのキャラデザインを現代的な若い女の子にしても、同時期に放映されているゲゲゲの鬼太郎もネコ娘のキャラデザインが一新されており、脚本も攻めまくったおもしろい内容になっているので「まぁ鬼太郎があるし、妖怪人間の方は見なくていいか」となってしまいます。
CSS modules で解決する
そこで、人間の手にたよらずに、ヤギの手も妖怪の手も借りずに自動で名前の衝突を避けてくれる技術の1つが CSS modules です。
では、お手軽にためしてみましょう。
nodeをインストールしていない方は先にインストールしておいてください。
以下はLinux OS上での操作を想定していますが、Macでもだいたい一緒だと思います。
うぃんどうずを使っている人はまずデュアルブートでUbuntuとかを入れましょう。
css-modules-sample
みたいなディレクトリを作り、そのディレクトリの中で
$ npm i postcss-cli postcss-modules
を実行して必要なライブラリをローカルインストールしましょう。
あとは、CSSファイルと以下の設定ファイルをこのディレクトリに置いておきます。
(設定ファイルの詳しい仕様を知りたくなったら公式ドキュメントをご参照ください)
module.exports =
{
"modules": true,
"plugins": {
"postcss-modules": {},
}
}
これで準備は完了です。
このディレクトリ内に yagi.css
を用意して、以下のコマンドを実行してみましょう。
$ npx postcss yagi.css -o dist/yagi.css
あらたに yagi.css.json
と dist/yagi.css
が生成されているはずです。
dist/yagi.css
をひらいてみると、以下のような内容になっています。
._yagi_1d9sa_1 {
width: 10em;
}
._big_1d9sa_5 {
width: 50em;
}
._yagi_1d9sa_1::after {
content: '';
display: block;
background-image: url('https://user-images.githubusercontent.com/1481749/56465716-251ebf00-643f-11e9-8c66-8d0de8953663.jpg');
background-size: contain;
background-repeat: no-repeat;
padding-top: calc(100% * 225 / 400);
width: 100%;
}
なんかランダムっぽい識別子がくっついて名前の衝突を防げるようになっていますね?
yagi.css.json
は、オリジナルのクラス名がCSS modulesによってどのように変換されたかをJSON形式で保持しています。
{"yagi":"_yagi_1d9sa_1","big":"_big_1d9sa_5"}
なんらかの工夫をして、HTML側のタグにつけるクラス名もこのJSONファイルを見て変換しないといけません。
Reactとかだと何かそういうライブラリとかがあるみたいです。
でも、ちょっとそれってめんどくさいですよね?
そこでそんなめんどくさがりの人のために、ずるいやり方 があります。
設定ファイル postcssrc.js
を以下のように書き換えてしまってください。
module.exports =
{
"modules": true,
"plugins": {
"postcss-modules": {
"generateScopedName": "[name]__[local]",
"getJSON": () => null
}
}
}
postcss-modulesの説明にあるとおり、
-
generateScopedName
は「どのようにクラス名を変換するか」のルールを指定し -
getJSON
は変換の対応表の書き出し方を指定します
ここでは generateScopedName
に ファイル名__元のクラス名
という形式を指定し、getJSON
には対応表は特に生成しないように指定しています。
実行して結果を見てみましょう。
$ npx postcss yagi.css -o dist/yagi.css
.yagi__yagi {
width: 10em;
}
.yagi__big {
width: 50em;
}
.yagi__yagi::after {
content: '';
display: block;
background-image: url('https://user-images.githubusercontent.com/1481749/56465716-251ebf00-643f-11e9-8c66-8d0de8953663.jpg');
background-size: contain;
background-repeat: no-repeat;
padding-top: calc(100% * 225 / 400);
width: 100%;
}
これなら事前にどのようなクラス名に変換されるか予測できるため、HTML側を書く時に
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="yagi.css">
<link rel="stylesheet" href="button.css">
</head>
<body>
<button type="button" class="button__button">Button</button>
<button type="button" class="button__button button__big">Big Button</button>
<div class="yagi__yagi"></div>
<div class="yagi__yagi yagi__big"></div>
</body>
</html>
としておけばいいだけです。
Elmを使っている方は、「Elmでもっと気軽にCSS modules」で紹介した方法を使うと簡単です。
Parcelで使う
ファイルを結合したり、Babelを使ったり、Minifyしてくれたり、そういう面倒なタスクをお手軽にいろいろ勝手にやってくれる技術の1つにParcelがあります。
Parcelを使う場合は先ほどの .postcssrc.js
をディレクトリ内に置いた状態で
$ npm i parcel https://github.com/arowM/parcel/archive/parcel-bundler@1.12.4.tar.gz
$ parcel build mix2.html --public-url ./
とすれば、dist
にコンパイル後のファイルが生成されます。
サンプルリポジトリ を作っておいたので、このリポジトリをcloneして
$ npm i && npm run build
としても同じことができます。
この通り、うまくいきました!
なお、Parcel には postcss-modules の generateScopedName
が効かないという問題があり、これを修正したものがまだリリースされていません。
そこで、上記のコマンドやサンプルリポジトリでは、parcelをフォークして独自に修正した parcel-bundler@1.12.4 をインストールしています。