コンテンツにスキップ

テンプレートエンジン

出典: フリー教科書『ウィキブックス(Wikibooks)』

テンプレートエンジンは、Webアプリケーション開発における重要な基盤技術です。HTMLCSSの生成を効率化し、コードの再利用性と保守性を高めることができます。本ハンドブックでは、主要なテンプレートエンジンの特徴と実践的な使用方法について解説します。

HTMLテンプレートエンジン

[編集]

Pug(旧Jade)

[編集]

Pugは、インデントベースの文法を採用し、HTMLの冗長性を排除することで、より読みやすく保守しやすいコードの記述を実現します。特にNode.js環境において広く採用されています。

基本的な構文例を見てみましょう:

doctype html
html(lang="ja")
  head
    meta(charset="UTF-8")
    title マイページ
    link(rel="stylesheet" href="/styles/main.css")
  body
    header.main-header
      nav
        ul
          li: a(href="/") ホーム
          li: a(href="/about") 概要
          li: a(href="/contact") お問い合わせ
    
    main.content
      h1 ようこそ#{username}さん
      if messages.length
        .message-list
          each message in messages
            article.message
              h2= message.title
              p= message.content
              time(datetime=message.created)= message.formattedDate
      else
        p メッセージはありません

Pugの強力な機能の一つが継承機能です。以下のように基本レイアウトを定義し、各ページで拡張して使用できます:

layout.pug
doctype html
html
  head
    block head
      title デフォルトタイトル
  body
    block header
      include includes/header
    
    block content
    
    block footer
      include includes/footer
    
    block scripts
page.pug
extends layout

block head
  title マイページ - サイト名
  
block content
  main
    h1 マイページ
    p ここにコンテンツが入ります
    
block scripts
  script(src="/js/main.js")

また、再利用可能なコンポーネントをミックスインとして定義できます:

mixin userCard(user)
  .user-card
    img.avatar(src=user.avatar alt=user.name)
    .user-info
      h3= user.name
      p.title= user.title
      if user.isOnline
        span.status.online オンライン
      else
        span.status.offline オフライン

// 使用例
+userCard({
  name: '山田太郎',
  title: 'シニアエンジニア',
  avatar: '/images/yamada.jpg',
  isOnline: true
})

EJS

[編集]

EJSは、JavaScriptの式を直接HTMLに埋め込めるテンプレートエンジンです。学習曲線が緩やかで、既存のJavaScript知識をそのまま活用できる点が特徴です。

基本的な使用例を見てみましょう:

<!DOCTYPE html>
<html>
<head>
  <title><%= title %></title>
  <link rel="stylesheet" href="/css/style.css">
</head>
<body>
  <header>
    <% if (user) { %>
      <nav>
        <a href="/dashboard">ダッシュボード</a>
        <a href="/profile">プロフィール</a>
        <a href="/logout">ログアウト</a>
      </nav>
    <% } else { %>
      <nav>
        <a href="/login">ログイン</a>
        <a href="/register">新規登録</a>
      </nav>
    <% } %>
  </header>

  <main>
    <h1><%= pageTitle %></h1>
    
    <%- include('partials/messageList', { messages: messages }) %>
  </main>

  <%- include('partials/footer') %>
</body>
</html>

部分テンプレート(パーシャル)の例:

<!-- partials/messageList.ejs -->
<% if (messages && messages.length) { %>
  <div class="message-list">
    <% messages.forEach(function(message) { %>
      <article class="message <%= message.type %>">
        <header>
          <h2><%= message.title %></h2>
          <time datetime="<%= message.created %>">
            <%= message.formattedDate %>
          </time>
        </header>
        <div class="content">
          <%= message.content %>
        </div>
        <% if (message.attachments && message.attachments.length) { %>
          <footer>
            <h3>添付ファイル</h3>
            <ul class="attachments">
              <% message.attachments.forEach(function(file) { %>
                <li>
                  <a href="<%= file.url %>">
                    <%= file.name %> (<%= file.size %>)
                  </a>
                </li>
              <% }); %>
            </ul>
          </footer>
        <% } %>
      </article>
    <% }); %>
  </div>
<% } else { %>
  <p class="no-messages">メッセージはありません</p>
<% } %>

