Skip to main content

Як використовувати шаблонні движки в Express.js?

Шаблонні движки (template engines) в Express.js генерують HTML на сервері, об'єднуючи файл шаблону з об'єктом даних. Замість res.send('<h1>' + name + '</h1>') ти тримаєш розмітку в .ejs або .pug файлах і викликаєш res.render('view', data) з обробника маршруту.

Теорія

TL;DR

  • Шаблон = файл з плейсхолдерами; res.render('view', data) заповнює їх і повертає HTML браузеру
  • Аналогія: як гра Mad Libs, шаблон - це текст з пропусками, а дані - слова для них
  • EJS найпоширеніший стартовий варіант, бо це просто HTML з тегами <% %>
  • Використовуй шаблонний движок коли надсилаєш HTML; для JSON API або React/Vue він не потрібен
  • Express не включає жоден движок, тому спочатку npm install ejs (або pug, handlebars)

Швидке налаштування

bash
npm install ejs
js
// app.js const express = require('express'); const path = require('path'); const app = express(); app.set('view engine', 'ejs'); app.set('views', path.join(__dirname, 'views')); // абсолютний шлях уникає помилок пошуку app.get('/', (req, res) => { res.render('home', { title: 'Привіт', users: ['Alice', 'Bob'] }); }); app.listen(3000);
html
<!-- views/home.ejs --> <h1><%= title %></h1> <ul> <% users.forEach(user => { %> <li><%= user %></li> <% }); %> </ul> <!-- Результат: <h1>Привіт</h1><ul><li>Alice</li><li>Bob</li></ul> -->

Express завантажує .ejs файл, компілює його у функцію Node.js, підставляє дані і надсилає готовий HTML рядок. Скомпільована функція залишається в пам'яті, тому повторні запити не читають диск.

Популярні движки

ДвижокФайлСтильДля чого
EJS.ejsHTML з тегами <% %>Швидкий старт, HTML-розробники
Pug.pugВідступи, без закриваючих тегівЛаконічна розмітка
Handlebars.hbsСинтаксис {{ }} без логікиПростий вивід даних
Nunjucks.njkСинтаксис Jinja2Складні цикли та фільтри

EJS найпопулярніший серед початківців, бо виглядає як звичайний HTML. Pug скорочує шаблонний код. Handlebars тримає шаблони чистими, не дозволяючи писати логіку всередині них. Жоден не є об'єктивно кращим. Вибирай виходячи з того, що вже знає твоя команда.

Теги EJS

ТегЩо робить
<%= value %>Виводить значення з HTML-екрануванням
<%- value %>Виводить сирий HTML без екранування
<% code %>Виконує JavaScript, без виводу
<%- include('partial') %>Вставляє інший файл шаблону

Різниця між <%= %> і <%- %> важлива для безпеки. Дані від користувача завжди йдуть через <%= %>, бо він екранує <, > і &. Тег <%- %> рендерить сирий HTML, що відкриває XSS якщо туди потрапляють недовірені дані.

Коли використовувати

  • Блог або дашборд з даними з бази: шаблонний движок підходить добре
  • API що повертає JSON: пропускай, використовуй res.json()
  • SPA з React або Vue на фронтенді: не потрібен, клієнт сам рендерить
  • Email або HTML звіт на вимогу: шаблонний движок також підходить
  • Швидкий прототип де треба одразу бачити HTML: EJS, нульова крива навчання

Партшали (partials) і глобальні змінні

EJS підтримує партшали через <%- include('header') %>. Це дозволяє повторно використовувати навігацію, футери і каркас сторінки без копіювання HTML між файлами.

Для даних потрібних у кожному шаблоні, наприклад назва застосунку або поточний рік, використовуй app.locals:

js
app.locals.siteName = 'MyApp'; app.locals.year = new Date().getFullYear(); // Тепер <%= siteName %> працює в кожному шаблоні без явної передачі

Для даних конкретного запиту, наприклад авторизованого користувача, використовуй res.locals у middleware:

js
app.use((req, res, next) => { res.locals.currentUser = req.user || null; next(); });

Більшість адмін-панелей що я бачив на Express використовують саме цей патерн: app.locals для статичних налаштувань, res.locals для контексту авторизації, і дані per-render тільки для того що змінюється між маршрутами.

Як Express обробляє рендеринг

res.render('dashboard', data) робить ось що: завантажує views/dashboard.ejs з диска при першому запиті, компілює шаблон у JavaScript функцію, кешує цю функцію в пам'яті, викликає її з об'єднаними locals (твої дані плюс app.locals плюс res.locals) і надсилає отриманий рядок. В NODE_ENV=production кеш завжди увімкнений. В режимі розробки Express перекомпільовує при кожному запиті, тому зміни видно без перезапуску.

Типові помилки

Не встановлено пакет движка. app.set('view engine', 'ejs') лише говорить Express що шукати. Якщо пропустити npm install ejs, отримаєш Cannot find module 'ejs' під час виконання. Express нічого не включає автоматично.

Відносний шлях для views.

js
// Ламається коли процес запускається з іншої директорії app.set('views', './views'); // Завжди використовуй так app.set('views', path.join(__dirname, 'views'));

Використання <%- %> для даних від користувача.

