クモのようにコツコツと

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

【EJS】include()でBEMのブロックごとにモジュール分割(Sass(SCSS)も修正)

メタ言語の続きです。前回はSass(SCSS)の変数($)や@mixinを使ってモジュールを超えた値の受け渡しをしました。今回はEJSの方もBEMのブロックごとにモジュールファイルを分割してみたく。include()メソッドを使います。それではいきましょう!

【目次】

※参考:前回記事
【Sass(SCSS)】変数($)、@mixinを使ってモジュールを超えた共通スタイルを設定する - クモのようにコツコツと

※参考:【メタ言語】HTMLテンプレートエンジン、AltCSS、AltJSのまとめ
qiita.com

前回までのおさらい(Sass(SCSS)のモジュール分割)

「scss」フォルダのファイル構成 f:id:idr_zz:20200530080408j:plain

前々回、Sass(SCSS)の方は「block」フォルダでBEMのブロックごとにモジュールをファイル分割。@importを使ってstyle.sccsでインポートした。

※参考:【Sass(SCSS)】@importでBEMのBlockごとにファイル分割する - クモのようにコツコツと

そして前回「setting」フォルダで変数や@mixinの設定ファイルも作成した。

※参考:【Sass(SCSS)】変数($)、@mixinを使ってモジュールを超えた共通スタイルを設定する - クモのようにコツコツと

なお、ファイル名の頭に_がついたファイルは「パーシャル」ファイルとしてコンパイルからは除外される。

今回はEJSの方も同じようにBEMのブロックごとにモジュールのファイル分割を行いたい。

「ejs」フォルダを作ってejsファイルを移動

これまでejsファイルはhtmlファイルと同様に「src」フォルダの直下に置いていた。
f:id:idr_zz:20200603183710j:plain
しかしこれからejsファイルをモジュール分割していくと多分収拾がつかなくなりそうな気がする。

そのため、「ejs」フォルダを作ってejsファイルをその中に移動してみた。 f:id:idr_zz:20200603183817j:plain
これからはこのフォルダでモジュール分割を行っていく。

gulpfile.jsのパスを書き換え

ejsタスクを修正

ejsファイルをejsフォルダに移動したことで「gulpfile.js」の中のejsコンパイル設定がリンク切れになると思ふ。パスを書き換えよう。

EJSコンパイル部分

// EJSコンパイル
gulp.task('ejs',  (done) => {
    gulp.src(["./src/ejs/**/*.ejs", "!./src/ejs/**/_*.ejs"])
      .pipe(plumber())
      .pipe(ejs({}, {}, { ext: ".html" }))
      .pipe(rename({ extname: ".html" }))
      .pipe(replace(/^[ \t]*\n/gmi, ""))
      .pipe(gulp.dest("./dest/"));
    done();
});
  • gulp.src()の引数にあるパスに/ejs/**を追加(2箇所)

ちなみに、このejsタスクを作っている当時はわかっていなかったのだが

gulp.src(["./src/ejs/**/*.ejs", "!./src/ejs/**/_*.ejs"])

このgulp.src()の引数にある配列の2番目は頭に!が付いている。!は「○○以外」なのでコンパイルから除外するという意味だった。

これによってSass(SCSS)と同様にファイル名の頭に_を入ったファイルがパーシャル(コンパイル除外)になる!

watchタスクを修正

watchの監視ファイルも変更

