クモのようにコツコツと

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

【EJS】fsモジュールを使ってコンテンツのJSONデータを読み込む

メタ言語の続きです。前回はEJSのinclude()を使ってBEMのブロックごとにモジュール分割しました。今回はコンテンツ(見出し、本文など)部分を外部JSONファイルにしてfsモジュールを使って読み込みます。それではいきましょう!

【目次】

※参考:前回記事
【EJS】include()でBEMのブロックごとにモジュール分割(Sass(SCSS)も修正) - クモのようにコツコツと

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

前回のおさらい

EJSファイルもBEMのブロックごとにモジュール分割した!
f:id:idr_zz:20200604072135j:plain
Sass(SCSS)と同じような構成になった。

コンパイルすると「index.html」1ファイルになる。
f:id:idr_zz:20200604072324j:plain
モジュールはファイル名に_を付けるとパーシャル化。

ブラウザではこのような表示。 f:id:idr_zz:20200604075733j:plain ヘッダー、フッターを新規モジュールとして追加した。

詳細は前回記事を参照

※参考:前回記事
【EJS】include()でBEMのブロックごとにモジュール分割(Sass(SCSS)も修正) - クモのようにコツコツと

index.ejsにコンテンツ部分がある

ただ、前回宿題にした課題がある。

「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 イイダリョウ'
    }
%>

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

後半のタグ部分はモジュール読み込みによってかなりスッキリしたが、前半にコンテンツ(見出しや本文など)部分が残っている。

ここも別ファイルに切り出したい。

_contents.ejs作成:コンテンツのモジュール化(うまくいかず…)

まず最初に試みたのは、冒頭のコンテンツ部分のモジュール化。

_contents.ejsを作成

「contents」フォルダを作って「_contents.ejs」を作成。
f:id:idr_zz:20200610063848j:plain

「_contents.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 イイダリョウ'
    }
%>

_contents.ejsをインクルード

「index.ejs」からinclude()メソッドで「_contents.ejs」をインクルードする

<%- include('contents/_contents'); %>

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

見た目はスッキリ!素晴らしい!

挙動確認(エラー!)

が、この状態でコンパイルするとうまく行かなかった。。

ターミナルでcdコマンドでフォルダに移動

cd /(パス)/gulp_frontend

gulpを起動

npx gulp

するとこんなエラーが…

 ReferenceError in plugin "gulp-ejs"
Message:
    /(パス)/gulp_frontend/src/ejs/index.ejs:6
    4| <html>
    5| <head>
 >> 6|  <%- include('base/_head', {main: main}); %>
    7| </head>
    8| <body>
    9|  <%- include('block/_header', {header: header}); %>

main is not defined

_head.ejsに渡す変数mainが見つからないよ、と。。

どうやら変数はindex.ejs上にないとダメっぽい。一つ目だけ表示されてるけど、他の変数も同じことでしょうな。

_contents.ejsの関数化(うまくいかず)

次に試したのは _contents.ejsの関数化。下記の記事のような方法。

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

※参考:EJSでテンプレートとデータでファイルを切り分けた開発を行う | 大阪市天王寺区SOHOホームページ制作 | デザインサプライ-DesignSupply.-

こんな感じでコンテンツ部分を_contents.ejsの連想配列(オブジェクト)を関数化する。

<% contents = function () { 
    return {
        header: {
            title:'クモのようにコツコツと ~別館~',
            text: 'イイダリョウのメタ言語実験室'
        }
        // 中略
    },
} %>
  • データ全体をグローバル変数contentsで囲う
  • contentsの値をfunction宣言で関数化する
  • 中のコンテンツ部分をreturnで返す
  • 変数だったheaderなどは全て連想配列(オブジェクト)のキーにする

インクルード部分はオブジェクト形式にする。

<head>
   <%- include('base/_head', {main: contents.main}); %>
</head>
<!-- 後略 -->
  • include ()メソッドの第2引数をcontents.mainなど、contentsオブジェクトのプロパティとして読み込む

しかしこの方法は自分の場合はエラーになり、うまくいかなかった。。

fsモジュールJSONファイルを読み込む

fsモジュールを読み込む

先ほどのこの記事にも言及されているように、データの量が増えていくと関数よりJSONファイルを読み込む方法がいいようだ。

※参考:EJSでテンプレートとデータでファイルを切り分けた開発を行う | 大阪市天王寺区SOHOホームページ制作 | デザインサプライ-DesignSupply.-

JSONファイルの読み込みには「fs」モジュールを使う。以前Node.jsでファイルを表示した時にも使ったモジュール。

※参考:【Node.js】ルーティング設定でCSS、JSファイルを読み込む - クモのようにコツコツと

「gulpfile.js」で「fs」モジュールを読み込む

const fs = require('fs');

EJSコンパイル部にJSONの設定を追記

EJSコンパイル部分を修正

// EJSコンパイル
gulp.task('ejs',  (done) => {
    const data = JSON.parse(fs.readFileSync('./src/json/data.json'));
    gulp.src(["./src/ejs/**/*.ejs", "!./src/ejs/**/_*.ejs"])
      .pipe(plumber())
      .pipe(ejs(data))
      .pipe(ejs({}, {}, { ext: ".html" }))
      .pipe(rename({ extname: ".html" }))
      .pipe(replace(/^[ \t]*\n/gmi, ""))
      .pipe(gulp.dest("./dest/"));
    done();
});
  • 変数dataJSON.parse()メソッドを実行。引数はfsモジュールのreadFileSync()メソッド
  • readFileSync()メソッドの引数は「data.json」のパス
  • .pipe()を追加。ejs()メソッドでdataを読み込む