html
<!-- XSS: рендерить <script>alert(1)</script> як справжній HTML --> <li><%- user.name %></li> <!-- Безпечно: виводить &lt;script&gt;alert(1)&lt;/script&gt; як текст --> <li><%= user.name %></li>

Забув передати дані. res.render('users') без другого аргументу означає що шаблон не отримає змінних. Звернення до <%= users.length %> кине Cannot read properties of undefined.

Змішування res.render() і res.json() в одному маршруті. В одному запиті викликається або один, або інший. Виклик обох призводить до Cannot set headers after they are sent.

Де використовується

  • Ghost (блог-платформа) використовує серверні шаблони для сторінок постів
  • Handlebars лежить в основі тем Shopify для рендерингу даних товарів
  • Документація Mozilla використовує Nunjucks для складної логіки фільтрів, аналогічно Python Flask
  • Express + EJS типовий стек для адмін-дашбордів і проектів у буткемпах де важливий SEO та швидке перше завантаження

Питання для поглиблення

Q: Як працює кешування в продакшені?
A: В NODE_ENV=production Express кешує скомпільовані функції шаблонів після першого рендеру. В режимі розробки перекомпільовує щоразу, тому зміни видно одразу. Налаштовувати це вручну не потрібно.

Q: Яка різниця між <% %>, <%= %> і <%- %> в EJS?
A: <% %> виконує JavaScript без виводу. <%= %> виводить значення з HTML-екрануванням. <%- %> виводить сирий HTML без екранування. Для будь-яких даних від користувача або бази завжди використовуй <%= %>.

Q: Чим res.render відрізняється від res.send?
A: res.render компілює файл шаблону з даними і надсилає результат. res.send надсилає безпосередньо той рядок або буфер що ти передаєш. Файли шаблонів не задіяні.

Q: Чи можуть два маршрути рендерити один шаблон без витоку даних між запитами?
A: Так. Express кешує скомпільовану функцію, але кожен виклик виконує її зі свіжими locals. Дані одного запиту не потрапляють в інший. Але те що потрапляє в app.locals є глобальним, тому туди мають йти лише дійсно статичні значення.

Q: Pug чи EJS для продуктивності під великим навантаженням?
A: Після першої компіляції обидва стають JavaScript функціями і показують приблизно однакову швидкість. Початковий парсинг Pug трохи повільніший, але ця різниця зникає після кешування. Не варто обирати движок виходячи з цього критерію.

Приклади

Базова EJS сторінка зі списком користувачів

js
// app.js const express = require('express'); const path = require('path'); const app = express(); app.set('view engine', 'ejs'); app.set('views', path.join(__dirname, 'views')); app.get('/users', (req, res) => { const users = [ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, ]; res.render('users', { title: 'Список користувачів', users }); }); app.listen(3000);
html
<!-- views/users.ejs --> <h1><%= title %></h1> <ul> <% users.forEach(user => { %> <li><a href="/users/<%= user.id %>"><%= user.name %></a></li> <% }); %> </ul> <% if (users.length === 0) { %> <p>Користувачів не знайдено.</p> <% } %>

Дані з маршруту потрапляють у шаблон. forEach рендерить по одному <li> на кожного користувача. Перевірка порожнього масиву тримає сторінку корисною.

Дашборд з партшалами та auth middleware

js
// app.js app.locals.siteName = 'MyApp'; // доступно в кожному шаблоні app.use((req, res, next) => { res.locals.currentUser = req.user || null; next(); }); app.get('/dashboard', async (req, res) => { const users = await db.query('SELECT * FROM users LIMIT 10'); res.render('dashboard', { recentUsers: users.rows, stats: { total: users.rows.length }, }); });
html
<!-- views/partials/header.ejs --> <header> <span><%= siteName %></span> <% if (currentUser) { %> <span>Привіт, <%= currentUser.name %></span> <% } %> </header> <!-- views/dashboard.ejs --> <%- include('partials/header') %> <p>Всього користувачів: <%= stats.total %></p> <table> <% recentUsers.forEach(u => { %> <tr> <td><%= u.name %></td> <td><%= u.email %></td> </tr> <% }); %> </table>

res.locals.currentUser встановлений у middleware доступний у партшалі без явної передачі. app.locals.siteName встановлений один раз при старті з'являється в кожному шаблоні. Саме так реальні Express застосунки уникають повторюваної передачі одних і тих самих даних по всіх маршрутах.

Безпечний і небезпечний вивід

js
// Маршрут що отримує контент від користувача app.get('/comment', (req, res) => { const comment = { author: 'Bob', text: '<script>alert("XSS")</script> Гарна стаття!', }; res.render('comment', { comment }); });
html
<!-- views/comment.ejs --> <strong><%= comment.author %></strong> <!-- Безпечний вивід --> <p><%= comment.text %></p> <!-- Браузер показує: <script>alert("XSS")</script> Гарна стаття! --> <!-- Кутові дужки екрановані, скрипт не виконується --> <!-- Небезпечний вивід --> <p><%- comment.text %></p> <!-- Браузер виконає тег script -->

<%= %> - вибір за замовчуванням для кожного значення що приходить від користувача, форми або бази даних. <%- %> тільки для довіреного контенту, наприклад HTML що ти сам формуєш на сервері.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?