15. EJSのレイアウト

レイアウト

レイアウトファイルとは

html, head タグ、ナビゲーション、フッターといった領域はWebページの共通な部分として利用されることが多く、すべてWebページで記述するのは非効率です。

共通部分を利用

レイアウトファイルは、HTMLの外枠部分を共通化したファイルで、各ページのコンテンツはページごとに分離します。

レイアウトのイメージ
<!DOCTYPE html>
<html lang="ja">

<!-- head は共通 -->
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
  <title>Document</title>
</head>

<body>
  <!-- ナビゲーション は共通 -->

  <div class="container">
      <!-- メインコンテンツはページごと -->
  </div>

  <!-- フッターは共通 -->
</body>

</html>

EJSレイアウト

EJS のレイアウトは「express-ejs-layouts」モジュールを利用すると便利です。

インストール

「express-ejs-layouts」をnpmインストールします。

ターミナル
npm i express-ejs-layouts

EJSレイアウトの設定

テンプレートエンジン設定

「express-ejs-layouts」モジュールを読み込み、 EJSをテンプレートエンジンとしてミドルウェアに登録します。

server.js
const layouts = require('express-ejs-layouts');
app.set('view engine', 'ejs');
app.use(layouts);

レイアウトの任意ファイル

レイアウトを任意指定するときは、app.set() で設定します。

server.js
const layouts = require('express-ejs-layouts');
// レイアウトの任意指定
app.set('layout', 'layouts/default');
app.set('view engine', 'ejs');
app.use(layouts);

レイアウトファイルの利用

ファイル構成

express_mvc/
├── data/
│   ├── items.json
│   └── users.json
├── models/
│   ├── user.js
│   └── item.js
├── node_modules
├── package-lock.json
├── package.json
├── public/
│   ├── images/
│   └── js/
├── routes.js
├── server.js
└── views/
    ├── components/
    │        ├── nav.ejs
    │        └── footer.ejs
    ├── index.ejs
    ├── item/
    │       ├── index.ejs
    │       └── show.ejs
    ├── layouts/
    │       └── default.ejs
    ├── login/
    │       └── index.ejs
    ├── profile/
    │       └── index.ejs
    └── user/
           └── index.ejs

レイアウトファイル作成

レイアウトファイル「views/layouts/default.ejs」を作成し、HTML の共通部分を記述します。

views/layouts/default.ejs
<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
  <title>MyPage</title>
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
</head>

<body>
  <div class="container">
    <%- body %>
  </div>
</body>

</html>

bodyタグ

レイアウトファイルで body タグ中身を読み込むには、<%- body %> を利用します。

<%- body %>

メインコンテンツの表示

トップページのビューは、レイアウト部分を削除します。

トップページ

views/index.ejs
<p>
  トップページです
</p>

レイアウトが利用できるか確認してみましょう。

コンポーネント

コンポーネントは、同じような処理を部品化したファイルです。コンポーネントを作成すると各ページから再利用できるため、開発効率が上がります。

include()

include() を利用すると、外部コンポーネントファイルを読み込むことができます。

<%- include(ファイルパス) %>

ナビゲーション

ナビゲーションファイル「views/components/nav.ejs」を作成します。

views/components/nav.ejs
<nav class="navbar navbar-expand-lg navbar-light bg-light">
  <div class="container-fluid">
    <a class="navbar-brand" href="/">MyShop</a>
    <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent"
      aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
      <span class="navbar-toggler-icon"></span>
    </button>

    <div class="collapse navbar-collapse" id="navbarSupportedContent">
      <ul class="navbar-nav mr-auto">
        <li class="nav-item">
          <a class="nav-link" href="/">Home</a>
        </li>
        <li class="nav-item">
          <a class="nav-link" href="/profile">Profile</a>
        </li>
        <li class="nav-item">
          <a class="nav-link" href="/item">Item</a>
        </li>
        <li class="nav-item">
          <a class="nav-link" href="/login">Login</a>
        </li>
    </div>
  </div>
</nav>

フッター

「views/components/footer.ejs」を作成します。

views/components/footer.ejs
<footer class="footer">
  <div class="container-fluid">
    <p class="text-muted text-end"><small>©2021 myshop inc.</small></p>
  </div>
</footer>

ヘッダー・フッター読み込み

レイアウトからヘッダーとフッターを読み込みます。

