Теперь, когда мы выпустили бета‑версию Vapor 5, пришло время посмотреть, что на самом деле изменилось! В этом посте мы расскажем о том, как работает Vapor 5, какие изменения мы внесли и как будет выглядеть ваш новый код. Этот пост — часть серии публикаций, посвящённых Vapor Week. Подробнее читайте в основном посте блога.

Как выглядит Vapor 5

Вот простой маршрут в Vapor 5:

app.get("hello") { req in
    "Hello, world!"
}

Знакомо? На самом деле это то же самое, что и в Vapor 4! А как насчёт работы с JSON через Content:

app.post("user") { req in
    let userData = try await req.content.decode(UserData.self)
    return UserMessage(message: "Hello \(userData.name), you are \(userData.age)")
}

Опять же, очень знакомо! Вы могли заметить добавленный await в вызове decode — поскольку Vapor 5 теперь по умолчанию использует потоковую передачу, тело запроса не собирается, если в этом нет необходимости, поэтому вызов стал асинхронным.

Наконец, давайте посмотрим, как использовать Fluent:

// dbPool — это тип `Databases`, определённый при конфигурации приложения

app.get("todos") { req in
    let todos = try await Todo.query(on: dbPool.database).all()
    return todos
}

Вероятно, это самое заметное отличие, которое обратит внимание большинство людей. Причины объясняются в следующем разделе, но, будем надеяться, для большинства это небольшое изменение, которое легко автоматизировать.

Примечание: API для Fluent всё ещё дорабатывается, поэтому названия могут измениться.

Различия, которые вы можете заметить

Простые вещи выглядят очень похоже. И действительно, если вы полностью перешли на асинхронные API с использованием await, то переход должен пройти гладко. Но кое‑что изменилось.

Прощайте, блокировки

Работа Vapor 4 по внедрению Sendable увенчалась огромным успехом — мы перешли от ежемесячных сообщений на GitHub о трудно диагностируемой гонке данных к нулю сообщений о сбоях из‑за гонки данных! Это было невероятное достижение. Однако, чтобы сделать систему действительно безопасной, нам пришлось внедрить блокировки повсеместно, что препятствовало любой оптимизации производительности. Теперь, когда у нас есть API, который мы можем ломать (Application немного изменился, Request и Response теперь являются структурами, а некоторые свойства стали неизменяемыми), мы наконец можем их убрать! Думаю, я убрал из кодовой базы более 50 блокировок и добавил лишь несколько Mutex в тех местах, где нам действительно нужен какой‑то изменяемый тип.

Больше никакого Storage

Вероятно, это одно из самых значительных изменений, но если вы не пишете пакеты для Vapor, то вряд ли его заметите. Однако вы заметите изменения в работе сервисов, о которых мы поговорим далее.

В Vapor 3 и Vapor 4 всё, что было связано с Request, было привязано к EventLoop. Это обеспечивало некоторую безопасность, поскольку, по сути, всё было привязано к одному потоку, а также повышало производительность, поскольку при обработке запросов не происходило переключения между потоками. Однако это привело к необходимости введения Storage, чтобы мы могли хранить фабрики и кэшировать типы, привязанные к нужному event loop. В Swift Concurrency эта модель не используется, и решение о том, где будет выполняться ваш код, зависит от пула задач (не считая пользовательских исполнителей). Это избавляет от необходимости привязывать логгер, клиент и базу данных к одному и тому же event loop и позволяет удалить Storage.

Настоящее внедрение зависимостей для сервисов

Поскольку нам больше не нужно следить за тем, чтобы сервисы использовали тот же event loop, что и Application и Request, сервисы претерпели значительные изменения, и именно это изменение повлияет на вас больше всего. Расширения и вычисляемые свойства ушли в прошлое, уступив место простому внедрению зависимостей. В Application в Vapor есть несколько типов, таких как ViewRenderer и Client, которые создаются при инициализации, но если вам нужен Databases (по сути, пул соединений с базой данных), просто передайте его в сервис! Я большой поклонник такого понятного и прямого внедрения зависимостей, потому что оно чётко показывает, какие из ваших типов, например контроллеры, нуждаются в той или иной зависимости, например в клиенте или базе данных, и его легко тестировать с помощью протоколов. Если вы предпочитаете свой любимый фреймворк для внедрения зависимостей, ничто не мешает вам использовать его для передачи типов в контроллеры и сервисы.

Потоковая передача (Streaming)

Вам больше не нужно указывать метод сбора тела при объявлении маршрутов, поскольку все маршруты по умолчанию используют потоковую передачу. Если вы используете какой‑либо из вспомогательных методов для сбора тела (например, .collect() или функции, созданные на их основе, такие как content.decode()), то будет использоваться параметр maxBodySize. В противном случае, если вы сами вызываете потоковый API, вы сами решаете, сколько данных собирать (поскольку в большинстве случаев, когда это требуется, вы обрабатываете каждый фрагмент отдельно). Вы можете просто прочитать тело запроса с помощью:

app.post("upload") { req -> UploadResult in
    var bytesReceived = 0

    try await req.body.forEachChunk { chunk in
        bytesReceived += chunk.count
        // Обработайте или сохраните фрагмент здесь, прежде чем запрашивать следующий.
    }

    return UploadResult(bytesReceived: bytesReceived)
}

