В этой части я попытаюсь кратко пройтись по пропущенным местам нашего очень упрощенного веб-приложения на Go.
Обертки обработчиков HTTP
Немного поворчу: мне не нравится слово “middleware”. Концепция обертки существует с начала вычислений, поэтому не вижу необходимости изобретать для нее новые слова.
Но отбросим это в сторону, допустим, нам потребовалась аутентификация для определенного URL. Сейчас наш обработчик главной страницы выглядит так:
func indexHandler(m *model.Model) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, indexHTML)
})
}
Мы можем написать функцию, которая принимает http.Handler
в качестве аргумента и возвращает (другой) http.Handler
. Возвращенный обработчик проверяет, аутентифицирован ли пользователь, с помощью m.IsAuthenticated()
(неважно, что конкретно там происходит) и перенаправляет пользователя на страницу входа или выполняет оригинальный обработчик, вызывая его метод ServeHTTP()
.
func requireLogin(h http.Handler, m *model.Model) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !m.IsAuthenticated(r) {
http.Redirect(w, r, loginURL, http.StatusFound)
return
}
h.ServeHTTP(w, r)
})
}
С учетом этого, регистрация обработчика теперь будет выглядеть так:
http.Handle("/", requireLogin(indexHandler(m), m))
Таким способом можно обернуть обработчики в любое необходимое количество слоев и этот очень гибкий подход. Все, начиная от установки заголовков до сжатия вывода, может быть выполнено с помощью обертки. Также отмечу, что мы можем передавать любые нужные нам аргументы, например, нашу *model.Model
.
Параметры в URL
В какой-то момент нам могут понадобиться параметры в URL, например, /Person/3
, где 3
— это идентификатор человека. Стандартная библиотека Go ничего для этого не предоставляет, оставляя эту задачу в качестве упражнения для разработчика. Программный компонент, ответственный за такую штуку, называется Mux, т.е. «мультиплексор», или «маршрутизатор», и его можно заменить нестандартной реализацией. Маршрутизатор также реализует метод ServeHTTP()
, что означает, что он удовлетворяет интерфейсу http.Handler, то есть он является обработчиком.
Очень популярным вариантом маршрутизатора является Gorilla Mux. Ему можно делегировать целые пути в тех местах, где требуется больше гибкости. Например, мы можем решить, что все, от /person
и ниже, обрабатывается маршрутизатором Gorilla, и мы хотим, чтобы все это еще было аутентифицировано, тогда это может выглядеть так:
// import "github.com/gorilla/mux"
pr := mux.NewRouter().PathPrefix("/person").Subrouter()
pr.Handle("/{id}", personGetHandler(m)).Methods("GET")
pr.Handle("/", personPostHandler(m)).Methods("POST")
pr.Handle("/{id}", personPutHandler(m)).Methods("PUT")
http.Handle("/person/", requireLogin(pr))
Примечание: я обнаружил, что слэши в конце важны, а правила на счет того, когда они требуются, немного запутаны.
Есть еще много других реализаций маршрутизатора/мультиплексора. Прелесть в том, что не привязываясь ни к какому фреймворку, мы можем выбрать тот маршрутизатор, который лучше всего нам подходит, или написать свой собственный (их нетрудно реализовать).
Обработка статики
Одна из самых изящных вещей в Go — это то, что скомпилированная программа представляет собой единственный двоичный файл, а не большую кучу файлов, как это часто бывает с большинством скриптовых языков и даже с некоторыми компилируемыми. Но если наша программа зависит от статических файлов (JS, CSS, изображений и других файлов), нам нужно будет скопировать и их на сервер во время развертывания.
Мы можем сохранить эту характерную особенность — «один бинарник» — нашей программы, включив статику как часть самого двоичного файла. Для этого существует проект go-bindata и его племянник go-bindata-assetsfs.
Поскольку упаковка статики в двоичный файл несколько выходит за пределы того, что может сделать go build
, нам понадобится какой-то скрипт, который об этом позаботится. Лично я предпочитаю использовать проверенный и "трушный" make
, и не так уж редко можно встретиться с «Makefile» в проекте Go.
Вот пример подходящего правила Makefile:
ASSETS_DIR = "assets"
build:
@export GOPATH=$${GOPATH-~/go} &&
go get github.com/jteeuwen/go-bindata/... github.com/elazarl/go-bindata-assetfs/... &&
$$GOPATH/bin/go-bindata -o bindata.go -tags builtinassets ${ASSETS_DIR}/... &&
go build -tags builtinassets -ldflags "-X main.builtinAssets=${ASSETS_DIR}"
Это правило создает файл bindata.go
, который помещается в тот же каталог, где находится main.go
, соответственно, он становится частью пакета main
. main.go
как-то узнает, что статические файлы встроены, — это получается с помощью трюка -ldflags "-X main.builtinAssets=${ASSETS_DIR}"
, так мы можем присваивать значения переменным на этапе компиляции. Это значит, что теперь наш код может проверять значение builtinAssets
, чтобы решить, что делать дальше, например:
if builtinAssets != "" {
log.Printf("Running with builtin assets.")
cfg.UI.Assets = &assetfs.AssetFS{Asset: Asset, AssetDir: AssetDir, AssetInfo: AssetInfo, Prefix: builtinAssets}
} else {
log.Printf("Assets served from %q.", assetsPath)
cfg.UI.Assets = http.Dir(assetsPath)
}
Вторая важная вещь заключается в том, что мы определяем build tag с именем builtinassets
. Мы также сообщаем go-bindata о нем, что означает «скомпилируй меня, только когда установлен builtinassets», а это позволяет контролировать, при каких условиях необходимо компилировать bindata.go
(который содержит нашу статику в виде кода Go).
Предварительная транспиляция JavaScript
Напоследок (по порядку, а не по важности), я хочу кратко упомянуть упаковку веб-статики. Для описания этого должным образом материала достаточно для целой новой серии статей, и это не имело бы ничего общего с Go. Но я могу хотя бы перечислить некоторые моменты.
-
В принципе, вы можете пойти на уступки — установить npm и настроить файл
package.json
. -
Как только npm установлен, банально устанавливается компилятор командной строки Babel —
babel-cli
, который является одним из способов транспиляции JavaScript. -
Более сложный, в чем-то разочаровывающий, но в конечном счете более гибкий способ — использовать webpack. Webpack будет не только претранспиливать JS-код, но и объединит все JS-файлы в один, а также минимизирует его.
- Я был удивлен тому, насколько сложно было обеспечить функциональность импорта модулей в JavaScript. Проблема в том, что есть стандарт ES6 для ключевых слов
import
иexport
, но нет реализации, и даже Babel предполагает, что реализовывать их для вас будет кто-нибудь другой. В конце концов, я остановился на SystemJS. Некоторое осложнение с SystemJS заключается в том, что внутрибраузерная транспиляция Babel должна быть чем-то, понятным для SystemJS, поэтому мне пришлось использовать его плагин для Babel. Webpack, в свою очередь (если я правильно понял), предоставляет свою собственную реализацию поддержки модулей, поэтому при упаковке SystemJS не нужен. В любом случае, это было довольно неприятно.
Заключение
Я бы сказал, что в примере, который я описываю в этой серии из четырех частей, Go безусловно блистает, а вот JavaScript — не особо. Но как только я преодолел начальные трудности с тем, чтобы заставить все это заработать, React/JSX оказался простым и, возможно, даже приятным для работы.
На этом я, пожалуй, закончу. Надеюсь, статьи оказались вам полезными.
Автор: Сергей Соломеин