// 監視ファイル
gulp.task('watch-files', (done) =>  {
    gulp.watch("./src/ejs/**/*.ejs", gulp.task('ejs'));
    gulp.watch("./dest/*.html", gulp.task('browser-reload'));
    gulp.watch("./src/scss/**/*.scss", gulp.task('sass'));
    gulp.watch("./dest/css/*.css", gulp.task('browser-reload'));
    gulp.watch("./src/ts/*.ts", gulp.task('ts'));
    gulp.watch("./dest/js/*.js", gulp.task('browser-reload'));
    done();
});
  • gulp.watch()のejsの部分に/ejs/**を追加

Sass(SCSS)の時と同じく/**で下層フォルダ全てを適用範囲にする。 パスの変更はあくまでコンパイル前のejsファイルのみのため、htmlの部分は変更しない。

gulp起動

動作確認してみよう。ターミナルでcdコマンドでフォルダに移動。

cd /(パス)/gulp_frontend

gulp実行!

npx gulp

ブラウザが立ち上がる。 f:id:idr_zz:20200530092242j:plain よし!cssファイルやjsファイルのリンクも問題ないようだ!

include()メソッドで別ファイルをインクルード

Sass(SCSS)、別ファイルを読み込む時に@importを使った。

@import 'フォルダ/ファイル';

※参考:【Sass(SCSS)】@importでBEMのBlockごとにファイル分割する - クモのようにコツコツと

同じような機能がEJSにもあり、include()メソッドを使う。

<%- include('フォルダ/ファイル') %>

第二引数で別ファイルの中にある変数の値を渡すこともできる。

<%- include('フォルダ/ファイル', {キー: 値}) %>

※参考:テンプレートエンジンEJSで使える便利な構文まとめ - Qiita

この@include()メソッドを使ってBEMブロックごとにejsファイルをパーシャル化しよう。

BEMのブロックごとにパーシャル化

headタグをパーシャルファイルにしてみる

まずは「base」フォルダを作り「_head.ejs」を作成。ファイル名に_をつけることでパーシャルファイルになる。

<meta charset="UTF-8">
<title><%= main.title %></title>
<meta name="description" content="<%= main.lede %>">
<link rel="stylesheet" href="css/style.css">
<script src="js/script.js"></script>

中身はmetaタグの中身をそのままペースト。

この部分はBEMブロックではなくページに表示されない設定部分のため「base」フォルダにした次第。

index.ejsのheadタグの中身をinclude()メソッドにする

<head>
   <%- include('base/_head', {main: main}); %>
</head>
  • include()メソッドの第1引数は「base」フォルダの_head.ejs(拡張子は省略)
  • include()メソッドの第2引数でmainキーに変数mainを渡す設定

わかりやすいようにキー名は変数mainと同じにしてみた。

mainブロックをパーシャルファイルにする

次に「block」フォルダを作成。その中に「_main.ejs」を作る。

<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>

mainブロック部分をそのままペースト。

index.ejsのmainブロックがあった部分にinclide()メソッド

<body>
    <%- include('block/_main', {main: main, inner: inner}); %>
</body>
  • include()メソッドの第1引数は「block」フォルダの_main.ejs(拡張子は省略)
  • include()メソッドの第2引数でmainキーに変数mainを、innerキーに変数innerを渡す設定

innerブロック部分をパーシャルファイルにする

「block」フォルダの中に「_inner.ejs」を作成

<% 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>  
<% } %>

innerブロック部分をペースト

「_main.ejs」の中のinnerブロック部分をinclude()メソッドに

<section id="scss" class="main">
    <h1 class="main__title"><%- main.title %></h1>
    <p class="main__lede"><%- main.lede %></p>
    <%- include('_inner', {inner: inner}); %>
    <p class="main__text"><a href="other.html" class="main__text-link"><%= main.link %></a></p>
</section>
  • include()メソッドの第1引数は同一フォルダの_inner.ejs(拡張子は省略)
  • include()メソッドの第2引数でinnerキーに変数innerを渡す設定

ブラウザ表示確認

この状態でブラウザを見ると…
f:id:idr_zz:20200530092242j:plain
よし、index.htmlに無事コンパイルさている!EJSでは初のモジュールインクルード成功♪〜

今度はモジュールの新規作成をしてみる。ヘッダーフッターを追加する。

ヘッダーの追加

_header.ejsを作成

「block」フォルダの中に「_header.ejs」を作成

<header class="header">
    <h1 class="header__title"><%- header.title %></h1>
    <p class="header__text"><%- header.text %></p>
</header>
  • headerタグのclass名はブロック名.headerにする
  • .headerの中にエレメント.header__title.header__textを入れる
  • .header__titleにはheaderオブジェクトのtitleプロパティを読み込む
  • .header__textにはheaderオブジェクトのtextプロパティを読み込む

既存の.mainブロックなどと同じBEMの構成。ちなみにheaderオブジェクトはまだないw これから作成する。

index.ejsでheaderモジュールをインポート

「index.ejs」でheaderモジュールをインポートする

<body>
    <%- include('block/_header', {header: header}); %>
    <%- include('block/_main', {main: main, inner: inner}); %>
</body>

bodyタグの中、mainブロックの上にheaderモジュールをインポートする

headerオブジェクトを作成

先程headerモジュールの中で読み込む設定にしたheaderオブジェクトを作成

<% 
    let header = {
        title:'クモのようにコツコツと ~別館~',
        text: 'イイダリョウのメタ言語実験室'
    }
   // 中略
%>
  • 変数headerの値は連想配列
  • titleキーの値は文字列
  • textキーの値は文字列

この構成もmainモジュールなどと同じ

コンパイル確認

この状態でブラウザを確認すると… f:id:idr_zz:20200604065203j:plain やた!ヘッダーが表示された!(CSSはまだ設定してないので左寄せだが)

index.htmlにもヘッダーがコンパイルされている。

<body>
    <header class="header">
    <h1 class="header__title">クモのようにコツコツと ~別館~</h1>
    <p class="header__text">イイダリョウのメタ言語実験室</p>
</header>
<!-- 中略 -->
</body>

インデントがおかしいのが少し気になるが。。

mainブロックにmainタグを追加

header、footerのタグとの見通しをよくするためmainブロック(_main.ejs)のタグ全体をmainタグで囲った。

<main>
    <section id="scss" class="main">
        <h1 class="main__title"><%- main.title %></h1>
        <p class="main__lede"><%- main.lede %></p>
        <%- include('_inner', {inner: inner}); %>
        <p class="main__text"><a href="other.html" class="main__text-link"><%= main.link %></a></p>
    </section>
</main>

フッターの追加

_footer.ejsを作成

同じ方法でフッターも追加してみる。「block」フォルダの中に「_footer.ejs」を作成

<footer class="footer">
    <p class="footer__text"><%- footer.text %></p>
</footer>
  • footerタグのclass名はブロック名.footerにする
  • .footerの中にエレメント. footer__textを入れる
  • .footer__textにはfooterオブジェクトのtextプロパティを読み込む

headerモジュールと同じ構成(タイトルはなし)

index.ejsでfooterモジュールをインポート

footerモジュールもインポートする

<body>
    <%- include('block/_header', {header: header}); %>
    <%- include('block/_main', {main: main, inner: inner}); %>
    <%- include('block/_footer', {footer: footer}); %>
</body>

bodyタグの中、mainブロックの下にfooterモジュールをインポートする

footerオブジェクトを作成

こちらもheaderオブジェクトと同じ

<%
    // 中略
    let footer = {
        text:'© 2020 イイダリョウ'
    }
%>
  • 変数footerの値は連想配列
  • textキーの値は文字列

コンパイル確認

この状態でブラウザを確認すると… f:id:idr_zz:20200604070222j:plain やた!フッターも表示された!(CSS設定はないのでこちらも左寄せ)

index.htmlにもフッター部分がコンパイルされている!

<body>
    <!-- 中略 -->
    <footer class="footer">
    <p class="footer__text">© 2020 イイダリョウ</p>
</footer>
</body>

やっぱりインデントはおかしい。。

今回変更したコード(全体)

index.ejs(オブジェクト部分)

<% 
    let header = {
        title:'クモのようにコツコツと ~別館~',
        text: 'イイダリョウのメタ言語実験室'
    }
    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>'
        }
    ]
    let footer = {
        text:'© 2020 イイダリョウ'
    }
%>

冒頭のオブジェクトはページに表示するテキストなど。だんだんボリュームが増えてきているのでなんとかしたい。単純にinclude()するだけだとこの中のキー名が他のモジュールに読み込まれなくなったため、次回に対策する。

index.ejs(タグ部分)

<!doctype html>
<html>
<head>
   <%- include('base/_head', {main: main}); %>
</head>
<body>
    <%- include('block/_header', {header: header}); %>
    <%- include('block/_main', {main: main, inner: inner}); %>
    <%- include('block/_footer', {footer: footer}); %>
</body>
</html>

後半はページタグの部分。こちらはモジュール化したおかげででかなりスッキリした!

_head.ejs

headタグで読み込まれるmetaタグなどのモジュール

<meta charset="UTF-8">
<title><%= main.title %></title>
<meta name="description" content="<%= main.lede %>">
<link rel="stylesheet" href="css/style.css">
<script src="js/script.js"></script>

_header.ejs

ヘッダー部分のモジュール

<header class="header">
    <h1 class="header__title"><%- header.title %></h1>
    <p class="header__text"><%- header.text %></p>
</header>

_main.ejs

ページ本文の部分のモジュール。

<main>
    <section id="scss" class="main">
        <h1 class="main__title"><%- main.title %></h1>
        <p class="main__lede"><%- main.lede %></p>
        <%- include('_inner', {inner: inner}); %>
        <p class="main__text"><a href="other.html" class="main__text-link"><%= main.link %></a></p>
    </section>
</main>

中でincludeモジュールを読み込んでいる。

_include.ejs

mainモジュールに読み込まれているincludeモジュール

<% 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>    
<% } %>

for文でオブジェクトの数量分ループしている

_footer.ejs

フッター部分のモジュール

<footer class="footer">
    <p class="footer__text"><%- footer.text %></p>
</footer>

ファイル構成

モジュール分割されたファイル構成 f:id:idr_zz:20200604072135j:plain

  • baseフォルダの中にheadモジュール
  • blockフォルダの中にheader、main、include、footerモジュール

コンパイルされたindex.html

コンパイルされるとこうなる f:id:idr_zz:20200604072324j:plain
(other.htmlは手付かずだがindex.htmlにモジュールが統合される)

index.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/style.css">
<script src="js/script.js"></script>
</head>
<body>
    <header class="header">
    <h1 class="header__title">クモのようにコツコツと ~別館~</h1>
    <p class="header__text">イイダリョウのメタ言語実験室</p>
</header>
    <main>
    <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>
</main>
    <footer class="footer">
    <p class="footer__text">© 2020 イイダリョウ</p>
</footer>
</body>
</html>

おまけ:Sass(SCSS)にヘッダー、フッターのモジュールを追加

ヘッダー、フッターのブロックにCSSが当たってないので、Sass(SCSS)にもモジュールを追加する。

_mixin.scssにページ横幅設定のmixinを追加

「setting」フォルダの「_mixin.scss」にページの横幅のmixinを追加した(複数のモジュールで共通するため)

@mixin pageSize($width: 960px) {
    width: $width;
    margin: 0 auto;
}
  • この設定は「_main.scss」にあったページ横幅設定
  • widthは後から変えられるように引数$widthにしてみた(初期値は960px

pageSizeのmixinを「_main.scss」でインクルード

/* main */
.main {
    @include pageSize();
    &__title {
      color: $text-color;
      font-size: 2em;
    }
    &__lede {
      font-size: 1em;
    }
  }
  • ページ幅の設定があった部分を@includeで差し替える(引数は変更しないので空欄)

