メタ言語の続きです。前回まではメタ言語(EJS、Sass(SCSS)、TypeScript)を同時にコンパイルする環境を作っていました。これによって、これまで「class名が長くなるんだよなー」という理由であまり手を出してこなかったBEMによるclass名がもっと楽につけれるのではと思い、やってみました。このBEM化が「モジュール設計」の事始めでもあります。それでは行きましょう!
【目次】
- BEMとは何か
- BEMの構成を考える
- Sass(SCSS)でBEMを書く
- EJSでBEMを書く
- エラーでgulpを止めないgulp-plumber(2020/05/18追記)
- 最後に:モジュール化して行きます!
※参考:【gulp】gulp-replaceの引数に書かれている正規表現を掘り下げる - クモのようにコツコツと
※メタ言語(HTMLテンプレートエンジン、AltCSS、AltJS)まとめ
qiita.com
BEMとは何か
妖怪人間、ではなくCSS設計のための命名規則。「Blocks, Elements, Modifiers」の略。
公式ページ(英語)
※参考:BEM — Block Element Modifier
日本語訳してくださった記事!
BEMは下記の3つの概念がある。
- Blocks:独立して存在できるブロック
- Elements:ブロックの中のエレメント(部品)
- Modifiers:ブロックやエレメントの外観/機能を変更するモディファイア
BlocksとElementsはアンダースコア2つ__
で繋ぎ、Modifiers
はハイフン-
で繋ぐ。
.Blocks__Elements-Modifiers { /* スタイル */ } .Elements-Modifiers { /* スタイル */ }
ブロックが入れ子になっていてもclass名としてはこのBlocks__Elements-Modifiers
の3階層がマックスになる。
それでもclass名が長く、重複するためこれまであまり積極的に使ってこなかった。しかし、より堅牢なCSS設計をする上では使った方がベターだし、Sass(SCSS)やEJSなどのメタ言語を使えばこの長い名前や重複もそんなに苦ではないような気がしてきた。
なお、class名で完結しid名は用いない。id名で優先度を変えるのではなく、Modifiers
でより詳細に特定する。
BEMの構成を考える
前回作ったトップページをBEMのclass名にしてみる。
main
blockの中にinner
ブロックがあるmain
blockの中にはmain__title
、main__text
などのエレメントがあるinner
ブロックの中にはinner__title
、inner__text
などのエレメントがあるinner__text
エレメントの中にinner__text-em
、inner__text-bikou
などのモディファイアがある
Sass(SCSS)でBEMを書く
BEMで書いたSass(SCSS)
Sass(SCSS)はネスト(入れ子)で書けて、&
で親セレクタの名前を継承できる。この機能を使うとBEMのclass名を楽に書ける!
.main__lede { font-size: 0.8em; } .inner { background: #fff; padding: 20px; margin: 0 0 20px; &__title { color: purple; font-size: 1.5em; line-height: 1.2; } &__text { &-em { font-weight: bold; color: brown; } &-bikou { font-size: 0.8em; &:before { content: "※"; } } &-btn { background: brown; color: #fff; padding: 10px; } }
コンパイルされたCSS
CSSコンパイルするとこうなった!
@charset "UTF-8"; .main__lede { font-size: 0.8em; } .inner { background: #fff; padding: 20px; margin: 0 0 20px; } .inner__title { color: purple; font-size: 1.5em; line-height: 1.2; } .inner__text-em { font-weight: bold; color: brown; } .inner__text-bikou { font-size: 0.8em; } .inner__text-bikou:before { content: "※"; } .inner__text-btn { background: brown; color: #fff; padding: 10px; }
うん、この長いclass名もSass(SCSS)を使えば辛くない♪
EJSでBEMを書く
class名を変数にしたらわかりにくい
最初、class名を変数にしようとした
<% const B1 = 'main'; const B1E1 = B1 + '__title' const B1E2 = B1 + '__text' %> <section class="<%- B1 %>"> <h2 class="<%- B1E1 %>">タイトル</h2> <p class="<%- B1E2 %>">本文本文本文本文、本文本文本文本文。本文本文本文本文、本文本文本文本文。本文本文本文本文、本文本文本文本文。</p> </section>
コンパイルされるとちゃんとBEMのclass名になる。
<section class="main"> <h2 class="main__title">タイトル</h2> <p class="main__text">本文本文本文本文、本文本文本文本文。本文本文本文本文、本文本文本文本文。本文本文本文本文、本文本文本文本文。</p> </section>
しかしEJSの段階ではclass名が記号みたいになり、そのタグがなんのブロックかわかりにくい。直感的ではない構造に感じられた。
コンテンツをオブジェクトとして定義
そこでclass名ではなくコンテンツ(タイトル、テキストなど)の方を読み込む構造にしてみた。テンプレートとコンテンツの分離。むしろ本来のテンプレートエンジンのコンセプトにも合うのではないかと。
まず冒頭でコンテンツ部分を書く。オブジェクト(連想配列)形式にて。
<% let main = { title:'メタ言語同時コンパイル(EJS、Sass(SCSS)、TypeScript)', lede: 'CodepenのSettingの旅CSS編第2回。今回はSCSS。LESSに似てる?', link: '別ページへ' } let inner = [ { title:'ネスト(階層化)', text: 'SCSSもdivのネストの階層化ができる。この段落は<em class="inner__text-em">ネスト</em>でスタイルを当てている。<br>ん?この書き方はLESSとほぼまったく同じだ。<br><span class="inner__text--bikou">インデントについても厳密でじゃでなくても解釈してくれるようだ(?)</span>' }, { title:'親セレクタの参照', text: 'おそらくLESSにはないSCSS専用の機能。ネストして「&」に擬似要素を付けると親セレクタを参照できる<br><span class="inner__text-bikou">この尾行欄につけている「※」印は「.bikou:before」ではなく「&:before」だけで済んでいる。 </span>' }, { title:'TypeScriptで書いたクリックイベント', text: '<button class="inner__text-btn">ここ押せワンワン</button>' } ] %>
- 変数
main
で.main
のオブジェクト{ }
を定義。キー名はtitle
、lede
、link
- 変数
inner
で.inner
のオブジェクト{ }
を定義。キー名はtitle
、text
inner
のオブジェクト{ }
は複数がるので全体を配列[ ]
に入れる
inner
のオブジェクトは配列に入れることでinner[0]
などinner
のi
番目、みたいな指定ができるようにした。(後でfor文に使う)
テンプレート部分を作る
次にテンプレート部分を作る
<!doctype html> <html> <head> <meta charset="UTF-8"> <title><%= main.title %></title> <meta name="description" content="<%= main.lede %>"> <link rel="stylesheet" href="css/common.css"> <link rel="stylesheet" href="css/other.css"> <script src="js/common.js"></script> </head> <body> <section id="scss" class="main"> <h1 class="main__title"><%- main.title %></h1> <p class="main__lede"><%- main.lede %></p> <% for (var i = 0; i < inner.length; i++) { %> <section id="nest" class="inner"> <h2 class="inner__title"><%- inner[i].title %></h2> <p class="inner__text"><%- inner[i].text %></p> </section> <% } %> <p class="main__text"><a href="other.html" class="main__text-link"><%= main.link %></a></p> </section> </body> </html>
- コンテンツ部分をEJSのテンプレートタグ
<% %>
で読み込む - テキストはテキスト内のHTMLタグをタグとして認識させるために
<%- %>
にする .inner
の中は同じ構造の繰り返しなのでfor文でループ。i
番目でinner
の配列を指定
この形式だとclass名とオブジェクト名が共通で直感的に認識しやすい。同じ構造はループにして重複を避けている。コンテンツをオブジェクトから読み込むことでメンテナンス性も担保されている。
なお、<%= %>
は全てテキストとして認識されるが、<%- %>
はテキスト内のHTMLタグがタグとして認識される。
※参考:テンプレートエンジンEJSで使える便利な構文まとめ - Qiita
コンパイルされたHTML
こんなんでました!
<!doctype html> <html> <head> <meta charset="UTF-8"> <title>メタ言語同時コンパイル(EJS、Sass(SCSS)、TypeScript)</title> <meta name="description" content="CodepenのSettingの旅CSS編第2回。今回はSCSS。LESSに似てる?"> <link rel="stylesheet" href="css/common.css"> <link rel="stylesheet" href="css/other.css"> <script src="js/common.js"></script> </head> <body> <section id="scss" class="main"> <h1 class="main__title">メタ言語同時コンパイル(EJS、Sass(SCSS)、TypeScript)</h1> <p class="main__lede">CodepenのSettingの旅CSS編第2回。今回はSCSS。LESSに似てる?</p> <section id="nest" class="inner"> <h2 class="inner__title">ネスト(階層化)</h2> <p class="inner__text">SCSSもdivのネストの階層化ができる。この段落は<em class="inner__text-em">ネスト</em>でスタイルを当てている。<br>ん?この書き方はLESSとほぼまったく同じだ。<br><span class="inner__text--bikou">インデントについても厳密でじゃでなくても解釈してくれるようだ(?)</span></p> </section> <section id="nest" class="inner"> <h2 class="inner__title">親セレクタの参照</h2> <p class="inner__text">おそらくLESSにはないSCSS専用の機能。ネストして「&」に擬似要素を付けると親セレクタを参照できる<br><span class="inner__text-bikou">この尾行欄につけている「※」印は「.bikou:before」ではなく「&:before」だけで済んでいる。 </span></p> </section> <section id="nest" class="inner"> <h2 class="inner__title">TypeScriptで書いたクリックイベント</h2> <p class="inner__text"><button class="inner__text-btn">ここ押せワンワン</button></p> </section> <p class="main__text"><a href="other.html" class="main__text-link">別ページへ</a></p> </section> </body> </html>
意図通りなHTMLになっている!まるでCMSだよこれは!わ〜い♪
エラーでgulpを止めないgulp-plumber(2020/05/18追記)
今回、EJS修正中にエラーが結構起きたんだけど、その度にgulpのwatchが止まって、再度gulpを起動する必要があった。どうにかできないかと調べたらこちらの記事で解決できた!
※参考:TypeScriptやSassやEJSのビルドエラー時にGulpのタスクが終了しないようにする方法 - Qiita
Sass(SCSS)の場合は、gulpfile.jsに下記を書く。
.on("error", sass.logError)
そうそう。これはもう書いてた。おかげでSass(SCSS)の修正では止まらない。
※参考:【Gulp】watch()メソッドでSass(SCSS)を自動コンパイル - クモのようにコツコツと
EJSやTypeScriptの場合は「gulp-plumber」をインストールする必要があると。ターミナルからインストール!
npm i gulp-plumber -D
package.jsonに追記された!
"devDependencies": { //中略 "gulp-plumber": "^1.2.1", //中略 },
gulpfile.jsのEJSに.pipe(plumber())
を追記
// EJSコンパイル gulp.task('ejs', (done) => { gulp.src(["./src/*.ejs", "!./src/_*.ejs"]) .pipe(plumber()) .pipe(ejs({}, {}, { ext: ".html" })) .pipe(rename({ extname: ".html" })) .pipe(replace(/^[ \t]*\n/gmi, "")) .pipe(gulp.dest("./dest/")); done(); });
TypeScriptにも
// TypeScriptコンパイル gulp.task('ts', (done) => { gulp.src('./src/ts/*.ts') .pipe(plumber()) .pipe(typescript()) .js .pipe(gulp.dest('./dest/js')); done(); });
これでSass(SCSS)と同様にエラーでgulpが止まらなくなった!快適〜♪
最後に:モジュール化して行きます!
ということでメタ言語(Sass(SCSS)、EJSを使うとBEMの長〜いclass名が苦もなく使えるようになりました!ここから先、やりたいことがあって、この部品をモジュール化して複数のファイル構成にして行きたいです。
ちょうどフロントエンドのモジュール設計が気になって調べていた時に「吉本式BEM設計」の吉本さんから「BEMはBlockごとにファイルを分ける」というアドバイスをいただきました。
自分はいくつか明確なルールを決めて分割していますが、
— 吉本 集 / aru inc. (@tsuDoi220) 2020年5月14日
ベースとなるルールとして
・必ずblockごとにscssファイルを分ける
ですね。
あと、大事なことは、決めたルールに対して「絶対に例外を作らない」ことです。
例外 = 迷い になってしまいます。
それが今回の記事のきっかけになっている。なので、現時点ではまだ単一ファイルなのですが、このBEM化がモジュール設計の「はじめの一歩」的な記事になるわけです。それではまた!
※メタ言語(HTMLテンプレートエンジン、AltCSS、AltJS)まとめ
qiita.com