Быстрый старт


Предыстория

Next является портированой версией микрофреймворка php-next2open in new window.

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 число с приоритетом, чем ни меньше число, тем раньше выполнится слой. Это позволяет конфигурировать слои декларативно, не в порядке добавления0100
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() на этапе ответа. Пример полного выполнения запроса, который проходит сквозь все слои:

200 flow

Пример flow, кога запрос досрочно может быть обработан одним из слоев, без делегирования обработки через ctx.Next():

401 flow

Каждый слой решает сам, создать 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.HandleFuncopen in new window через адаптер

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")
}