まえがき
チーム制作でのCSSセレクタにおけるクラスの命名規則を統一することで他人の書いたコードの可読性を補助する目的に、しばしばBEMやFLOCSSなどの記法が用いられる。
めちゃくちゃ正直に言うと、これらが好きではなかったのだ。
制約がやたらと多いし(使用目的を考えれば当然なのだけれども)、覚えるのも億劫だし。
これらを使いなさいと強制される時もあったので渋々使いはしたけれども、やっぱり好きになれなかった。
んじゃあ「なんで私はこれらが好きじゃないのさ」と、遠巻きに威嚇する足取りで接触を試みるのが今回の記事の趣旨です。
いつものPhewを使いつつ、CSS命名規則の使い心地選手権大会はっじまっるよー。
メリットとデメリット
独自の命名規則を全く新しいところから捻出するのは、車輪の再発明の愚を犯すことなのでやりません。
BEMもFLOCSSも、両者にメリット/デメリットがあって、デメリットの部分にどうしても馴染めなくて好きになれないってだけの話。
つまり両者の「ここはメリットだ」と思うところを抽出できれば解決できるって算段でアプローチしてみようじゃまいか。
BEMのメリット
- 命名から最小限のDOM構造が推測できる。
HTML構成の一団を「Block」、Blockの中のHTML要素を「Element」と命名する。
BlockまたはElementの中での近縁種を「Modifier」として派生的な命名を行う。
親Blockに対して、子Elementがあって、兄弟姉妹叔父叔母甥姪などの特定をするためのModifierを付与するので考え方としてはシンプルで分かりやすい。
BEMのデメリット
- ひとつひとつのクラス名が長くなりがち。
これについては最終章で頑張って解決させました。
- 文節境界のセパレータ記号が制作者に優しくない。
Block・Element間の記号は「--」、Element・Modifier間の記号は「__」、各Block・Element・Modifierを2単語以上で定義する場合は「-」で連結するというのが公式ルール。
ところがこの文字選定が非常に制作者に優しくなくて、一番嫌だったところ。
例えば「.header--logo-image__dark-2x」と命名した場合。
- Block: header
- Element: logo-image
- Modifier: dark-2x
これをエディタ上でダブルクリック文字選択してみると、「header」「logo」「image__dark」「2x」で分割されてしまうのが最悪。
Modifierはともかく、BlockやElementの単位で文字選択できないのはシンプルきつい。
文節境界として用いられるハイフンと、文節境界にならないアンダースコアを選定してしまったという仕様の欠陥だと思う。
- Element同士の親子関係を定義する方法がない。
ElementはBlockにしか所属できないので、Element同士の親子関係が発生した場合の手段がないのも好きじゃないポイント。
<form>
<fieldset>
<legend>名前</legend>
<input type="text" name="nickname" value="">
</fieldset>
<fieldset>
<legend>住所</legend>
<input type="text" name="address" value="">
</fieldset>
<fieldset>
<legend>性別</legend>
<label>
<input type="radio" name="gender" value="male" checked>
<span>男性</span>
</label>
<label>
<input type="radio" name="gender" value="female">
<span>女性</span>
</label>
</fieldset>
<fieldset>
<button type="submit">送信</button>
</fieldset>
</form>
例。こういうHTMLがあるとして、これを1つのBlockにしたい場合。
fieldset要素やlegend要素同士は全て並列の階層にあるのでElement+Modifierで問題なし。
問題になるのはinput要素とlabel要素で、fieldsetの子供だったり(名前・住所)孫だったり(男性・女性)。
機能は似ているけれども階層構造が違うパターン。
input[value="male"]・input[value="female"]の親となるlabel要素にもCSS装飾を施す場合。
構造的に見るのなら「form (Block) - fieldset - label - input」となってますね。
BEM記法に従うのであれば、input[type="radio"]要素近辺は次のようになるはず。
- fieldset: .form--fieldset__gender
- label: .form--fieldset-gender
- input (male): .form--fieldset-gender__male
- input (female): .form--fieldset-gender__female
fieldsetとlabelの両方をElementにする必要があるために発生する名前の紛らわしさ。
labelの階層を上げて「fieldset.form--fieldset__gender」・「label.form--gender」とするのが本来の意図なんだという理解なんですが、
- fieldset: .form--fieldset__gender
- label: .form--gender
- input (male): .form--gender__male
- input (female): .form--gender__female
fieldset属性とlabel属性間で何らかの関係性はありそうだけども名前からは類推できませんね。
キチンと構造を明記するのなら「.form--fieldset-gender--label-male__input」みたいになるんですが、BEM規則の「Block同士・Element同士のネストの禁止」に抵触するのでNG。
Block - Element - Element - Modifierの構造になってるので。
どこかしらで親子関係を崩壊させないとうまく表現できなかったんです。
恐らくBEMの設計思想としてはもう少しミニマルな、今回の例で言えばfieldset要素をBlockとするような規模を想定しているのだろうけれども現実的なコーディング作業を考えればセクション単位(今回の例で言うとform要素)で行った方が楽なのでちょっと感覚が合わないなって印象。
そもそも論ですが規模感の価値観の違いから、「ネストを禁止する」ってルールが気になるのでBsEsM (Blocks - Elements - Modifier)を許容してほしい。
もう一声踏み込んでみるならBsEs (Blocks - Elements)でも良くて、ModifierはElementsの必要と思う場所に適宜差し込めるならモアベター。
ああ、それと文字選択できないのでセパレータ記号を変更したい。
この2点に引っかかって好きになれないと分かりました。
それさえ解消できれば、クラス名から容易にDOM構造が類推できるので多分好き。
FLOCSSのメリット
- 役割ごとに接頭辞を用意しているので機能が明確。
「.c-button」や「.u-small」など、「そのクラス名が持っている機能」を把握するのが容易。
しかもF(oundation), L(ayout), C(omponent), P(roject), U(tility)の接頭辞を付けるので、ページ全体に影響するもの・サイト全体で使い回すもの・その要素で固有のものなどの規模感も掴みやすい。
- 親子関係を意識しなくていい。
根本的な視点がBEMとは違い、「1つのクラス名に対して1つの機能」を与えて使います。
TailwindCSSやBootstrapなどCSSフレームワークのクラス名の考え方なので、そもそも親子関係がどうこうというものではなく、それらに使い慣れているのならすごく馴染みやすいというか「CSSフレームワークに足りないコンポーネントを補う方法」と思えば良さそうな。
例えばBootstrapにはtext-indentに対するコンポーネントが存在しないので「.u-leading__1em」を作るとか、いっそ「.pb-0_33rem」(=padding-bottom: 0.33rem)みたいなものでもアリなのではと思わなくもない。
ちなみにTailwindCSSには任意値という機能(例えば「.w-[calc(4rem+5px)]」のような)があるので、コンポーネントとして存在しない場合を除いて不要ですし、何ならtailwind.config.jsでコンポーネントそのものも拡張できるので使う場面がそもそもないです。
FLOCSSのデメリット
- フォルダとファイルが無限に増える。
役割ごとにフォルダ分けして、コンポーネントごとにファイル分けして、それでやっとSCSS全体の可読性が上がるかなってくらいには増えます。
そしてファイルを探すのが大変。
- 役割の接頭辞を覚えるのが大変。
「.u-*」だ、「.f-*」だ、「.o-*」だ、色々あって大混乱。
CSSフレームワークの特にBootstrapを補強するものとして考えるなら「.u-*」(ユーティリティ)と「.c-*」(コンポーネント)だけで十分な気もしますがねい。
CSSフレームワークとの共存を図る。
その上でどういうクラス名を付けるか、という考えに至れたのは大きな収穫でした。
ファイル・フォルダの分類においては参考になる一方で、接頭辞はそんなにも要らないなという感想。
使用感は個人の感想です。
と書くだけというのも何なので、公正を期すために実際に使ってみた。
FLOCSS
mkdir dist
mkdir src
mkdir src/css
mkdir src/img
mkdir src/js
mkdir src/public
npm init -y
npm i -D cross-env dotenv gulp gulp-plumber gulp-filter gulp-if gulp-rename gulp-pug glob sass gulp-sass gulp-postcss autoprefixer postcss-csso gulp-sharp-optimize-images esbuild browser-sync gulp-connect-php
npm i -D --include-optional sharp
npm i smooth-scrollbar
- package.jsonのscriptsを書き換え
- .gitignoreを作成
- .envを作成
- gulpfile.mjsをコピペで持ってくる
- 編集はsrcフォルダ
- 「npm run dev」でhttp://localhost:3000の開発モード用サーバが起動
- 「npm run build:local」でローカルサーバ向けのファイル書き出し
- 「npm run build:server」で外部サーバに設置する設定でのファイル書き出し
実はこれ、作り直しです。
最初はCSSフレームワークに頼らず全部書いてました。
本当にファイルとフォルダでゴチャゴチャになって管理不全に陥るので断念したんです。
これでも十分SCSSファイルがゴチャってますけどねえ。
ということでBootstrap+FLOCSSでリブート。
足りないものを作るだけになりましたが、これでも結構マシになったんですが、それでもファイルで溢れ返りますわ。
<a href="#" class="c-anchor">
<div class="c-anchor__background"></div>
<hr class="c-anchor__line">
<span class="c-anchor__text">リンク</span>
</a>
それと上例のようなグループ単位のhoverが発生する場合は「クラス名に紐付けられた機能ごと」という思想では追いつかなかったです。
どうしてもコンポーネント単位でのCSSスタイルが必要になるのでちょっと困った。
なおTailwindCSSにはgroup-hoverというこれを補う機能があるはずなんですが、どうにもコンパイラが認識してくれなくて、こちらでも困っている。
BEM
mkdir dist
mkdir src
mkdir src/css
mkdir src/img
mkdir src/js
mkdir src/public
npm init -y
npm i -D cross-env dotenv gulp gulp-plumber gulp-filter gulp-if gulp-rename gulp-pug glob sass gulp-sass gulp-postcss autoprefixer tailwindcss postcss-csso gulp-sharp-optimize-images esbuild browser-sync gulp-connect-php
npm i -D --include-optional sharp
npm i smooth-scrollbar
npx tailwindcss init
- package.jsonのscriptsを書き換え
- tailwind.config.jsのcontentを書き換え
- .gitignoreを作成
- .envを作成
- gulpfile.mjsをコピペで持ってくる
- 編集はsrcフォルダ
- 「npm run dev」でhttp://localhost:3000の開発モード用サーバが起動
- 「npm run build:local」でローカルサーバ向けのファイル書き出し
- 「npm run build:server」で外部サーバに設置する設定でのファイル書き出し
SCSSと、TailwindCSSの@apply構文と、BEM。
HTMLのクラス名でなくSCSSで全て完結させる三者の噛み合わせがすごくうまくいった。
便利だしこれからBEM使う!
と言いたいところですが、前述のデメリットがあったので次のように不便を解消させました。
クラス名は最後に記した例外を除き、必ず「.c-」で始めること。
基本的には全部コンポーネント(Component)扱いってことです。
フォルダ分類はFLCとすること。
FLOCSSでの分類方法が分かりやすかったので倣って少し流用しました。
- foundation: 初期設定、CSSフレームワークやプラグイン
- layout: ページ全体の構造
- component: HTML要素全般
Blocks - Elements - Modifier間のセパレータは「--」
アンダースコア(「_」)記号は文節境界にならないので断固拒否。
2単語以上になる場合は、ハイフンでなくアンダースコアを使い、逆にこちらを1語にまとめます。
Modifierも「--」で接続しますし、BlockやElementのネスト制限も撤廃します。
【例外規定】クラス名が「.c-」で始まらないもの
- CSSフレームワークなどに由来する独自のクラス名はそのまま継承する。
- クラス名を付けられないものはそのまま書く。
- :root
- ::selection
- htmlなどのHTML要素名
- 状態識別を行うためのコンポーネントとして「.is」を用いる。
- .is--dark
かなり命名規則としては甘々になりました。
その代償としてクラス名は「.c-header--search--input--text」などのように、すごく長くなりました。
こうすることで少し問題となってくるのがSEO対策。
header・search・input・textのそれぞれが英単語として解釈可能なために、「これら単語の内容を扱ったページだ」と検索エンジンに誤った解釈をさせてしまう恐れが(少しだけ)あります。
それに加えて、文字数が嵩むためにHTML・CSSファイルのサイズも増えるのでページ表示速度への影響も(わずかながら)あります。
これら諸問題を解消するために、クラス名をハッシュ化してやりましたというのが今回のハイライト。
(「ハッシュ化」についての解説記事、「難読化」とも言う)
BEM+クラス名のハッシュ化
クラス名を短く、かつ意味のない文字列へと変換します。
クラス名の有用な情報量が減った反動で、HTML本文の情報により焦点が当たるようになることを期待します。
mkdir dist
mkdir src
mkdir src/css
mkdir src/img
mkdir src/js
mkdir src/public
npm init -y
npm i -D cross-env dotenv gulp gulp-plumber gulp-filter gulp-if gulp-rename gulp-pug glob fs-extra sass gulp-sass gulp-postcss autoprefixer tailwindcss postcss-csso postcss-uuid-obfuscator gulp-sharp-optimize-images esbuild browser-sync gulp-connect-php
npm i -D --include-optional sharp
npm i smooth-scrollbar
npx tailwindcss init
- package.jsonのscriptsを書き換え
- tailwind.config.jsのcontentを書き換え
- .gitignoreを作成
- .envを作成
- gulpfile.mjsをコピペで持ってくる
- 編集はsrcフォルダ
- 「npm run dev」でhttp://localhost:3000の開発モード用サーバが起動
- 「npm run build:local」でローカルサーバ向けのファイル書き出し
- 「npm run build:server」で外部サーバに設置する設定でのファイル書き出し
gulpfile.mjsとpackage.jsonだけの変更なので違いはないです。
この処理を行う場合、一番手っ取り早い方法はpostcss-obfuscatorプラグインを導入すること。
PostCSSの処理中にクラス名を難読化し、処理後にはHTML・Javascriptファイルを元のクラス名から難読化した後のクラス名へと文字置換してくれます。
この難読化処理には少し時間が掛かるので、開発モードでは実行されず、ビルドモードでのみ有効になります。
余談ではありますが類名のpostcss-classname-obfuscatorは似て非なる別物なので要注意。
などと説明しておいて何ですが、postcss-obfuscatorプラグインは使いません。
特に重大な問題が2点あるからです。
文字置換が一部で効かなかったり、逆に効き過ぎたりする。
postcss-obfuscatorの内部では次の条件で文字置換しています。
/([\s"'\`]|^)(${className.slice(1)})(?=$|[\s"'\`])/g
クラス名の先頭のピリオドを外して、前後に[文頭・文末・空白記号・引用符]のいずれかを伴った箇所を対象とします。
ということで、「<hr class="hoge fuga">」や「document.body.classList.add("piyo")」は置換してくれる一方で、「document.querySelector("foobar")」は置換してくれず、その代わりに「<div>Hello "baz".</div>」が置換されてしまいましてこの過剰置換についてはissueが上がっていたりします。
PostCSS UUID Obfuscator
じゃあどうするのって話ですが、自分でPostCSSプラグインを作っちゃった
設計思想自体はいいものだと思っているので、基本的にはコードを踏襲しつつ問題のある部分を書き換えました。
導入方法や使い方についてはREADME.mdに書いといたので気が向いたらどうぞ。
なおREADME.mdを書くにあたって、一番苦労したのは英語への翻訳。
結論
HTMLファイルの情報量についてはご覧の通り。
「伝えたいこと」だけが書いてあるという素敵ソース。
<!DOCTYPE html>
<html class="s4j" lang="ja">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<title>Phew</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
</head>
<body class="tk0">
<link rel="stylesheet" fetchpriority="low" href="https://fonts.googleapis.com/css2?family=Ubuntu:wght@500&family=IBM+Plex+Sans+JP:wght@400&display=swap" />
<link rel="stylesheet" fetchpriority="low" href="https://cdn.jsdelivr.net/npm/remixicon@4.3.0/fonts/remixicon.css" />
<link rel="stylesheet" href="css/index.css" />
<script async src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.min.js"></script>
<script src="js/index.js"></script>
<div class="ul4" role="banner">
<header class="t13">
<h1>
<a href="/">
<picture>
<source srcset="img/logo.webp" type="image/webp" />
<img class="v96" src="img/logo.png" alt="Phew Co., Ltd. ロゴ画像" />
</picture>
</a>
</h1>
<ul class="sd5">
<li><a class="v15" href="#page1">PAGE_1</a></li>
<li><a class="v15" href="#page2">PAGE_2</a></li>
</ul>
<ol class="sc8">
<li>
<button class="s3u vfb" aria-label="light">
<i class="ri-sun-line ri-fw"></i>
</button>
</li>
<li>
<button class="s3u vo6" aria-label="dark">
<i class="ri-moon-clear-fill ri-fw"></i>
</button>
</li>
</ol>
</header>
</div>
<main class="tt1" role="main" data-scrollbar>
<div role="region" aria-label="メインコンテンツ">
<section class="tts" role="article" aria-label="ファーストビュー">
<picture class="v4h">
<source srcset="img/header-index.webp" type="image/webp" />
<img class="tvn" src="img/header-index.jpg" alt />
<figcaption class="uuk">CREATE OUR FUTURE</figcaption>
</picture>
<span class="tvt">
<i class="ri-scroll-to-bottom-line ri-fw sfc"></i>
<small class="v4c">Scroll</small>
</span>
</section>
<section class="skm" role="article" aria-label="インフォ">
<div class="sct">
<p class="vat">
うるわしい空の色が違います。私はここへKを入れた氷嚢を頭の上まで跳かしていた、と叔父がいうのです。
</p>
<p class="vat">
私はちょっと首を傾けた。同時に私は正直な路を歩くつもりで、それを大事そうに父と母の前に坐っている女二人を認めました。それから若い男だろうか年輩の人だろうかと疑ってみました。それがまた滅多に起る現象でなかったけれども、そういう艶っぽい問題になると看病はむしろ楽であった。
</p>
<p class="vat">
私は今でも記憶して下さい。最後に私を変化させるのかも解りませんでしたが、私の心はとっくの昔からすでに恋で動いているようにしますから実際私には解らないのですから。
</p>
<p class="vat">
私に添われないから悲しいのではなかろうかという好奇心も動いた。私は夢中で医者の家へ始終遊びに行きました。
</p>
<p class="vat">
これが私のようなKに向って、それを取り出して、それで旗竿の先へ持って行って下さい。私はいつものように鹿爪らしく控えているのです。この余裕ある私の学生生活が私を詰るのです。それを誤解だといって、起って行って頂きましょうなどと調子を合せていた私はついに先生を見逃したかも知れませんが、話の区切りの付くまで二人の相手になって来たには相違ありませんから九月に入って行こうといって、私を先生の眉間に認めたところであった。静かな素人屋に下宿するくらいの人だからというだけで、取り合ってくれないのです。
</p>
</div>
</section>
<secion class="uvn" role="article" aria-label="リンク">
<ul class="u9n">
<li>
<a class="v1q t4j" href="#page1">
<b class="t90">PAGE_1</b>
<span class="uqv">
私は父の神経を過敏にしたくなかったのです。まあ活花の程度ぐらいなものだろうと思いますわ奥さんの言葉は少し手痛かった。
</span>
<i class="v3b s2g">READ MORE</i>
</a>
</li>
<li>
<a class="v1q vnk" href="#page2">
<b class="t90">PAGE_2</b>
<span class="uqv">
そうしてその妻をいっしょに連れて来なければ、容易に腰を上げない事さえありました。それで奥さんに対して面と向うには足りませんよ。
</span>
<i class="v3b vtn">READ MORE</i>
</a>
</li>
</ul>
</secion>
<section class="tf4" role="contentinfo" aria-label="フッタ">
<picture class="vb6">
<source srcset="img/footer.webp" type="image/webp" />
<img class="tqg" src="img/footer.jpg" alt />
</picture>
<article class="shj">
<div>
<picture class="va0">
<source srcset="img/logo.webp" type="image/webp" />
<img class="v69" src="img/logo.png" alt="Phew Co., Ltd. ロゴ画像" />
</picture>
<aside class="ulg">
<strong class="tq5">Phew Co., Ltd.</strong>
<em class="vru">〒369-011𝒊 埼玉県鴻巣市新宿御苑2丁目634-333</em>
</aside>
</div>
<footer class="sov">
<h2>©2024 Phew Co., Ltd. as Gothicum.</h2>
<p>version 1.0.0-2000-0101-1203</p>
</footer>
</article>
</section>
</div>
</main>
</body>
</html>
ちなみにCSSファイルでは10.50%、HTMLファイルでは19.68%の容量削減になりました。
これが大規模サイトになるともっと効果が出ると思いますが、今のところHTML・CSS・Javascriptにしか対応してないんですよね。
PHPファイルは未対応です。
(追記:version 1.1.0でPHPに対応しました!)
使い心地選手権としてはTailwindCSS + 変造BEM + gulpfile.mjs + PostCSS UUID Obfuscatorが優勝。
ほとんど一度雛形を作ってしまえば使い回しも効きますしで。