Handlebars (HBS)

[編集]

Handlebarsは、ロジックレスなテンプレートエンジンとして知られています。テンプレート内のロジックを最小限に抑え、プレゼンテーション層とビジネスロジックの分離を促進します。

基本的な使用例:

<!DOCTYPE html>
<html>
<head>
    <title>{{title}}</title>
</head>
<body>
    <header>
        {{> header}}
    </header>

    <main>
        <h1>{{pageTitle}}</h1>
        
        {{#if user}}
            <div class="profile">
                <h2>プロフィール</h2>
                {{#with user}}
                    <p>名前: {{firstName}} {{lastName}}</p>
                    <p>メール: {{email}}</p>
                    {{#if bio}}
                        <p>自己紹介: {{bio}}</p>
                    {{/if}}
                {{/with}}
            </div>
        {{/if}}

        {{#if posts.length}}
            <section class="posts">
                {{#each posts}}
                    {{> postCard}}
                {{/each}}
            </section>
        {{else}}
            <p>投稿がありません</p>
        {{/if}}
    </main>

    {{> footer}}
</body>
</html>

カスタムヘルパーの定義と使用:

// ヘルパーの登録
Handlebars.registerHelper('formatDate', function(date) {
    return new Date(date).toLocaleDateString('ja-JP');
});

Handlebars.registerHelper('truncate', function(text, length) {
    if (text.length > length) {
        return text.substring(0, length) + '...';
    }
    return text;
});
postCard.hbs
{{!-- postCard.hbs --}}
<article class="post-card">
    <header>
        <h2>{{title}}</h2>
        <time datetime="{{createdAt}}">{{formatDate createdAt}}</time>
    </header>
    <div class="content">
        {{truncate content 200}}
    </div>
    <footer>
        <div class="meta">
            <span class="author">作成者: {{author.name}}</span>
            <span class="comments">コメント: {{comments.length}}</span>
        </div>
        <a href="/posts/{{id}}" class="read-more">続きを読む</a>
    </footer>
</article>

CSSプリプロセッサ

[編集]

Sass/SCSS

[編集]

Sassは最も成熟したCSSプリプロセッサの一つです。SCSSシンタックスは、標準のCSSと完全な互換性を持ちながら、強力な機能を提供します。

変数とネスティングを活用した基本的な例:

// variables.scss
$primary-color: #007bff;
$secondary-color: #6c757d;
$spacing-unit: 8px;
$border-radius: 4px;
$transition-base: all 0.2s ease-in-out;

// コンポーネントの定義
.card {
  background: white;
  border-radius: $border-radius;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  transition: $transition-base;
  
  &:hover {
    transform: translateY(-2px);
    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
  }

  .card-header {
    padding: $spacing-unit * 2;
    border-bottom: 1px solid rgba(0, 0, 0, 0.1);
    
    h2 {
      margin: 0;
      color: $primary-color;
    }
  }

  .card-body {
    padding: $spacing-unit * 3;
  }
}

ミックスインとエクステンドの活用例:

// mixins.scss
@mixin flex-container($direction: row, $justify: center, $align: center) {
  display: flex;
  flex-direction: $direction;
  justify-content: $justify;
  align-items: $align;
}

@mixin responsive-text($min-size, $max-size, $min-width: 320px, $max-width: 1200px) {
  font-size: clamp(#{$min-size}, #{($max-size - $min-size) / ($max-width - $min-width)} * 100vw, #{$max-size});
}

// プレースホルダーセレクタ
%button-base {
  padding: $spacing-unit $spacing-unit * 2;
  border: none;
  border-radius: $border-radius;
  cursor: pointer;
  transition: $transition-base;
}

// 実装例
.container {
  @include flex-container(column, flex-start, stretch);
  max-width: 1200px;
  margin: 0 auto;
}

.heading {
  @include responsive-text(24px, 48px);
  color: $primary-color;
}

.button {
  @extend %button-base;
  
  &.primary {
    background-color: $primary-color;
    color: white;
    
    &:hover {
      background-color: darken($primary-color, 10%);
    }
  }
  
  &.secondary {
    background-color: $secondary-color;
    color: white;
    
    &:hover {
      background-color: darken($secondary-color, 10%);
    }
  }
}

Stylus

[編集]

Stylusは、より簡潔な文法を提供し、オプションの括弧やセミコロンを特徴とします。

基本的な使用例:

// variables
primary-color = #007bff
secondary-color = #6c757d
spacing = 8px

// mixins
flex-center()
  display flex
  align-items center
  justify-content center

button(bg-color)
  background-color bg-color
  color white
  padding spacing * 2
  border none
  border-radius 4px
  cursor pointer
  transition all 0.2s ease
  
  &:hover
    background-color darken(bg-color, 10%)

// implementation
.header
  flex-center()
  height 60px
  background-color #f8f9fa
  
  .nav-item
    margin 0 spacing
    
    a
      color primary-color
      text-decoration none
      
      &:hover
        color darken(primary-color, 20%)

.button
  &.primary
    button(primary-color)
  
  &.secondary
    button(secondary-color)

Less

[編集]

Lessは、CSSに近い文法を持ちながら、変数やミックスインなどの機能を提供します。

// Variables
@primary-color: #007bff;
@secondary-color: #6c757d;
@spacing: 8px;
@border-radius: 4px;

// Mixins
.gradient-background(@start-color, @end-color) {
  background: @start-color;
  background: linear-gradient(180deg, @start-color 0%, @end-color 100%);
}

.transition(@property: all, @duration: 0.2s, @timing: ease) {
  transition: @arguments;
}

// Components
.navbar {
  .gradient-background(#ffffff, #f8f9fa);
  padding: @spacing * 2;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);

  &-brand {
    color: @primary-color;
    font-size: 1.5em;
    .transition(color);

    &:hover {
      color: darken(@primary-color, 10%);
    }
  }

  &-nav {
    display: flex;
    gap: @spacing * 2;

    .nav-item {
      a {
        color: @secondary-color;
        text-decoration: none;
        .transition(color);

        &:hover {
          color: darken(@secondary-color, 15%);
        }
      }
    }
  }
}

モダンなテンプレートエンジン

[編集]

Nunjucks

[編集]

Nunjucksは、Mozillaが開発したPythonJinja2に影響を受けたテンプレートエンジンです。

layout.njk
{# layout.njk #}
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>{{ title }} | サイト名</title>
    {% block styles %}
    <link rel="stylesheet" href="/css/main.css">
    {% endblock %}
</head>
<body>
    {% include "partials/header.njk" %}
    
    <main>
        {% block content %}{% endblock %}
    </main>
    
    {% include "partials/footer.njk" %}
    
    {% block scripts %}
    <script src="/js/main.js"></script>
    {% endblock %}
</body>
</html>
index.njk
{# index.njk #}
{% extends "layout.njk" %}

{% block content %}
<div class="container">
    <h1>{{ pageTitle }}</h1>
    
    {% if posts.length %}
        <div class="post-grid">
        {% for post in posts %}
            {% include "partials/post-card.njk" %}
        {% endfor %}
        </div>
    {% else %}
        <p>投稿がありません。</p>
    {% endif %}
    
    {% if pagination.pages > 1 %}
        <nav class="pagination">
            {% for pageNum in range(1, pagination.pages + 1) %}
                <a href="?page={{ pageNum }}"
                   class="page-link {{ 'active' if pageNum == pagination.current }}">
                    {{ pageNum }}
                </a>
            {% endfor %}
        </nav>
    {% endif %}
</div>
{% endblock %}

マクロの使用例:

macros/forms.njk
{# macros/forms.njk #}
{% macro input(name, label, type="text", value="", required=false) %}
<div class="form-group">
    <label for="{{ name }}">{{ label }}</label>
    <input type="{{ type }}" 
           id="{{ name }}"
           name="{{ name }}"
           value="{{ value }}"
           {% if required %}required{% endif %}
           class="form-control">
</div>
{% endmacro %}

{% macro select(name, label, options, selected="") %}
<div class="form-group">
    <label for="{{ name }}">{{ label }}</label>
    <select id="{{ name }}" name="{{ name }}" class="form-select">
        {% for option in options %}
            <option value="{{ option.value }}"
                    {{ 'selected' if option.value == selected }}>
                {{ option.label }}
            </option>
        {% endfor %}
    </select>
</div>
{% endmacro %}

{# usage #}
{% import "macros/forms.njk" as forms %}

<form method="post" action="/register">
    {{ forms.input('username', 'ユーザー名', required=true) }}
    {{ forms.input('email', 'メールアドレス', type='email', required=true) }}
    {{ forms.input('password', 'パスワード', type='password', required=true) }}
    {{ forms.select('role', '権限', [
        {value: 'user', label: '一般ユーザー'},
        {value: 'admin', label: '管理者'}
    ]) }}
    <button type="submit">登録</button>
</form>

Twig

[編集]

Twigは、PHPのテンプレートエンジンとして広く使用されており、特にSymfonyフレームワークのデフォルトテンプレートエンジンとして知られています。

基本的な構文例:

layout.twig
{# layout.twig #}
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>{% block title %}デフォルトタイトル{% endblock %}</title>
    {% block stylesheets %}
        <link href="{{ asset('css/main.css') }}" rel="stylesheet">
    {% endblock %}
</head>
<body>
    {% include 'partials/header.twig' with {'menu_items': main_menu} %}

    <div class="container">
        {% block content %}{% endblock %}
    </div>

    {{ include('partials/footer.twig') }}

    {% block javascripts %}
        <script src="{{ asset('js/main.js') }}"></script>
    {% endblock %}
</body>
</html>

マクロとコンポーネントの実装例:

components/forms.twig
{# components/forms.twig #}
{% macro form_row(label, name, type = 'text', value = '', attributes = {}) %}
    <div class="form-group">
        <label for="{{ name }}">{{ label }}</label>
        <input type="{{ type }}"
               id="{{ name }}"
               name="{{ name }}"
               value="{{ value }}"
               {% for attr, val in attributes %}
                   {{ attr }}="{{ val }}"
               {% endfor %}
               class="form-control">
    </div>
{% endmacro %}

{% macro alert(message, type = 'info') %}
    <div class="alert alert-{{ type }}" role="alert">
        {% if type == 'error' %}
            <i class="fas fa-exclamation-triangle"></i>
        {% endif %}
        {{ message }}
        {% if type != 'error' %}
            <button type="button" class="close" data-dismiss="alert">
                <span>&times;</span>
            </button>
        {% endif %}
    </div>
{% endmacro %}

{# usage.twig #}
{% import "components/forms.twig" as forms %}

<form action="/register" method="post">
    {{ forms.form_row('ユーザー名', 'username', 'text', '', {
        'required': 'required',
        'minlength': '3',
        'maxlength': '20'
    }) }}

    {{ forms.form_row('メールアドレス', 'email', 'email', '', {
        'required': 'required'
    }) }}

    {% if errors %}
        {{ forms.alert(errors.join('\n'), 'error') }}
    {% endif %}
</form>

Liquid

[編集]

Liquidは、Shopifyで開発され、Jekyllなどの静的サイトジェネレータでも使用される安全なテンプレート言語です。

layout.liquid
{% comment %}layout.liquid{% endcomment %}
<!DOCTYPE html>
<html lang="{{ page.language | default: 'en' }}">
<head>
    <meta charset="UTF-8">
    <title>
        {{ page.title | default: site.title }} | {{ site.name }}
    </title>
    {% if page.description %}
        <meta name="description" content="{{ page.description | escape }}">
    {% endif %}
    <link rel="stylesheet" href="{{ 'css/main.css' | asset_url }}">
</head>
<body class="{{ page.layout }}">
    {% include 'header' %}
    
    <main>
        {{ content }}
    </main>
    
    {% include 'footer' %}
    
    {% if page.custom_js %}
        {% for js in page.custom_js %}
            <script src="{{ js | asset_url }}"></script>
        {% endfor %}
    {% endif %}
</body>
</html>

製品リスト表示の実装例:

products.liquid
{% comment %}products.liquid{% endcomment %}
{% assign sorted_products = collection.products | sort: 'price' %}

<div class="product-grid">
    {% paginate sorted_products by 12 %}
        {% for product in sorted_products %}
            <div class="product-card">
                {% if product.featured_image %}
                    <img src="{{ product.featured_image | img_url: '300x300', crop: 'center' }}"
                         alt="{{ product.title | escape }}"
                         loading="lazy">
                {% endif %}
                
                <h3>{{ product.title }}</h3>
                
                <div class="price">
                    {% if product.compare_at_price > product.price %}
                        <span class="sale-price">
                            {{ product.price | money }}
                        </span>
                        <s class="compare-price">
                            {{ product.compare_at_price | money }}
                        </s>
                    {% else %}
                        <span class="regular-price">
                            {{ product.price | money }}
                        </span>
                    {% endif %}
                </div>
                
                {% if product.available %}
                    <form method="post" action="/cart/add">
                        <input type="hidden" name="id" value="{{ product.variants.first.id }}">
                        <button type="submit" class="add-to-cart">
                            カートに追加
                        </button>
                    </form>
                {% else %}
                    <button disabled class="sold-out">
                        売り切れ
                    </button>
                {% endif %}
            </div>
        {% endfor %}
        
        {% if paginate.pages > 1 %}
            <nav class="pagination">
                {{ paginate | default_pagination }}
            </nav>
        {% endif %}
    {% endpaginate %}
</div>

パフォーマンス最適化

[編集]

テンプレートエンジンを効率的に使用するためのベストプラクティス:

キャッシュの活用
// Express + EJSでのキャッシュ設定
app.set('view cache', true);

// カスタムキャッシュの実装
const cache = new Map();

function renderWithCache(template, data, cacheKey) {
    if (cache.has(cacheKey)) {
        return cache.get(cacheKey);
    }
    
    const rendered = ejs.render(template, data);
    cache.set(cacheKey, rendered);
    return rendered;
}
部分的なレンダリング
// パーシャルの効率的な使用
const headerHtml = await renderWithCache('header', headerData, 'header');
const footerHtml = await renderWithCache('footer', footerData, 'footer');
const contentHtml = await render('content', pageData);

const fullPage = `
    ${headerHtml}
    ${contentHtml}
    ${footerHtml}
`;
非同期レンダリング
// [[Node.js]]での非同期レンダリング
async function renderTemplate(template, data) {
    return new Promise((resolve, reject) => {
        ejs.renderFile(template, data, {}, (err, str) => {
            if (err) reject(err);
            else resolve(str);
        });
    });
}

// 複数テンプレートの並列レンダリング
const [header, content, footer] = await Promise.all([
    renderTemplate('header.ejs', headerData),
    renderTemplate('content.ejs', contentData),
    renderTemplate('footer.ejs', footerData)
]);

テンプレートエンジンの選定基準

[編集]

テンプレートエンジンを選択する際の主要な考慮点:

プロジェクトの要件
  • 静的/動的コンテンツの比率
  • リアルタイム更新の必要性
  • SEO要件
  • パフォーマンス要件
// 動的コンテンツが多い場合の例(React + EJS)
function DynamicTemplate({ initialData }) {
    const [data, setData] = useState(initialData);
    
    useEffect(() => {
        const socket = new WebSocket('ws://api.example.com');
        socket.onmessage = (event) => {
            setData(JSON.parse(event.data));
        };
        return () => socket.close();
    }, []);
    
    return <div dangerouslySetInnerHTML={{ __html: 
        ejs.render(template, data)
    }} />;
}
開発チームのスキルセット
  • 既存の技術スタックとの整合性
  • 学習曲線
  • ドキュメントの充実度
メンテナンス性
  • コードの可読性
  • デバッグのしやすさ
  • エラーハンドリング
// デバッグしやすい実装例
const debugTemplate = (template, data) => {
    try {
        return ejs.render(template, data);
    } catch (error) {
        console.error('Template rendering failed:', {
            template: template.slice(0, 100) + '...',
            data: JSON.stringify(data, null, 2),
            error: error.message
        });
        throw error;
    }
};

セキュリティ対策

[編集]

テンプレートエンジンを使用する際は、適切なセキュリティ対策が不可欠です。

XSS対策

[編集]

テンプレートエンジンでの一般的なXSS対策実装例:

// EJSでのエスケープ処理
const escapeHtml = str => {
  const htmlEscapes = {
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#39;',
  };
  return str.replace(/[&<>"']/g, match => htmlEscapes[match]);
};
// カスタムフィルターの実装
app.locals.safeHtml = (unsafe) => {
    // 許可するタグのホワイトリスト
    const allowedTags = {
        'b': [],
        'i': [],
        'em': [],
        'strong': [],
        'a': ['href', 'title'],
        'p': []
    };
    
    // DOMPurifyなどのライブラリを使用した安全な HTML サニタイズ
    return DOMPurify.sanitize(unsafe, {
        ALLOWED_TAGS: Object.keys(allowedTags),
        ALLOWED_ATTR: ['href', 'title']
    });
};

// テンプレートでの使用例
app.get('/article/:id', async (req, res) => {
    const article = await getArticle(req.params.id);
    res.render('article', {
        title: escapeHtml(article.title),
        content: req.app.locals.safeHtml(article.content)
    });
});

CSRF対策

[編集]

CSRFトークンの実装と検証:

// CSRFトークンの生成
const generateCsrfToken = () => {
    return crypto.randomBytes(32).toString('hex');
};

// CSRFミドルウェア
const csrfProtection = (req, res, next) => {
    if (req.method === 'GET') {
        // GETリクエストの場合、新しいトークンを生成
        const token = generateCsrfToken();
        res.locals.csrfToken = token;
        res.cookie('_csrf', token, { 
            httpOnly: true,
            secure: process.env.NODE_ENV === 'production'
        });
    } else {
        // POST/PUT/DELETEリクエストの場合、トークンを検証
        const cookieToken = req.cookies._csrf;
        const bodyToken = req.body._csrf;
        
        if (!cookieToken || !bodyToken || cookieToken !== bodyToken) {
            return res.status(403).json({ error: 'CSRF token validation failed' });
        }
    }
    next();
};

// テンプレートでの使用例
<form method="post" action="/submit">
    <input type="hidden" name="_csrf" value="{{ csrfToken }}">
    <!-- フォームフィールド -->
</form>

入力検証

[編集]

テンプレートエンジンでの入力検証の実装:

// バリデーションルールの定義
const validationRules = {
    username: {
        required: true,
        minLength: 3,
        maxLength: 20,
        pattern: /^[a-zA-Z0-9_]+$/
    },
    email: {
        required: true,
        pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/
    },
    age: {
        required: true,
        min: 18,
        max: 120
    }
};

// バリデーション関数
const validateInput = (data, rules) => {
    const errors = {};
    
    for (const [field, rule] of Object.entries(rules)) {
        const value = data[field];
        
        if (rule.required && !value) {
            errors[field] = `${field} is required`;
            continue;
        }
        
        if (value) {
            if (rule.minLength && value.length < rule.minLength) {
                errors[field] = `${field} must be at least ${rule.minLength} characters`;
            }
            
            if (rule.maxLength && value.length > rule.maxLength) {
                errors[field] = `${field} must be no more than ${rule.maxLength} characters`;
            }
            
            if (rule.pattern && !rule.pattern.test(value)) {
                errors[field] = `${field} format is invalid`;
            }
            
            if (rule.min && Number(value) < rule.min) {
                errors[field] = `${field} must be at least ${rule.min}`;
            }
            
            if (rule.max && Number(value) > rule.max) {
                errors[field] = `${field} must be no more than ${rule.max}`;
            }
        }
    }
    
    return errors;
};

デバッグとトラブルシューティング

[編集]

デバッグモードの実装

[編集]
const debugMiddleware = (req, res, next) => {
    if (process.env.NODE_ENV === 'development') {
        // デバッグ情報の収集
        res.locals.debug = {
            startTime: Date.now(),
            queries: [],
            templates: [],
            logs: []
        };
        
        // SQLクエリのログ
        const logQuery = (query, params, duration) => {
            res.locals.debug.queries.push({
                query,
                params,
                duration,
                timestamp: Date.now()
            });
        };
        
        // テンプレートのログ
        const logTemplate = (template, data) => {
            res.locals.debug.templates.push({
                template,
                data: JSON.stringify(data, null, 2),
                timestamp: Date.now()
            });
        };
        
        // デバッグ情報をレスポンスヘッダーに追加
        res.on('finish', () => {
            const duration = Date.now() - res.locals.debug.startTime;
            console.log(`Request completed in ${duration}ms`);
            console.log('Queries:', res.locals.debug.queries.length);
            console.log('Templates:', res.locals.debug.templates.length);
        });
    }
    next();
};

エラーハンドリング

[編集]
// カスタムエラーハンドラー
class TemplateError extends Error {
    constructor(message, template, data) {
        super(message);
        this.name = 'TemplateError';
        this.template = template;
        this.data = data;
        Error.captureStackTrace(this, TemplateError);
    }
}

// エラーハンドリングミドルウェア
const errorHandler = (err, req, res, next) => {
    if (err instanceof TemplateError) {
        console.error('Template Error:', {
            message: err.message,
            template: err.template,
            data: err.data,
            stack: err.stack
        });
        
        if (process.env.NODE_ENV === 'development') {
            res.status(500).render('error', {
                error: err,
                stack: err.stack,
                template: err.template,
                data: err.data
            });
        } else {
            res.status(500).render('error', {
                message: 'An error occurred while processing your request.'
            });
        }
    } else {
        next(err);
    }
};

パフォーマンスモニタリング

[編集]
const performanceMiddleware = (req, res, next) => {
    const metrics = {
        startTime: process.hrtime(),
        templateRenderTime: 0,
        queryTime: 0,
        totalQueries: 0
    };
    
    // テンプレートレンダリング時間の計測
    const originalRender = res.render;
    res.render = function(view, options, callback) {
        const renderStart = process.hrtime();
        
        const renderCallback = (err, html) => {
            if (!err) {
                const renderDuration = process.hrtime(renderStart);
                metrics.templateRenderTime = renderDuration[0] * 1e3 + renderDuration[1] / 1e6;
            }
            if (callback) callback(err, html);
        };
        
        return originalRender.call(this, view, options, renderCallback);
    };
    
    // レスポンス完了時のメトリクス記録
    res.on('finish', () => {
        const duration = process.hrtime(metrics.startTime);
        const totalTime = duration[0] * 1e3 + duration[1] / 1e6;
        
        console.log({
            path: req.path,
            method: req.method,
            totalTime: `${totalTime.toFixed(2)}ms`,
            templateRenderTime: `${metrics.templateRenderTime.toFixed(2)}ms`,
            queryTime: `${metrics.queryTime.toFixed(2)}ms`,
            totalQueries: metrics.totalQueries
        });
    });
    
    next();
};

テンプレートエンジンの将来展望

[編集]

新しいトレンド

[編集]
Hybrid Rendering
  • サーバーサイドとクライアントサイドのレンダリングを組み合わせた手法
  • 初期表示の高速化とインタラクティブ性の両立
TypeScriptサポート
  • 型安全なテンプレートエンジン
  • コンパイル時のエラー検出
マイクロフロントエンド対応
  • 複数のテンプレートエンジンの共存
  • モジュール化されたテンプレート管理

これらのトレンドに対応した実装例:

// TypeScriptを活用したテンプレートエンジンの型定義
interface TemplateContext {
    user?: {
        id: string;
        name: string;
        email: string;
    };
    content: {
        title: string;
        body: string;
        tags: string[];
    };
    settings: {
        theme: 'light' | 'dark';
        language: string;
    };
}

// ハイブリッドレンダリングの実装例
class HybridTemplate {
    private static readonly clientPrefix = 'data-client-render';
    
    static async render(template: string, context: TemplateContext): Promise<string> {
        // サーバーサイドレンダリング
        const ssrContent = await this.serverRender(template, context);
        
        // クライアントサイド用のデータ埋め込み
        return `
            ${ssrContent}
            <script>
                window.__INITIAL_STATE__ = ${JSON.stringify(context)};
            </script>
            <script src="/js/client-hydration.js"></script>
        `;
    }
    
    private static async serverRender(template: string, context: TemplateContext): Promise<string> {
        // テンプレートのサーバーサイドレンダリング
        return ''; // 実装省略
    }
}

これらの新しいアプローチにより、テンプレートエンジンはより強力で柔軟なツールとして進化を続けています。開発者は、プロジェクトの要件に応じて適切なアプローチを選択し、最新のトレンドを取り入れることで、より効率的な開発を実現できます。