В данной статье мы разберем принцип отображения динамических данных из MySQL базы данных на некоторых HTML страницах нашего веб-приложения на Golang.
Содержание статьи
- Вывод заметок в HTML шаблоне
- Go-структура с данными для передачи в шаблон
- Экранирование данных
- Шаблоны с вложенными шаблонами
- Вызов методов в шаблоне
- HTML комментарии в шаблоне
Вы научитесь:
Рекомендуем вам супер TELEGRAM канал по Golang где собраны все материалы для качественного изучения языка. Удивите всех своими знаниями на собеседовании! 😎
Мы публикуем в паблике ВК и Telegram качественные обучающие материалы для быстрого изучения Go. Подпишитесь на нас в ВК и в Telegram. Поддержите сообщество Go программистов.
- Передавать динамические данные в HTML шаблоны простым, масштабируемым и безопасным способом;
- Использовать различные операторы и функции из пакета
html/template
для управления отображением динамических данных в шаблоне сайта; - Кэшировать шаблон, чтобы не тратить ресурсы на повторную обработку шаблона для каждого HTTP запроса;
- Изящно обрабатывать возникшие ошибки рендеринга шаблонов во время работы;
- Реализуем способ для передачи глобальных динамических данных на веб-страницы без повторения кода в обработчиках;
- Создавать собственные функции для форматирования и отображения данных в HTML шаблонах.
Вывод заметок в HTML шаблоне
На данный момент обработчик showSnippet
извлекает заметку models.Snippet из базы данных, а затем показывает его содержимое в виде обычного текстового HTTP ответа. Сейчас мы обновим текущий HTML шаблона, чтобы наши данные отображались на веб-странице следующим образом:
Начнем с обработчика showSnippet
и добавим код для рендеринга файла шаблона show.page.tmpl
(которого мы создадим через минуту). Следующий код должен показаться знакомым по предыдущим урокам.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
package main import ( "errors" "fmt" "golangify.com/snippetbox/pkg/models" "html/template" "net/http" "strconv" ) func (app *application) showSnippet(w http.ResponseWriter, r *http.Request) { id, err := strconv.Atoi(r.URL.Query().Get("id")) if err != nil || id < 1 { app.notFound(w) return } s, err := app.snippets.Get(id) if err != nil { if errors.Is(err, models.ErrNoRecord) { app.notFound(w) } else { app.serverError(w, err) } return } // Инициализируем срез, содержащий путь к файлу show.page.tmpl // Добавив еще базовый шаблон и часть футера, который мы сделали ранее. files := []string{ "./ui/html/show.page.tmpl", "./ui/html/base.layout.tmpl", "./ui/html/footer.partial.tmpl", } // Парсинг файлов шаблонов... ts, err := template.ParseFiles(files...) if err != nil { app.serverError(w, err) return } // А затем выполняем их. Обратите внимание на передачу заметки с данными // (структура models.Snippet) в качестве последнего параметра. err = ts.Execute(w, s) if err != nil { app.serverError(w, err) } } |
Затем нам нужно создать файл show.page.tmpl
, содержащий HTML разметку для страницы отображения заметок. Но перед этим давайте немного поговорим о теории…
В ваших HTML шаблонах любые передаваемые в него динамические данные представлены символом точки .
в начале.
В данном конкретном случае базовым типом точки будет структура models.Snippet
. Если базовым типом точки является структура, вы можете отобразить (или получить) значение любого поля из неё, добавив точку в начале. Поскольку у структуры models.Snippet
есть поле Title
, можно получить заголовок заметки, указав {{.Title}}
в наших шаблонах.
Создайте новый файл в ui/html/show.page.tmpl
и добавьте следующую разметку:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
{{template "base" .}} {{define "title"}}Заметка #{{.ID}}{{end}} {{define "main"}} <div class='snippet'> <div class='metadata'> <strong>{{.Title}}</strong> <span>#{{.ID}}</span> </div> <pre><code>{{.Content}}</code></pre> <div class='metadata'> <time>Создан: {{.Created}}</time> <time>Срок: {{.Expires}}</time> </div> </div> {{end}} |
Если вы перезапустите веб-приложение и перейдете на страницу http://127.0.0.1:4000/snippet?id=1 в браузере, вы увидите, что нужная заметка была получена из базы данных, передана шаблону, и её содержимое красиво отображено на экране.
1 |
go run ./cmd/web |
Содержимое страницы с заметкой:
Go-структура с данными для передачи в шаблон
Важно понять, что пакет html/template
позволяет передавать только один источник динамических данных при рендеринге шаблона. Однако, в реальном приложении зачастую есть несколько источников данных, которых нужно отобразить на одной странице.
Легкий способ достичь этого — обернуть динамические данные в новую структуру, которая будет действовать как единая «структура хранения» для ваших данных.
Для этого создадим новый файл cmd/web/templates.go
, содержащий структуру templateData
.
1 2 3 4 5 6 7 8 9 10 11 |
package main import "golangify.com/snippetbox/pkg/models" // Создаем тип templateData, который будет действовать как хранилище для // любых динамических данных, которые нужно передать HTML-шаблонам. // На данный момент он содержит только одно поле, но мы добавим в него другие // по мере развития нашего приложения. type templateData struct { Snippet *models.Snippet } |
Затем обновляем обработчик showSnippet
для использования данной структуры при отображении наших шаблонов:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
func (app *application) showSnippet(w http.ResponseWriter, r *http.Request) { id, err := strconv.Atoi(r.URL.Query().Get("id")) if err != nil || id < 1 { app.notFound(w) return } s, err := app.snippets.Get(id) if err != nil { if errors.Is(err, models.ErrNoRecord) { app.notFound(w) } else { app.serverError(w, err) } return } // Создаем экземпляр структуры templateData, содержащей данные заметки. data := &templateData{Snippet: s} files := []string{ "./ui/html/show.page.tmpl", "./ui/html/base.layout.tmpl", "./ui/html/footer.partial.tmpl", } ts, err := template.ParseFiles(files...) if err != nil { app.serverError(w, err) return } // Передаем структуру templateData в качестве данных для шаблона. err = ts.Execute(w, data) if err != nil { app.serverError(w, err) } } |
Теперь данные нашей заметки из структуры models.Snippet
переданы внутри структуры templateData
. Чтобы получить данные из неё, нужно вызвать соответствующие названия полей следующим образом (файл ui/html/show.page.tmpl
):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
{{template "base" .}} {{define "title"}}Snippet #{{.Snippet.ID}}{{end}} {{define "main"}} <div class='snippet'> <div class='metadata'> <strong>{{.Snippet.Title}}</strong> <span>#{{.Snippet.ID}}</span> </div> <pre><code>{{.Snippet.Content}}</code></pre> <div class='metadata'> <time>Created: {{.Snippet.Created}}</time> <time>Expires: {{.Snippet.Expires}}</time> </div> </div> {{end}} |
Попробуйте перезапустить приложение и обновите страницу http://127.0.0.1:4000/snippet?id=1. В браузере вы должны увидеть рендеринг той же страницы, что и раньше.
Защита от XSS-атак в Golang — Экранирование данных
Пакет html/template
автоматически экранирует любые данные, находящиеся между тегами {{}}
. Такое поведение помогает избежать атак с использованием межсайтовых сценариев (XSS) и поэтому мы используем именно пакет html/template
вместо более простого пакета text/template
.
Допустим кто-то взломал наше приложение и вместо обычного текста для заметки, он добавил туда вредоносный JavaScript код, что произойдет далее?
1 |
<span>{{"<script>alert('xss attack')</script>"}}</span> |
Пакет html\template
обработает данный контент и выведет на экран пользователя следующее:
1 |
<span><script>alert('xss attack')</script></span> |
Пакет html/template
также достаточно умен, чтобы выполнять экранирование в зависимости от обстоятельств. Он будет использовать соответствующие экранированные последовательности в зависимости от того, отображаются ли данные в части страницы, содержащей HTML, CSS или Javascript.
Шаблоны с вложенными шаблонами
Очень важно отметить, что при вызове одного шаблона из другого, точка .
должна быть явно передана в вызываемый шаблон. Это делается через добавление данной точки в конец каждого вызова {{template}}
или {{block}}
.
Например:
1 2 3 4 5 6 7 |
{{template "base" .}} {{template "main" .}} {{template "footer" .}} {{block "sidebar" .}} Любой контент {{end}} |
Мы советуем выработать привычку использования точки всякий раз, когда вы вызываете шаблон с помощью {{template}}
или {{block}}
.
Вызов методов в шаблоне
Если у переданного в шаблон объекта есть методы, вы можете вызывать их (при условии, что они экспортируются и возвращают только одно значение — или одно значение и ошибку).
Например, если поле .Snippet.Created
является типом time.Time
, вы можете отобразить в шаблоне название дня недели, вызвав его метод Weekday() следующим образом:
1 |
<span>{{.Snippet.Created.Weekday}}</span> |
Также, методу можно передать параметры. К примеру, вы можете использовать метод AddDate() чтобы увеличить текущее время на шесть месяцев:
1 |
<span>{{.Snippet.Created.AddDate 0 6 0}}</span> |
Обратите внимание, что данный синтаксис отличается от синтаксиса вызова функций в Go — параметры не заключены в круглые скобки и разделены через пробел, а не запятой.
HTML комментарии в шаблоне
Пакет html/template
всегда удаляет любые HTML комментарии, которые вы оставляете в шаблонах, включая любые условные комментарии которые часто применяются фронтенд разработчиками.
Это необходимо во избежания XSS-атак при отображении динамического контента. Разрешение условных комментариев означало бы, что Go не всегда может предугадать, как браузер интерпретирует разметку на странице. Следовательно, не факт, что он сможет избежать все опасные сценарии. Для решения этой проблемы Go просто удаляет все HTML комментарии.
Скачать исходный код
В конце каждого урока вы можете скачать актуальную на данный момент версию сайта на Golang.
Скачать: snippetbox-23
Администрирую данный сайт с целью распространения как можно большего объема обучающего материала для языка программирования Go. В IT с 2008 года, с тех пор изучаю и применяю интересующие меня технологии. Проявляю огромный интерес к машинному обучению и анализу данных.
E-mail: vasile.buldumac@ati.utm.md
Образование
Технический Университет Молдовы (utm.md), Факультет Вычислительной Техники, Информатики и Микроэлектроники
- 2014 — 2018 Universitatea Tehnică a Moldovei, ИТ-Инженер. Тема дипломной работы «Автоматизация покупки и продажи криптовалюты используя технический анализ»
- 2018 — 2020 Universitatea Tehnică a Moldovei, Магистр, Магистерская диссертация «Идентификация человека в киберпространстве по фотографии лица»