クモのようにコツコツと

フロントエンドエンジニア イイダリョウの技術ブログ。略称「クモコツ」

【メタ言語】BEMによるclass名をSass(SCSS)とEJSで書いてみる(モジュール事始め)

メタ言語の続きです。前回まではメタ言語(EJS、Sass(SCSS)、TypeScript)を同時にコンパイルする環境を作っていました。これによって、これまで「class名が長くなるんだよなー」という理由であまり手を出してこなかったBEMによるclass名がもっと楽につけれるのではと思い、やってみました。このBEM化が「モジュール設計」の事始めでもあります。それでは行きましょう!

【目次】

※参考:【gulp】gulp-replaceの引数に書かれている正規表現を掘り下げる - クモのようにコツコツと

※メタ言語(HTMLテンプレートエンジン、AltCSS、AltJS)まとめ
qiita.com

BEMとは何か

妖怪人間、ではなくCSS設計のための命名規則。「Blocks, Elements, Modifiers」の略。

f:id:idr_zz:20200517080741j:plain

公式ページ(英語)

※参考:BEM — Block Element Modifier

日本語訳してくださった記事!

※参考:BEM公式ページ_日本語訳 - Qiita

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名にしてみる。

f:id:idr_zz:20200517084728j:plain

  • mainblockの中にinnerブロックがある
  • mainblockの中にはmain__titlemain__textなどのエレメントがある
  • innerブロックの中にはinner__titleinner__textなどのエレメントがある
  • inner__textエレメントの中にinner__text-eminner__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のオブジェクト{ }を定義。キー名はtitleledelink
  • 変数inner.innerのオブジェクト{ }を定義。キー名はtitletext
  • innerのオブジェクト{ }は複数がるので全体を配列[ ] に入れる

innerのオブジェクトは配列に入れることでinner[0]などinneri番目、みたいな指定ができるようにした。(後で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ごとにファイルを分ける」というアドバイスをいただきました。

それが今回の記事のきっかけになっている。なので、現時点ではまだ単一ファイルなのですが、このBEM化がモジュール設計の「はじめの一歩」的な記事になるわけです。それではまた!


※メタ言語(HTMLテンプレートエンジン、AltCSS、AltJS)まとめ
qiita.com