Я очень рад, что 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. Он не только содержит множество руководств, но также дает подробное объяснение каждой техники. Кроме того, я полностью открыт для обсуждений и отзывов, так что поделитесь, пожалуйста, своими мыслями.