Что нового в бета-версии Vapor 5

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Теперь, когда мы выпустили бета-версию 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 более удобным в использовании, более производительным, лучше интегрирующимся в экосистему и позволяющим создавать более качественные и безопасные приложения! Нам не терпится увидеть, что у вас получится!

Источник: https://habr.com/ru/articles/1096956/


Интересные статьи

Интересные статьи

«Первая Форма» и российская компания «Нейроинформ», которая занимается анализом и оценкой киберрисков, подписали соглашение о стратегическом партнёрстве. В рамках сотрудничества клиенты «Первой Ф...
Привет! Меня зовут Анастасия Соколенко, я отвечаю за безопасную разработку в Битрикс24. Вместе с коллегами мы не только проверяем код, который пишут наши разработчики, но и учим их делать его максимал...
Был такой функционал в Битрикс24 - Face-трекер. В 2022 году его отключили, о чем вышло официальное уведомлениеПричина для отключения - "Провайдер, с которым мы сотрудничаем, сейчас не может предостави...
Софт с открытым кодом незаменим при внедрении технологий искусственного интеллекта и больших данных. IT-стартапы уже не используют проприетарные решения. От государства п...
Принято считать, что персонализация в интернете это магия, которая создается сотнями серверов на основе БигДата и сложного семантического анализа контента.