Теперь, когда мы выпустили бета-версию 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, который вы можете добавить в качестве зависимости. Он позволяет определять маршруты и контроллеры с помощью макросов:
@Controllerstruct 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/