В данном случае каждый чанк — это Span<UInt8>. Поскольку мы указываем время жизни, компилятор не позволяет выйти за его пределы. Чтение тела также автоматически поддерживает обратное давление.

Тела ответов получают writer для записи:

app.get("events") { req in
    Response(
        headers: [.contentType: "text/event-stream"],
        body: .init(stream: { writer in
            for count in 1...5 {
                try await writer.write("data: \(count)\n\n")
                try await Task.sleep(for: .seconds(1))
            }
        })
    )
}

Опять же, это автоматически поддерживает противодавление (через await в вызове write), и мы используем гарантии времени жизни в Swift, чтобы гарантировать, что writer никогда не будет освобождён.

Макросы

Мы представили экспериментальный продукт VaporMacros, который вы можете добавить в качестве зависимости. Он позволяет определять маршруты и контроллеры с помощью макросов:

@Controller
struct UserController {
    @GET("users")
    func index(req: Request) async throws -> [User] {
        // Fetch the users from your repository.
    }

    @GET("users", UUID.self)
    func show(req: Request, id: UUID) async throws -> User {
        // `id` has already been extracted and decoded for us.
        User(id: id, name: "Vapor")
    }

    @POST("users")
    func create(req: Request) async throws -> User {
        let input = try await req.content.decode(CreateUser.self)
        return User(id: UUID(), name: input.name)
    }
}

Что действительно круто в этом подходе, так это то, что он обеспечивает типобезопасную маршрутизацию! То есть, если вы определяете маршрут с помощью @GET("users", UUID.self), вы должны привязать его к функции с параметром типа UUID. Если вы используете неправильный тип или забудете указать параметр id, вы получите ошибку компиляции. Это должно существенно изменить ситуацию для тех, кто, как и я, никогда не был в восторге от использования строк для всех маршрутов.

У нас также есть макросы для аутентификации:

@GET("account", "me")
@AuthMiddleware(User.self, TokenAuthMiddleware())
func me(req: Request, user: User) async throws -> User {
    // You have a guaranteed user here!
}

Опять же, если обработчик маршрута определён неправильно, вы получите ошибки компиляции, и у вас будет пользовательская модель с проверкой типов. Если вам нужно только проверить, существует ли пользователь, сделайте параметр необязательным, и тогда вместо 401 Unauthorized в случае, если пользователь не существует, запрос будет пропущен! Этот макрос меня особенно радует!

Обратите внимание, что эта функция всё ещё активно дорабатывается. У нас есть прототип для определения промежуточного программного обеспечения, и в настоящее время мы работаем над тем, чтобы компилятор мог передавать зависимости в промежуточное ПО. Следите за релизами, чтобы узнать о новых улучшениях.

Частичные параметры маршрута

Одна из новых функций нашего маршрутизатора, довольно специфическая, но очень важная для тех, кому она нужна (например, для реестров пакетов!), — это частичные параметры маршрута! Теперь это работает:

app.get("files", ":{file}.json") { req -> String in
    let file = try req.parameters.require("file")
    return "Requested JSON file: \(file)"
}

Это должно избавить вас от необходимости подбирать строки и писать собственный код для решения этой задачи.

Более глубокая интеграция с экосистемой

Наконец, как и в случае с каждым крупным релизом, мы можем в полной мере воспользоваться преимуществами расширенной экосистемы и лучше интегрироваться с ней. Например, ConsoleKit теперь предлагает только высокопроизводительный LogHandler. Вся работа с командами передана Swift Argument Parser. Что касается логов, то теперь мы используем Logger.current везде, что значительно упрощает доступ к логгеру в любом месте ваших сервисов или маршрутов. А поскольку теперь мы поддерживаем эту функцию, к любому вызову логгера внутри запроса автоматически добавляется идентификатор запроса в метаданных.

Теперь мы используем Swift Configuration для настройки вашего сервера, то есть вы можете использовать любой формат конфигурации: файлы YAML, файлы .env, переменные среды, аргументы командной строки — и мы всё это подхватим, если вы укажете провайдера. Все типы, которые мы предоставляем для запросов и ответов, теперь основаны на Swift HTTP Types, и вам больше не нужно преобразовывать их в типы NIO и обратно!

Наконец, мы обеспечиваем интеграцию с Swift Service Lifecycle. Во‑первых, если у вас есть другие сервисы Service, которые вы хотите использовать в своём приложении Vapor, вы можете передать их в своё приложение, и Vapor запустит их при запуске приложения и остановит при его завершении с помощью одной простой команды:

app.addService(MyWorker())

Если вы сами управляете несколькими сервисами и хотите просто добавить своё приложение Vapor в группу, Application соответствует Service и позволяет это сделать:

let app = try await Application()

let services = ServiceGroup(
    configuration: .init(
        services: [
            .init(
                service: app,
                successTerminationBehavior: .gracefullyShutdownGroup
            ),
            .init(
                service: MyWorker(),
                successTerminationBehavior: .gracefullyShutdownGroup
            ),
        ],
        gracefulShutdownSignals: [.sigterm, .sigint],
        logger: Logger.current
    )
)
try await services.run()

Лучший Vapor

Все эти изменения делают Vapor более удобным в использовании, более производительным, лучше интегрирующимся в экосистему и позволяющим создавать более качественные и безопасные приложения! Нам не терпится увидеть, что у вас получится!

Комментарии (0)