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>