Это продолжение материала, часть 1, часть 2
Вывод топика
У нас пока не работают ссылки на топики из общего списка. Сейчас мы добавим обработчики клика и шаблон для вывода топика.
Настройка роутов
Мы можем создать роут для обработки запросов для топика подобно роуту из предыдущей части. Однако, мы должны учитывать, что хотя обработчик для всех топиков будет один, URL каждого топика должен быть уникальным. Gin позволяет это сделать с помощью передачи параметров в роут:
router.GET("/article/view/:article_id", getArticle)
Этот роут будет обрабатывать соответствующие указанному пути запросы, а также хранить значение параметра, переданного в роут — article_id, который мы обработаем в обработчике роута. Обработчиком нашего роута будет функция getArticle.
Изменённый файл routes.go:
// routes.go
package main
func initializeRoutes() {
// обработчик главного роута
router.GET("/", showIndexPage)
// Обработчик GET-запросов на /article/view/некоторый_article_id
router.GET("/article/view/:article_id", getArticle)
}
Шаблоны
Для вывода топика нам нужно создать новый шаблон templates/article.html. Он будет создан так же, как шаблон index.html, но с небольшим отличием: вместо передачи в него переменной со списком топиков, мы будем передавать в него только один топик.
Посмотреть код шаблона article.html можно на Github.
Определяем требования к обработчику роутов юнит-тестами
Тест обработчика будет проверять выполнение следующих условий:
- Обработчик отвечает статус-кодом HTTP 200,
- Возвращаемый HTML содержит тег title, содержащий название полученного топика.
Код теста будет в функции TestArticleUnauthenticated в файле handlers.article_test.go. Вспомогательные функции мы разместим в файле common_test.go.
Создаём обработчик роута
Итак, что должен делать обработчик роута для топика — getArticle:
1. Получить ID топика для вывода
Для вывода нужного топика, мы должны получить его ID из контекста. Примерно так:
c.Param("article_id")
где c — это Контекст Gin, который передаётся параметром в любой обработчик при разработке с Gin.
2. Получить сам топик
Это можно сделать с помощью функции getArticleByID() из файла models.article.go:
article, err := getArticleByID(articleID)
Функция getArticleByID (в models.article.go) выглядит так:
func getArticleByID(id int) (*article, error) {
for _, a := range articleList {
if a.ID == id {
return &a, nil
}
}
return nil, errors.New("Article not found")
}
Эта функция считывает список топиков в цикле и возвращает топик, ID которого соответствует переданному ID. Если такого топика нет, она возвращает ошибку.
3. Обработать шаблон article.html, передав в него топик
Код ниже как раз делает это:
c.HTML(
// Зададим HTTP статус 200 (OK)
http.StatusOK,
// Используем шаблон article.html
"article.html",
// Передадим данные в шаблон
gin.H{
"title": article.Title,
"payload": article,
},
)
Обновлённый файл handlers.article.go будет таким:
// handlers.article.go
package main
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
func showIndexPage(c *gin.Context) {
articles := getAllArticles()
// Вызовем метод HTML из Контекста Gin для обработки шаблона
c.HTML(
// Зададим HTTP статус 200 (OK)
http.StatusOK,
// Используем шаблон index.html
"index.html",
// Передадим данные в шаблон
gin.H{
"title": "Home Page",
"payload": articles,
},
)
}
func getArticle(c *gin.Context) {
// Проверим валидность ID
if articleID, err := strconv.Atoi(c.Param("article_id")); err == nil {
// Проверим существование топика
if article, err := getArticleByID(articleID); err == nil {
// Вызовем метод HTML из Контекста Gin для обработки шаблона
c.HTML(
// Зададим HTTP статус 200 (OK)
http.StatusOK,
// Используем шаблон index.html
"article.html",
// Передадим данные в шаблон
gin.H{
"title": article.Title,
"payload": article,
},
)
} else {
// Если топика нет, прервём с ошибкой
c.AbortWithError(http.StatusNotFound, err)
}
} else {
// При некорректном ID в URL, прервём с ошибкой
c.AbortWithStatus(http.StatusNotFound)
}
}
Если сейчас собрать и запустить наше приложение, при открытии localhost:8080/article/view/1 в браузере оно будет выглядеть так:
Новые файлы, добавленные в этом разделе:
└── templates
└── article.html
Ответ в JSON/XML
В этом разделе мы немного перепишем приложение так, что оно, в зависимости от заголовков запроса, будет отвечать в формате HTML, JSON или XML.
Повторно используемые функции
До сих пор мы использовали метод HTML Контекста Gin для обработки шаблонов прямо из обработчика. Этот способ хорошо если мы всегда будем выводить только в формате HTML. Однако, если мы хотим менять формат ответа, к примеру, на основе какого-то параметра, мы должны переписать эту часть функции, чтобы она делала только валидацию данных и их получение, а выводом в шаблон будет заниматься другая функция в зависимости от формата вывода на основе заголовка Accept. Мы создадим эту функция в файле main.go и она будет общая для всех обработчиков.
В Gin в Контексте, переданном обработчику роута, есть поле Request. В этом поле есть Header, в котором содержатся все заголовки запроса. Для получения заголовка Accept мы можем использовать метод Get в Header, вот так:
// c - это Gin Context
c.Request.Header.Get("Accept")
- Если заголовок: application/json, функция выводит JSON,
- Если заголовок: application/xml, функция выводит XML, и
- Если заголовок любой другой или вообще пустой, функция выводит HTML.
Полный код функции:
// Render one of HTML, JSON or CSV based on the 'Accept' header of the request
// If the header doesn't specify this, HTML is rendered, provided that
// the template name is present
func render(c *gin.Context, data gin.H, templateName string) {
switch c.Request.Header.Get("Accept") {
case "application/json":
// Respond with JSON
c.JSON(http.StatusOK, data["payload"])
case "application/xml":
// Respond with XML
c.XML(http.StatusOK, data["payload"])
default:
// Respond with HTML
c.HTML(http.StatusOK, templateName, data)
}
}
Изменяем требования к обработчику роутов
Так как мы теперь должны проверить ответ в JSON и XML если задан специальный заголовок, нам нужно добавить тесты в файл handlers.article_test.go для проверки этих условий:
- Проверить, что приложение вернёт список топиков в формате JSON если заголовок Accept равен application/json
- Проверить, что приложение вернёт список топиков в формате XML если заголовок Accept равен application/xml
Мы добавим соответствующие функции TestArticleListJSON и TestArticleXML.
Обновляем обработчики
Обработчик у нас уже полностью готов, нам нужно только изменить метод обработки c.HTML на соответствующий требуемому формату метод вывода.
К примеру, обработчик роута showIndexPage будет изменён с такого:
func showIndexPage(c *gin.Context) {
articles := getAllArticles()
// Call the HTML method of the Context to render a template
c.HTML(
// Set the HTTP status to 200 (OK)
http.StatusOK,
// Use the index.html template
"index.html",
// Pass the data that the page uses
gin.H{
"title": "Home Page",
"payload": articles,
},
)
}
на такой:
func showIndexPage(c *gin.Context) {
articles := getAllArticles()
// Call the render function with the name of the template to render
render(c, gin.H{
"title": "Home Page",
"payload": articles}, "index.html")
}
Получаем список топиков в формате JSON
Чтобы увидеть приложение в работе, соберём его и запустим. Затем выполним следующую команду:
curl -X GET -H "Accept: application/json" http://localhost:8080/
Она должна вернуть следующее:
[{"id":1,"title":"Article 1","content":"Article 1 body"},{"id":2,"title":"Article 2","content":"Article 2 body"}]
Как вы видите, мы получили ответ в формате JSON, передав заголовок Accept как application/json.
Список топиков в формате XML
Теперь запросим детали конкретной статьи в формате XML. Для этого запустите приложение как написано выше и затем выполните команду:
curl -X GET -H "Accept: application/xml" http://localhost:8080/article/view/1
В ответ должно прийти следующее:
<article><ID>1</ID><Title>Article 1</Title><Content>Article 1 body</Content></article>
Тестирование приложения
Мы использовали тесты для определения требований к обработчикам роутов и моделям, поэтому можем теперь запустить их и проверить, что всё работает как предполагалось. В директории проекта запустите следующую команду:
go test -v
Результат должен быть примерно таким:
=== RUN TestShowIndexPageUnauthenticated
[GIN] 2016/06/14 - 19:07:26 | 200 | 183.315µs | | GET /
--- PASS: TestShowIndexPageUnauthenticated (0.00s)
=== RUN TestArticleUnauthenticated
[GIN] 2016/06/14 - 19:07:26 | 200 | 143.789µs | | GET /article/view/1
--- PASS: TestArticleUnauthenticated (0.00s)
=== RUN TestArticleListJSON
[GIN] 2016/06/14 - 19:07:26 | 200 | 51.087µs | | GET /
--- PASS: TestArticleListJSON (0.00s)
=== RUN TestArticleXML
[GIN] 2016/06/14 - 19:07:26 | 200 | 38.656µs | | GET /article/view/1
--- PASS: TestArticleXML (0.00s)
=== RUN TestGetAllArticles
--- PASS: TestGetAllArticles (0.00s)
=== RUN TestGetArticleByID
--- PASS: TestGetArticleByID (0.00s)
PASS
ok github.com/demo-apps/go-gin-app 0.084s
Как мы видим, эта команда запускает все написанные нами тесты и, в нашем случае, сообщает, что всё работает как положено. Если вы присмотритесь к выводу, то увидите, что Go также сделал и HTTP запросы для нас, проверив обработчики роутов.
Заключение
В этом цикле статей мы сделали приложение с помощью фреймворка Gin и постепенно добавили в него функционал. Мы написали тесты, чтобы наше приложение было надёжным, а также использовали методологию повторно используемого кода, чтобы сделать вывод в различные форматы без больших затрат времени.
Код приложения можно скачать в этом Github репозитории.
Gin очень прост для того, чтобы начать писать веб-приложения — вкупе со встроенной функциональностью Go, он легко позволяет строить высококачественные, хорошо покрытые тестами веб-приложения и микросервисы.
По материалам Building Go Web Applications and Microservices Using Gin
Разработчик: java, kotlin, c#, javascript, dart, 1C, python, php.
Пишите: @ighar. Buy me a coffee, please :).