ブラウザ表示を確認すると…
f:id:idr_zz:20200604070222j:plain よし、ページ幅は保持されている。

header.scss、footer.scssを作成

「block」フォルダの中に「_header.scss」を作成

/* header */
.header {
    @include pageSize();
}

pageSizeのmixinをインクルード

同様に「_footer.scss」も作成

/* footer */
.footer {
    @include pageSize();
}

pageSizeのmixinをインクルード

ブラウザを見ると
f:id:idr_zz:20200604073309j:plain よし!ヘッダーフッターにもページ幅を適用されたぞ

「src」フォルダにヘッダー用の画像を追加

もう少し細かくスタイルを設定しよう。

まず「dest」フォルダに「img」フォルダを作成し、画像を追加。 f:id:idr_zz:20200604073551j:plain

これはヘッダーに使う画像を追加(当ブログにヘッダーと同じ画像)

今回は画像はコンパイルを行わない*1ため、「src」フォルダ直置きでいいかと。

_header.scssを修正

ヘッダーで先程追加した画像を背景に使う。

/* header */
.header {
    @include pageSize();
    padding: 80px 10px;
    background-image: url(../img/header_bg.jpeg);
    background-size: cover;
    text-align: center;
    &__title {
        margin: 0;
        color: $text-color_w;
        text-shadow: 0 0 20px #000;
    }
}