views/layouts/default.ejs
<body>
  <%- include('../components/nav') %>
  
  <div class="container">
    <%- body %>
  </div>

  <%- include('../components/footer') %>
</body>

レイアウトとコンポーネントの確認

作成したレイアウト、コンポーネントが表示されるか、各ページで確認してみましょう。

レイアウト任意指定

render() でレイアウトを任意指定することもできます。 「layouts/login.ejs」レイアウトを指定した場合です。

routes.js
router.get("/login", (req, res) => {
    var data = {
        title: 'ログイン',
        layout: 'layouts/login'
    }
    res.render('login/index.ejs', data)
})
views/layouts/login.ejs
<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
  <title>MyShop</title>
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
</head>

<body>
  <div class="container">
    <%- body %>
  </div>

  <%- include('../components/footer') %>
</body>

</html>
views/login/index.ejs
  <form action="/auth" method="post">
    <div>
      <label>ログイン名</label>
      <input type="text" name="login_name">
    </div>

    <div>
      <label>パスワード</label>
      <input type="password" name="password">
    </div>

    <div>
      <button>Loigin</button>
    </div>
  </form>

ソース

サーバー

server.js
const express = require('express');
const routes = require('./routes');

const dotenv = require('dotenv');
dotenv.config();
const host = process.env.HOST;
const port = process.env.PORT;

const app = express();

//レイアウト
const layouts = require('express-ejs-layouts');
app.set('layout', 'layouts/default');
app.set('view engine', 'ejs');
app.use(layouts);

app.use(routes);

app.listen(port, host, () => {
    console.log(`Server listen: http://${host}:${port}`);
})

レイアウト

views/layouts/default.ejs
<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
  <title>MyShop</title>
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
</head>

<body>
  <%- include('../components/nav') %>
  
  <div class="container">
    <h1 class="h1 fs-2 pt-3"><%=title %></h1>

    <%- body %>
  </div>

  <%- include('../components/footer') %>
</body>

</html>
views/layouts/login.ejs
<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
  <title>MyShop</title>
</head>

<body>
  <div class="container">
    <%- body %>
  </div>

  <%- include('../components/footer') %>
</body>

</html>

ページコンテンツ

views/index.ejs
<p>
  トップページです
</p>

コンポーネント

views/components/nav.ejs
<nav class="navbar navbar-expand-lg navbar-light bg-light">
  <div class="container-fluid">
    <a class="navbar-brand" href="/">MyShop</a>
    <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent"
      aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
      <span class="navbar-toggler-icon"></span>
    </button>

    <div class="collapse navbar-collapse" id="navbarSupportedContent">
      <ul class="navbar-nav mr-auto">
        <li class="nav-item">
          <a class="nav-link" href="/">Home</a>
        </li>
        <li class="nav-item">
          <a class="nav-link" href="/profile">Profile</a>
        </li>
        <li class="nav-item">
          <a class="nav-link" href="/item">Item</a>
        </li>
        <li class="nav-item">
          <a class="nav-link" href="/login">Login</a>
        </li>
    </div>
  </div>
</nav>
views/components/footer.ejs
<footer class="footer">
  <div class="container-fluid">
    <p class="text-muted text-end"><small>©2021 myshop inc.</small></p>
  </div>
</footer>

演習

問題1

プロフィールページをレイアウトに対応させてみましょう。

プロフィールページ

views/profile/index.ejs
<div>
  <dl>
    <dt>名前</dt>
    <dd><%= user.name %></dd>
    <dt>出身地</dt>
    <dd><%= user.birthplace %></dd>
    <dt>趣味</dt>
    <dd>
      <% user.hobby.forEach((hobby) => { %>
      <%= hobby %>
      <% }) %>
    </dd>
  </dl>
</div>

問題2

商品一覧、詳細ページをレイアウトに対応させてみましょう。

商品一覧ページ

views/item/index.ejs
<dl>
  <% items.forEach ((item) => { %>
  <dd><a href="/item/<%= item.id %>"><%= item.name %></a></dd>
  <dd><%= item.price %>円</dd>
  <% }) %>
</dl>

商品詳細ページ

views/item/show.ejs
<div>
  <% if (item) { %>
  <dl>
    <dt>商品名</dt>
    <dd><%= item.name %></dd>
    <dt>価格</dt>
    <dd><%= item.price %>円</dd>
  </dl>
  <% } else { %>
  <div><%= message %></div>
  <% } %>
</div>