Я очень рад, что Apple сделала Swift открытым в 2015 году, потому что это не только означает, что появятся более интересные функции, но и мы можем запускать Swift на машинах с Linux. Что еще более важно, последний дает нам возможность написать сервер на Swift. В настоящее время существует несколько различных серверных фреймворков Swift, таких как Vapor, Perfect и Kitura. Причина, по которой я выбрал Vapor 3 для этой статьи, заключается в том, что он быстро поддерживает SwiftNIO. В результате Vapor 3 предоставляет сжатые асинхронные API-интерфейсы, и это очень хороший шанс попрактиковаться в асинхронном программировании. В этой статье я собираюсь продемонстрировать, как создавать простые конечные точки RESTful с помощью Vapor 3.
Подготовка
Если вы еще не установили Vapor, следуйте этой инструкции, чтобы правильно установить Vapor. После успешной установки мы можем сгенерировать нашу новую папку проекта с помощью команды Vapor’s toolbox new.
vapor new CRUDControllers
Поскольку нам не нужны модели и шаблоны контроллеров, созданные с помощью набора инструментов, удалите все в папках Models
и Controllers
с помощью следующих команд.
cd CRUDControllers rm -rf Sources/App/Models/ rm -rf Sources/App/Controllers/
Кроме того, перед сборкой проекта следует удалить ненужный код. Прежде всего, откройте Sources/App/configure.swift
file и удалите следующую строку.
migrations.add(model: Todo.self, database: .sqlite)
Во-вторых, перейдите в Sources/App/router.swift
файл и удалите следующие строки.
// Example of configuring a controller let todoController = TodoController() router.get("todos", use: todoController.index) router.post("todos", use: todoController.create) router.delete("todos", Todo.parameter, use: todoController.delete)
Наконец, мы можем сгенерировать файл проекта Xcode с vapor xcode -y
, и эта команда автоматически откроет CRUDControllers.xcodeproj
. Мы можем выбрать Run
схему и проект должен быть построен успешно.
Перед созданием нашего типа модели необходимо упомянуть еще одну важную вещь внутри configure.swift
. В этой статье мы собираемся использовать базу данных SQLite в памяти, чтобы мы могли сохранить поставщика по умолчанию FluentSQLiteProvider
и конфигурации базы данных, сгенерированные набором инструментов.
Модель
Лучше всего создавать файлы вне Xcode. Это позволяет Swift Package Manager, который используется набором инструментов Vapor, гарантировать, что файлы ссылаются на правильную цель. Давайте создадим наш User
файл модели и повторно сгенерируем файл проекта Xcode со следующими рекомендациями.
mkdir Sources/App/Models touch Sources/App/Models/User.swift vapor xcode -y
Наша модель User
на данный момент будет иметь три свойства: id
, name
и username
. Кроме того, как я упоминал ранее, наша User
модель будет храниться в базе данных SQLite. Поэтому откройте User.swift
с помощью Xcode и запишите следующие строки в файл.
import Vapor import FluentSQLite final class User: Codable { var id: Int? var name: String var username: String init(name: String, username: String) { self.name = name self.username = username } } extension User: SQLiteModel {} extension User: Migration {}
Причина, по которой наша User
модель соответствует Migration
протоколу, заключается в том, что этот протокол используется для создания таблицы для модели в базе данных. Причем таблица должна создаваться при запуске приложения. Давайте переключимся на configure.swift
и добавим следующую строку перед services.register(migrations)
.
migrations.add(model: User.self, database: .sqlite)
Вообще говоря, миграцию следует выполнять только один раз. Если они выполнялись в базе данных, они никогда не будут выполняться снова. Однако, поскольку сейчас мы используем базу данных в памяти, миграция будет выполняться каждый раз при запуске приложения.
Учитывая, что наши конечные точки CRUD должны иметь возможность получать данные JSON в качестве тела HTTP и возвращать ответы в формате JSON, Vapor предоставляет протокол Content
, который позволяет нам преобразовывать модель в формат JSON. Поскольку наша User
модель уже соответствует протоколу Codable
, все, что нам нужно сделать, это добавить следующую строку внизу User.swift
.
extension User: Content {}
Наконец, чтобы было проще получать User
модель с помощью наших конечных точек, добавьте следующую строку под extension User: Content {}
extension User: Parameter {}
На этом наша User
модель завершена. Попробуйте собрать и запустить приложение, чтобы убедиться, что все работает нормально.
Контроллер
Vapor предоставляет нам контроллеры для обработки взаимодействий с клиентом, таких как запросы, их обработки и возврата ответов. В нашем случае один UsersController
будет обрабатывать операции CRUD для модели User
. Опять же, вернитесь в Терминал и создайте наш файл контроллера с помощью следующих команд.
mkdir Sources/App/Controllers touch Sources/App/Controllers/UsersController.swift vapor xcode -y
Давайте по очереди реализуем наши функции CRUD. Прежде всего, наш UsersController
должен уметь создавать нашу User
модель. Пожалуйста, напишите следующие строки в наш UsersController.swift
файл.
import Vapor final class UsersController { func createHandler(_ req: Request) throws -> Future<User> { return try req.content.decode(User.self).flatMap { (user) in return user.save(on: req) } } }
Поскольку наша User
модель уже соответствует протоколу Content
, экземпляр User
может быть сгенерирован из данных JSON тела HTTP с помощью req.content.decode(User.self)
. Кроме того, поскольку модель также соответствует протоколу SQLiteModel
, экземпляр можно сохранить в базе данных SQLite с помощью user.save(on: req)
. Мы связываем эти две операции с flatMap
, потому что обе они асинхронны. Здесь мы впервые встречаем тип Future
. Как я упоминал в начале этой статьи, Vapor 3 предоставляет асинхронные API, поскольку поддерживает SwiftNIO. Если вы хотите узнать больше о типе Future
, пожалуйста, прочтите этот документ для более подробной информации.
Во-вторых, наш UsersController
должен иметь возможность получить нашу User
модель. Добавьте следующие строки под только что написанным createHandler
методом.
final class UsersController { // ... func getAllHandler(_ req: Request) throws -> Future<[User]> { return User.query(on: req).decode(User.self).all() } func getOneHandler(_ req: Request) throws -> Future<User> { return try req.parameters.next(User.self) } }
С одной стороны, мы получаем все экземпляры нашей User
модели, запрашивая базу данных. С другой стороны, поскольку наша User
модель соответствует Parameter
протоколу, req.parameters.next(User.self)
будет извлекать экземпляр с данным идентификатором из базы данных.
Следующим шагом будет реализация функции обновления для нашего UsersController
. Давайте добавим следующий метод под методами получения.
final class UsersController { // ... func updateHandler(_ req: Request) throws -> Future<User> { return try flatMap(to: User.self, req.parameters.next(User.self), req.content.decode(User.self)) { (user, updatedUser) in user.name = updatedUser.name user.username = updatedUser.username return user.save(on: req) } } }
Используемая здесь функция flatMap
отличается от предыдущей. Фактически он ожидает завершения обоих req.parameters.next(User.self)
и req.content.decode(User.self)
, а затем выполняет блок. Внутри блока мы просто обновляем экземпляр новыми значениями, а затем сохраняем его в базе данных.
Затем давайте добавим последнюю часть наших конечных точек CRUD - функцию удаления. Как обычно, добавьте следующий метод ниже updateHandler
метод.
final class UsersController { // ... func deleteHandler(_ req: Request) throws -> Future<HTTPStatus> { return try req.parameters.next(User.self).flatMap { (user) in return user.delete(on: req).transform(to: HTTPStatus.noContent) } } }
Мы получаем экземпляр с помощью req.parameters.next(User.self)
и удаляем его из базы данных с помощью user.delete(on: req)
. Поскольку содержимого для возврата нет, мы можем просто предоставить ответ 204 No Content с transform(to: HTTPStatus.noContent)
, который преобразует Future<User>
в Future<HTTPStatus>
.
И последнее, но не менее важное: мы должны зарегистрировать наш UsersController
на маршрутизаторе. Есть две необходимые вещи, чтобы все работало. Во-первых, наш UsersController
должен соответствовать протоколу RouteCollection
и реализовывать метод func boot(router: Router) throws
следующим образом.
final class UsersController: RouteCollection { // ... func boot(router: Router) throws { let usersRoute = router.grouped("api", "users") usersRoute.get(use: getAllHandler) usersRoute.get(User.parameter, use: getOneHandler) usersRoute.post(use: createHandler) usersRoute.put(User.parameter, use: updateHandler) usersRoute.delete(User.parameter, use: deleteHandler) } }
Внутри этого метода мы сообщаем маршрутизатору, какой путь, HTTP-метод и функция-обработчик должны использоваться для каждой конечной точки. Во-вторых, чтобы правильно зарегистрировать наш UsersController
на маршрутизаторе, переключитесь на Sources/App/routes.swift
и напишите следующие строки.
public func routes(_ router: Router) throws { let usersController = UsersController() try router.register(collection: usersController) }
На этом этапе мы можем запустить наше приложение и проверить реализацию с помощью Почтальона.
Заключение
Вот весь проект.
Хотя это очень простой сервер, замечательно, что Vapor предоставляет прочную структуру и лаконичный интерфейс для написания сервера на Swift. В настоящее время не так много продуктов используют Vapor в качестве серверной части. Однако по мере того, как сообщество растет, а Vapor становится все более надежным, появляется все больше и больше разработчиков, желающих попробовать. Разработчику iOS всегда приятно понимать, что происходит на сервере, с которым мы общаемся. Знание серверной части также полезно при сотрудничестве с разработчиками серверной части, даже если они не используют Vapor или Swift.
Я собираюсь поделиться другими функциями, основанными на реализации этого проекта, в следующих статьях. Если вас также интересуют серверные Swift и Vapor, я предлагаю прочитать raywenderlich.com Server Side Swift with Vapor book. Он не только содержит множество руководств, но также дает подробное объяснение каждой техники. Кроме того, я полностью открыт для обсуждений и отзывов, так что поделитесь, пожалуйста, своими мыслями.