Быстрый старт
Предыстория
Next является портированой версией микрофреймворка php-next2.
Hello World
Минимальное приложение выглядит так:
package main
import (
"github.com/valyala/fasthttp"
"github.com/alexpts/go-next/next"
"github.com/alexpts/go-next/next/layer"
)
type Layer = layer.Layer
type HandlerCtx = layer.HandlerCtx
func main() {
app := next.ProvideMicroApp(nil, nil)
app.Use(Layer{}, func(ctx *HandlerCtx) {
ctx.Response.AppendBodyString(`Hello`)
})
server := &fasthttp.Server{
Handler: app.FastHttpHandler,
}
_ = server.ListenAndServe(":3000")
}
Glossary
Middleware
Middleware - это часть приложения, которая отвечает за конкретную единицу логики в обработке request/response.
Layer
Это абстракция которая является по сути middleware. Layer может иметь как 1 обработчик, так и несколько, чтобы декомпозировать на более мелкие части код.
Application
Приложение является самым высоким уровнем и реалзиует полноую обработку запроса. Приложение это набор Layers, которые определяются для каждого http запроса индивидуально. Задача приложения получить на входе http request и создать http response.
Приложение осознано сводит число сущностей к минимуму, чтобы оставаться действительно простым, понятным и надежным. Основное понятие с которым придется постоянно работать это Layer (Слой).
Приложеине получает на вход объект fasthttp.RequestCtx
и прогоняет через все обработчики слоев. Какие именно слои примут частие в обработке запроса определяется метаифнормацией слоя. Семантически можно выделить следующие типы слоев:
- Активация на любом запросе
- Активация по http методу
- Активация по соответствию uri в регулярном выражении
- Актичация по кастомной стратегии
Собственные стратегии
Из коробки идет минимальный набор стратений (по http методу и поиск по uri). Можно дополнительно реализовать любые кастомные правила, по которым будет принято решение активировать ли слоя для обработки запроса или нет. Для этого нужно добавить свою реализацию next.ResolverContract
Layers Store
Приложение App
хранит в себе в свойстве LayersStore
хранилище слоев. Хранилище слоев позволяет добавлять обрабочтики к приложению посредством методов AddLayer
, Get
, Post
и др. Все методы LayersStore
также можно вызывать на приложении, если хочется писать лаконично.
app := next.NewApp()
app.Use(layer.Config{}, func(ctx *next.HandlerCxt) error {
ctx.Response.AppendBodyString(`Hello`)
return nil
})
Handler
Функция обработчик в общем случае принмает 1 аргумент.
type Handler func(ctx *layer.HandlerCxt)
Context
Контекст - это связующая часть между всеми слоями. Базовый контекст хранит request, response и позволяет получить доступ к текущему слою (Layer) в runtime.
Чтобы делегировать управление следующему обработчику, нужно вызвать метод Next
на объекте контекста обработчика:
app := next.NewApp()
app.Use(layer.Layer{}, func(ctx *next.HandlerCxt) {
ctx.Response.AppendBodyString(`Hello`)
ctx.Next()
})
Слоенный подход
Каждый слой имеет 1 определенную зону отвественности, это позволяет оставаться вашему коду простым и чистым. Слой может самостоятельно создать http response и не передавать управление следующему слою. Например слой, который проверяет аунтификацию, может сам создать http response с статус кодом 401
. С таким подходом, мы можем очень быстро обрабатывать некоторые запросы.
Другой пример, это обработчик, который может кешировать запрос и повторно очень быстро отвечать на запрос из кеша.
Options
Опции объекта Layer
Опция | Описание | По умолчанию | Пример значения |
---|---|---|---|
Path | описывает uri путь, на который должен активироваться слой, можро использловать регулярки, без указария этого параметра слой активируется на любой uri | - | /users/{id}/ |
Name | определяем уникальное имя слоя, через это имя можно будет найти слой (по умолчанию все слои получают имена вида l-0 , l-1 , l-2 , и т.д.) | - | usersAction |
Methods | принимает slice из http методов, слой активируется только на http запросы с указанными http методами. Если методы не переданы, то слой активируется на любой http метод | [] | ['GET'] |
Priority | принимает int число с приоритетом, чем ни меньше число, тем раньше выполнится слой. Это позволяет конфигурировать слои декларативно, не в порядке добавления | 0 | 100 |
Restrictions | принимает массив вида ['id' => \id+], позволяя накладывать дополнительные ограничения на переменны в uri запроса | map[string]string | ['id'=>'\d+'] |
Meta | позволяет прикрепить к слою любые произволные данные | map[string]any | ['foo'=>'bar'] |
Пример использования options в коде:
package main
import (
"github.com/valyala/fasthttp"
"github.com/alexpts/go-next/next"
"github.com/alexpts/go-next/next/layer"
)
func Action(ctx *layer.HandlerCtx) {
name, _ := ctx.UriParams["name"]
ctx.Response.AppendBodyString(`Hello ` + name)
}
func Fallback404(ctx *layer.HandlerCtx) {
ctx.Response.SetStatusCode(404)
ctx.SetContentType("application/json")
ctx.Response.AppendBody([]byte(`{"error": "not found handler"}`))
}
func main() {
app := next.ProvideMicroApp(nil, nil)
app.
Use(layer.Layer{
Path: `/hello/{name}/`,
Name: `HelloAction`,
Methods: []string{`GET`, `POST`},
Priority: 100,
Restrictions: layer.Restrictions{
`name`: `[a-z]+`,
},
}, Action).
Use(layer.Layer{
Name: "Fallback 404 runner layer",
Priority: -9999,
}, Fallback404)
server := &fasthttp.Server{
Handler: app.FastHttpHandler,
}
_ = server.ListenAndServe(":3000")
}
Fast http methods
Для упрощения также доступны методы, которые зеркалируют основные http методы, например:
app := next.NewApp()
handler := func(ctx *next.HandlerCxt) {
ctx.Next()
})
app.
Get(`/users/{id}/`, handler).
Post(`/users/{id}/`, handler).
Put(`/users/{id}/`, handler).
Patch(`/users/{id}/`, handler).
Delete(`/users/{id}/`, handler)
Декларативная кофнигурация
Очень удобно конфигурировать маршруты и порядок декларативно, например посредством yml файлов, простой пример такой конфигурации может выглядить так:
# middlewares
ThrowableToResponse:
controller: xxx
# 404 page
otherwise:
handler: xxx
name: otherwise
priority: 10000
# actions
main:
path: /
methods: [ 'GET' ]
handler: xxx
posts:
path: /post/
methods: [ 'GET' ]
handler: xxx
cat:
path: /cat/{id}/
restrictions:
id: \d+
methods: [ 'GET' ]
handler: xxx
Микрофреймворт не посталвядет фабрику слоев, чтобы не ограничивать синтаксис декларативной конфигурации. Написать такую фабрику это тривиальная задача, пример:
package main
import (
"log"
"os"
"strings"
"gopkg.in/yaml.v3"
"github.com/alexpts/go-next/next/layer"
)
type RouterConfig struct {
Path string
Methods string
Controller string
Name string
Priority int
Handler layer.Handler
Restrictions layer.Restrictions
}
var HandlerMap = map[string]layer.Handler{
"otherwise": MainPageAppHandler2,
"mainPage": MainPageAppHandler2,
"mainPage2": MainPageAppHandler2,
"hello": MainPageAppHandler2,
}
func CreateLayers(projectDir string) []*layer.Layer {
rawData, err := os.ReadFile(projectDir + "/config/router.yml")
var routes map[string]RouterConfig
err = yaml.Unmarshal(rawData, &routes)
if err != nil {
log.Fatalf("error: %v", err)
}
return factoryLayers(routes)
}
func factoryLayers(routes map[string]RouterConfig) []*layer.Layer {
var layers []*layer.Layer
var methods []string
for name, route := range routes {
handler := HandlerMap[route.Controller]
if route.Name == `` {
route.Name = name
}
if route.Methods != `` {
methods = strings.Split(route.Methods, `|`) // GET|POST|PUT
}
l := layer.Layer{
Handlers: []layer.Handler{handler},
Priority: route.Priority,
Name: route.Name,
Path: route.Path,
Restrictions: route.Restrictions,
Methods: methods,
}
layers = append(layers, &l)
}
return layers
}
func MainPageAppHandler2(ctx *layer.HandlerCtx) {
ctx.Response.AppendBodyString(`MainPageAppHandler2`)
}
Inline ограничения параметров в uri
Ограничения на параметр в Path
можно описать дополнительно прямо в Path
помимо конфигурации опции Restrictions
у слоя. Inline формат записи проще для простых регулярок. Для сложных и длинных регулярок лучше использовать явно опцию Restrictions
, чтобы конфигурация слоя оставалась простой для чтения и интерпритации. В общем виде такая форма записи выглядит так {name:restrictRegExp}
. Следующие конфигурации будут эквивалентны:
cat:
path: /cat/{id}/
restrictions:
id: \d+
cat2:
path: /cat/{id:\d+}/
Приоритет
Inline ограничения будут проигнорированы, если этот параметр сконфигурирован через опцию Restrictions
Flow
Каждый слой может что-то делать до вызова ctx.Next()
на этапе запроса, также может что-то делать после вызова ctx.Next()
на этапе ответа. Пример полного выполнения запроса, который проходит сквозь все слои:
Пример flow, кога запрос досрочно может быть обработан одним из слоев, без делегирования обработки через ctx.Next()
:
Каждый слой решает сам, создать http response и пректатить обработку, либо вызвать следующий слой и делегировать ему тем самым обработку.
Fasthttp
Integration
Приложение реализует интерфейс обработчика fasthttp.RequestCtx
. За счет этого оно может интегрировться в любые приложения fasthttp как middleware, принимая на вход fasthttp.RequestCtx
.
Схематично это выглядит как fasthttp
-> fasthttp
-> next
-> fasthttp
.
Если микро приложение должно делегировать обработку запроса далее в классическое fasthttp
приложение, то можно обернуть в такую middleware:
// main
// fasthttp -> fasthttp -> next -> fasthttp
package main
import (
"github.com/valyala/fasthttp"
"github.com/alexpts/go-next/next"
"github.com/alexpts/go-next/next/layer"
)
// nextAppToFasthttpMd - convert next app to fasthttp middleware
func nextAppToFasthttpMd(app *next.MicroApp, handler fasthttp.RequestHandler) fasthttp.RequestHandler {
app.Use(layer.Layer{}, func(ctx *layer.HandlerCtx) {
handler(ctx.RequestCtx)
})
return app.FastHttpHandler
}
func fasthttpMd1(next fasthttp.RequestHandler) fasthttp.RequestHandler {
return func(ctx *fasthttp.RequestCtx) {
ctx.Response.AppendBodyString(`fasthttpMd-1 | `)
next(ctx)
}
}
func fasthttpMd2(next fasthttp.RequestHandler) fasthttp.RequestHandler {
return func(ctx *fasthttp.RequestCtx) {
ctx.Response.AppendBodyString(`fasthttpMd-2 | `)
next(ctx)
}
}
func fasthttpHandler(ctx *fasthttp.RequestCtx) {
ctx.Response.AppendBodyString(`fasthttp-3 | `)
}
func main() {
app := next.ProvideMicroApp(nil, nil)
app.Use(layer.Layer{}, func(ctx *layer.HandlerCtx) {
ctx.Response.AppendBodyString(`next | `)
ctx.Next()
})
handler := fasthttpMd2(fasthttpHandler)
appAsFasthttpMd := nextAppToFasthttpMd(&app, handler) // app as middleware of fasthttp + delegate to next handler
allHandler := fasthttpMd1(appAsFasthttpMd)
server := &fasthttp.Server{
Handler: allHandler,
}
_ = server.ListenAndServe(":3000")
}
Такие интеграции позволяют точечно заменять части старого приложения или переносить небольшими кусочками часть функционала.
net/http mux
Adapter
Можно использвать обработчики в формате http.HandleFunc через адаптер
package main
import (
"net/http"
"github.com/valyala/fasthttp"
"github.com/valyala/fasthttp/fasthttpadaptor"
"github.com/alexpts/go-next/next"
"github.com/alexpts/go-next/next/layer"
)
func FromMux(handler http.HandlerFunc) layer.Handler {
fasthttpHandler := fasthttpadaptor.NewFastHTTPHandlerFunc(handler)
return func(cxt *layer.HandlerCtx) {
fasthttpHandler(cxt.RequestCtx)
}
}
func netHttpHandlerFunc(w http.ResponseWriter, request *http.Request) {
_, _ = w.Write([]byte(request.RequestURI))
}
func main() {
app := next.ProvideMicroApp(nil, nil)
wrapHandler := FromMux(netHttpHandlerFunc)
wrapHandlerWithNext := func() layer.Handler {
return func(ctx *layer.HandlerCtx) {
fasthttpHandler := fasthttpadaptor.NewFastHTTPHandlerFunc(func(w http.ResponseWriter, request *http.Request) {
_, _ = w.Write([]byte(request.Method + " "))
ctx.Next()
})
fasthttpHandler(ctx.RequestCtx)
}
}()
app.Use(layer.Layer{}, wrapHandlerWithNext, wrapHandler)
server := &fasthttp.Server{
Handler: app.FastHttpHandler,
}
_ = server.ListenAndServe(":3000")
}