JSON.parse()メソッドはJSONデータをJSに変換する(ちなみに逆の変換はJSON.stringify()

※参考:REST APIとは何かを調べまくったらようやくイメージができてきたのでまとめた - クモのようにコツコツと

JSONファイルは拡張子が「.json」になるので「ejs」フォルダ内ではなく「src」フォルダ直下に「json」ファイルを作ってその中に置こうと思う。

そのため、パスを./src/json/data.jsonにした。

watchの監視にJSONファイルを追加

jsonファイルの修正もwatch()メソッドで監視したい。

// 監視ファイル
gulp.task('watch-files', (done) =>  {
    gulp.watch(["./src/ejs/**/*.ejs", "./src/json/**/*.json"], 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();
});
  • ejsファイルを監視してたwatch()の第1引数を配列にしてjsonファイルも監視対象に追加

これでJSONファイルを修正するとejsタスクでコンパイルされて、ブラウザも自動リロードするはず。

JSONデータを作成

まず先ほどの「contents」フォルダと「_contents.ejs」は削除する。

「ejs」フォルダの中ではなく「src」フォルダ直下に「json」フォルダを作成。その中に「data.json」を作る。

f:id:idr_zz:20200611071806j:plain

「data.json」の中身

{
    "data": {
        "header": {
            "title":"クモのようにコツコツと ~別館~",
            "text": "イイダリョウのメタ言語実験室"
        },
        "main": {
            "title":"メタ言語同時コンパイル(EJS、Sass(SCSS)、TypeScript)",
            "lede": "CodepenのSettingの旅CSS編第2回。今回はSCSS。LESSに似てる?",
            "link": "別ページへ"
        },
        "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>"
            }
        ],
        "footer": {
            "text":"© 2020 イイダリョウ"
        }
    }
}
  • 全体をdataキーでheadermaininnerfooterキーを包んでいる
  • シングルコーテーションは全てダブルコーテーションにする
  • キー名も全てダブルコーテーションを付ける
  • タグの中の属性値はシングルコーテーションにする(重複しないように)

JSONデータはダブルコーテーションがメインの世界。

※参考:JSON入門 - とほほのWWW入門

index.ejsでJSONデータをリンク(コンテンツ部分は削除)

インクルード部分を修正

index.ejsでインクルード部分を修正

<!doctype html>
<html>
<head>
   <%- include('base/_head', {main: data.main}); %>
</head>
<body>
    <%- include('block/_header', {header: data.header}); %>
    <%- include('block/_main', {main: data.main, inner: data.inner}); %>
    <%- include('block/_footer', {footer: data.footer}); %>
</body>
</html>
  • 冒頭にあった「contents.ejs」のインクルードは削除
  • include()で渡す変数はdata.mainなどのようにdataオブジェクトのプロパティとして読み込む

index.ejsすごくシンプルになった!

ejsファイル上ではJSONファイルのインクルードの記述は不要で、JSONデータの中に書かれているキー名を追記するだけでOK!

自分はこの方法が断然いい!

動作確認

「gulpfile.js」の変更を反映するため、いったんGulpを停止する。

「Control」+「C」を押すとターミナルにこの記述が出る。

^C

gulp再起動!

npx gulp

ブラウザが立ち上がる f:id:idr_zz:20200604075733j:plain やた!JSONデータのテキストが読み込まれて表示されとる!

JSONデータを打ち替えてみる

このテキストが本当にJSONデータからのテキストなのかを確認したい。

headerの修正

headertextに「!」を5つ追加してみると…

         "header": {
            "title":"クモのようにコツコツと 〜別館〜",
            "text": "イイダリョウのメタ言語実験室!!!!!"
        },

おお、ヘッダーのテキストに反映された!!!!! f:id:idr_zz:20200611073547j:plain

mainの修正

次にmainledeのテキストも打ち替えてみる

        "main": {
            "title":"メタ言語同時コンパイル(EJS、Sass(SCSS)、TypeScript)",
            "lede": "HTMLテンプレートエンジンとAltCSSとALtJSを同時にコンパイルする!!",
            "link": "別ページへ"
        },

h1タグの下のリード文が打ち変わった! f:id:idr_zz:20200611073751j:plain

innerの修正

最後にinnerに段落(titletextのセット)を追加してみる

        "inner": [
            {
            "title":"JSONデータの読み込み",
            "text": "コンテンツ部分を外部のJSONデータから読み込むことができる。<br>gulpfile.jsでfsモジュールを使って設定している。"
            },
            // 中略
        ],

本文に段落が追加された!楽ちん!!! f:id:idr_zz:20200611074206j:plain

最後に:データとテンプレートの分離

今回、コンテンツ部分を以下の方法を試みました

  • モジュール化→変数が読み込まれず
  • モジュールの関数化→変数が読み込まれず
  • JSONファイル化→変数が読み込まれた!

JSONファイルを読み込むにはgulpfileでfsモジュールを使った処理を設定する必要がありました。

それによってデータとテンプレートの分離がより明確になり、結果的には一番いい方法になったと思います!

JSONデータを読み込むということは、今回のような自作JSONファイルだけでなくデータベースとの連携などもできそうです。

次回からはTypeScriptのモジュール化を体験してみたく思います。それではまた!


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