templ¶
HTML-шаблони, які компілюються, а не парсяться. Ви пишете файли
.templ, генератор видає .go, і одруківка в назві поля стає
помилкою компіляції замість порожньої сторінки.
Модуль:
github.com/a-h/templ, плюс CLItempl.
Порівняйте з шаблонами, що
виконує ту саму роботу через html/template.
Ідея¶
html/template розв'язує {{.Name}} під час виконання. Перейменуйте
поле — і дізнаєтесь про це, коли сторінка рендериться — або коли
рендериться саме та гілка, а це може статись у продакшені.
templ генерує Go. i.Name — справжній доступ до поля, тож компілятор
перевіряє його, редактор доповнює, а перейменування через інструмент
рефакторингу оновлює шаблон.
Синтаксис¶
Блок templ — це функція, що повертає компонент:
package main
import "fmt"
templ itemRow(i Item) {
<li class="row">
<span>{ i.Name }</span>
<span>{ fmt.Sprintf("$%.2f", i.Price) }</span>
if len(i.Tags) > 0 {
<em>{ fmt.Sprint(len(i.Tags)) } tags</em>
} else {
<em>untagged</em>
}
</li>
}
Фігурні дужки інтерполюють вираз Go, що повертає рядок. Не мова
шаблонів — справжній Go, зі справжніми імпортами. Ось чому форматування
ціни — це fmt.Sprintf, а не власна функція шаблону.
if, for і switch — ключові слова Go зі синтаксисом Go, тож немає
другого діалекту, який треба вивчати, і немає {{end}}, який можна
забути.
@name(args) рендерить інший компонент.
Композиція через children...¶
Макет бере вміст сторінки як дочірні елементи:
templ layout(title string) {
<html>
<head><title>{ title }</title></head>
<body>
{ children... }
</body>
</html>
}
templ ItemList(title string, items []Item) {
@layout(title) {
<ul>
for _, i := range items {
@itemRow(i)
}
</ul>
}
}
Блок після @layout(title) стає його дочірніми елементами. Це заміняє
define/block з html/template, і компонується як звичайні виклики
функцій.
Генерація¶
Створює page_templ.go поруч із кожним .templ, з заголовком:
Комітьте згенеровані файли. Так репозиторій збирається звичайним
go build, і CI не потребує CLI — та сама аргументація, що й у статті
про збірку та кодогенерацію.
Підключіть templ generate у ваш Makefile, і хай CI регенерує й
провалюється на diff, щоб ніхто не забув.
templ generate --watch регенерує при збереженні під час розробки.
Рендеринг¶
Компонент рендериться в будь-який io.Writer:
Що в обробнику net/http виглядає так:
func (h Handler) list(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := ItemList("Shop", items).Render(r.Context(), w); err != nil {
http.Error(w, "render failed", http.StatusInternalServerError)
}
}
Є нюанс, про який варто знати: Render стрімить, тож частина сторінки
могла бути записана вже до провалу — і статус уже 200. Якщо це має
значення, рендеріть спершу в bytes.Buffer і копіюйте назовні лише
у разі успіху.
Екранування¶
Контекстне, як і в html/template:
Текст екранується, атрибути екрануються, а templ.URL санітизує URL в
href. Небезпечну схему заміняють повністю:
"/x?q=1&b=2" <a href="/x?q=1&b=2">link</a>
"https://example.com/a" <a href="https://example.com/a">link</a>
"mailto:a@b.c" <a href="mailto:a@b.c">link</a>
"javascript:alert(1)" <a href="about:invalid#TemplFailedSanitizationURL">link</a>
"data:text/html,<script>1" <a href="about:invalid#TemplFailedSanitizationURL">link</a>
about:invalid#TemplFailedSanitizationURL — це еквівалент маркера
ZgotmplZ з html/template, але для templ: якщо ви бачите його на
відрендереній сторінці, це означає, що небезпечний вміст потрапив у
позицію URL, і виправляти треба дані, а не шаблон.
templ.Raw відмовляється від екранування й повинен бачити лише той
вміст, який ви самі створили.
Оскільки екранування відбувається на етапі генерації у відомих позиціях, обійти його випадково важче, ніж у шаблоні на основі рядків.
В'юмоделі¶
Не передавайте свої моделі бази даних напряму в шаблон. Окремий тип в'юмоделі тримає логіку відображення подалі від обох:
Обробник мапить домен на в'юмодель, шаблон рендерить в'юмодель. Чисті
функції-мапери легко тестувати, а шаблон залишається вільним від
умов на кшталт if user.Role == "admin" && !user.Suspended.
Тестування¶
Компонент — це функція, що повертає щось із методом Render, тож
тести — звичайний Go:
var sb strings.Builder
err := ItemList("t", items).Render(context.Background(), &sb)
// then assert on sb.String()
Перевіряйте структуру, а не точні байти — golden-файл зі статті про хелпери й golden-файли добре тут працює, оскільки ціла сторінка задовга, щоб вбудовувати її в код.
Ціна¶
- Крок збірки. Редагування HTML тепер означає регенерацію, а застарілий згенерований файл — заплутана помилка.
- Підтримка редактора окрема. Є LSP і плагіни, але це не той
готовий досвід, що з
.html. - Згенерований код шумний у diff. Деякі команди додають його в
gitignore й генерують у CI — за ціну того, що
go buildбільше не працює самостійно. - Це інша мова в тому самому файлі. Дизайнерам, що редагують шаблони, доведеться її вивчити.
Проти цього: перейменоване поле не може мовчки зламати сторінку, і взагалі немає парсингу шаблонів під час виконання.
З досвіду Python: це Jinja, заміщена чимось ближчим до компілятора JSX — шаблони стають типізованими функціями, перевіреними на етапі збірки. Компроміс той самий: типобезпека й підтримка редактора проти кроку компіляції й формату, менш доступного для тих, хто не програміст.
Швидка довідка¶
| Задача | Форма |
|---|---|
| компонент | templ Name(args) { ... } у файлі .templ |
| інтерполяція | { goExpression } — справжній Go, що повертає рядок |
| керування потоком | Go if, for, switch — без {{end}} |
| викликати інший | @other(arg) |
| макети | { children... } плюс @layout(x) { ... } |
| генерувати | templ generate, --watch під час розробки |
| комітити | так — згенеровані файли _templ.go |
| рендерити | .Render(ctx, w) у будь-який io.Writer |
| безпечні URL | templ.URL(...); templ.Raw лише для власного HTML |
| тест | рендерити в strings.Builder, перевіряти або використати golden-файл |