その他、titleを当ブログと同じ白文字のシャドー付きにしたり。

_footer.scssを修正

フッターも上に線を表示したりテキストセンター寄せにしたりと微調整

/* footer */
.footer {
    @include pageSize();
    padding: 20px 0;
    border-top: 1px solid #ccc;
    text-align: center;
    &__test {
        font-size: 0.8em;
    }
}

ブラウザ表示確認

こんなんでました! f:id:idr_zz:20200604075733j:plain ヘッダー画像が付いて少し寂しさが紛れたw

CSSコンパイル結果

style.cssにもヘッダー、フッターが追加されている

@charset "UTF-8";
/***** base *****/
body {
  margin: 0;
  padding: 0;
  font-family: sans-serif;
  color: #333;
}

body a {
  color: #C30D23;
}

/***** block *****/
/* header */
.header {
  width: 960px;
  margin: 0 auto;
  padding: 80px 10px;
  background-image: url(../img/header_bg.jpeg);
  background-size: cover;
  text-align: center;
}

.header__title {
  margin: 0;
  color: #fff;
  text-shadow: 0 0 20px #000;
}

/* main */
.main {
  width: 960px;
  margin: 0 auto;
}

.main__title {
  color: #333;
  font-size: 2em;
}

.main__lede {
  font-size: 1em;
}

/* inner */
.inner {
  margin: 0 0 40px;
}

.inner__title {
  padding: 10px;
  background: #eee;
  border-left: 2px solid #C30D23;
  font-size: 1.5em;
  line-height: 1.2;
}

.inner__text-em {
  font-weight: bold;
  background: #eee;
  padding: 5px;
  color: #C30D23;
}

.inner__text-bikou {
  font-size: 0.8em;
}

.inner__text-bikou:before {
  content: "※";
}

.inner__text-flex {
  display: flex;
  flex-wrap: nowrap;
  justify-content: center;
  align-items: stretch;
}

.inner__text-btn {
  background: #C30D23;
  color: #fff;
  padding: 10px;
}

/* footer */
.footer {
  width: 960px;
  margin: 0 auto;
  padding: 20px 0;
  border-top: 1px solid #ccc;
  text-align: center;
}

.footer__test {
  font-size: 0.8em;
}

最後に

ということでSass(SCSS)に続き、EJSのほうもBEMの構成でモジュール分割しました!

やってみるとだんだん慣れていくし、あとあとの修正時にどこを直せばいいのか直感的にわかりそうなのがいいです。(縦に長〜いファイルだと検索置換などで探す必要がありますかね。。)

次回は今回宿題になった、EJSの本文部分のオブジェクトを外部ファイルにして読み込みたく思います。それではまた!


※参考:【メタ言語】HTMLテンプレートエンジン、AltCSS、AltJSのまとめ
qiita.com

*1:gulp画像圧縮などもできるようだが