Новые горизонты с серверным Swift

Прочтите этот пост в лучшем формате на моем сайте.

Решив присоединиться к другим конференциям в этом году, самой первой стала dotSwift. Он проходил в большом старинном театре Парижа. Конференция длилась полдня, но все было лучше, чем я предполагал. Были хорошие разговоры о бэкэнд-разработке на Swift. Много услышав об этом и отличной производительности Swift, я решил попробовать. И вот, шаг за шагом, мои первые впечатления.

В Swift есть два основных фреймворка для бэкэнда: Kitura и Vapor. Я сначала выбираю Китуру. Потому что именование методов Китуры было близко к тем, которые я знал по Node.js. Мне было удобнее понимать, что делает каждый метод.

Во-первых, настроить среду было легко, потому что у меня Mac и я активно занимаюсь разработкой приложений для iOS. Итак, Xcode и другие вещи уже были настроены.

Создание серверного проекта Swift означает создание нового пакета Swift с помощью однострочной команды swift package init. Это создает новый структурированный пакет. Но пакеты не исполняемые, и мне нужен был исполняемый проект для запуска моего бэкэнда. Создание main.swift файла в каталоге Sources дало мне такую ​​возможность. На данный момент моя текущая структура папок была такой:

SwiftBackend
  .gitignore
  Package.swift
  |--Sources
  main.swift
  |--Tests

Пришло время расположить Package.swift и добавить Китуру. Вот файл Package.swift с Китурой, добавленной в качестве зависимости:

import PackageDescription
let package = Package(
  name: "SwiftBackend",
  dependencies: [
    .Package(url: "https://github.com/IBM-Swift/Kitura.git", majorVersion: 1, minor: 4)
  ]
)

swift build команда устанавливает зависимости и строит проект. После добавления новой зависимости всегда логично построить проект и посмотреть, работает ли он.

Следующий шаг - настройка конечной точки. Базовый пример в руководстве Китуры:

import Kitura
let router = Router()
router.get("/") { request, response, next in
  response.send("Hello, World!")
  next()
}
Kitura.addHTTPServer(onPort: 8090, with: router)
Kitura.run()

Я добавил этот код в main.swift файл. Когда я запускал проект локально, я должен был увидеть Hello, World! когда я ввожу localhost:8090 через браузер (или делаю запрос на получение через Postman и т. д.). Чтобы запустить этот проект, мне сначала нужно было собрать его через swift build, и эта команда создала для меня исполняемый файл под .build/debug/SwiftBackend. Я только что запустил исполняемый файл, набрав в командной строке .build/debug/SwiftBackend. На этом этапе я смог отправить запрос и увидеть текст «Hello, World!» в браузере. Но в консоли я не видел никаких логов по этим запросам.

HeliumLogger вошел в этот момент. Это компонент регистратора, доступный как отдельный модуль Swift. Я добавил этот модуль в файл Package.swift , добавив в качестве новой зависимости. На тот момент мой Package.swift файл был таким:

import PackageDescription
let package = Package(
  name: "SwiftBackend",
  dependencies: [
    .Package(url: "https://github.com/IBM-Swift/Kitura.git", majorVersion: 1, minor: 4),
    .Package(url: "https://github.com/IBM-Swift/HeliumLogger.git", majorVersion: 1, minor: 4)
  ]
)

И я снова выполнил команду swift build, чтобы установить HeliumLogger. После этого мне нужно было импортировать HeliumLogger и использовать его в main.swift файле. Это была всего одна строчка. Мой main.swift файл стал таким:

import Kitura
import HeliumLogger
HeliumLogger.use()
let router = Router()
router.get("/") { request, response, next in
  response.send("Hello, World!")
  next()
}
Kitura.addHTTPServer(onPort: 8090, with: router)
Kitura.run()

После создания и повторного запуска проекта я смог увидеть журналы запросов в консоли.

В качестве следующего шага я хотел подключить базу данных к моему внутреннему API. Я использовал CouchDB, потому что есть пакет Kitura-CouchDB.

После добавления пакета CouchDB в мой Package.swift файл он стал таким:

import PackageDescription
let package = Package(
  name: "SwiftBackend",
  dependencies: [
    .Package(url: "https://github.com/IBM-Swift/Kitura.git", majorVersion: 1, minor: 4),
    .Package(url: "https://github.com/IBM-Swift/HeliumLogger.git", majorVersion: 1, minor: 4),
    .Package(url: "https://github.com/IBM-Swift/Kitura-CouchDB.git", majorVersion: 1, minor: 4)
  ]
)

Я снова выполнил команду swift build, чтобы установить новый пакет.

При реализации операций с базой данных я хотел создать структуру, не позволяющую делать все в main.swift файле. Сначала я создал структуру User как объект модели. Моей основной целью операций с базой данных было добавление этого User объекта в базу данных CouchDB в качестве документа. Простая модель, простота в эксплуатации. Итак, структура User была такой:

// User.swift
import Foundation
public struct User {
  let name: String
  let identifier: String
}

Итак, как я уже сказал, моей основной целью было добавить документ в базу данных CouchDB. Я создал DatabaseInteraction структуру с одним методом для достижения этой цели. Я планировал добавить в эту структуру все операции с базой данных. Вот структура DatabaseInteraction:

// DatabaseInteraction.swift
import Foundation
import CouchDB
import SwiftyJSON
public struct DatabaseInteraction {
  var db: Database
  public init(db: Database) {
    self.db = db
  }
func addNewUser(_ user: User, handler: @escaping (String?, String?, JSON?, NSError?) -> ()) {
    let userDict: [String: Any] = [
      "name": user.name,
      "identifier": user.identifier
    ]
    let userJSON = JSON(userDict)
    db.create(userJSON) {  (id, revision, doc, error) in
      if let error = error {
        handler(nil, nil, nil, error)
        return
      } else {
        handler(id, revision, doc, nil)
      }
    }
  }
}

Я также отделил методы HTTP-запроса от маршрутизаторов. Во-первых, я создал UserRouter для обработки HTTP-запросов пользователей и написал только один метод публикации для получения пользовательских данных в теле. КлассUserRouter выглядит следующим образом:

// UserRouter.Swift
import Foundation
import Kitura
import CouchDB
import SwiftyJSON
public class UserRouter {
  var db: DatabaseInteraction
public init(db: DatabaseInteraction) {
    self.db = db
  }
  
  public func bindAll(to router: Router) {
    addCreateUser(to: router)
  }
private func addCreateUser(to router: Router) {
    router.post("/user/", handler: { req, res, next in
      guard let parsedBody = req.body else {
        res.status(.badRequest)
        next()
        return
      }
      switch(parsedBody) {
        case .json(let jsonBody):
          let name = jsonBody["name"].string ?? ""
          let user = User(name: name, identifier: "\(name.characters.count)")
          self.db.addNewUser(user) { (id, revision, doc, error) in
            if let error = error {
              res.status(.internalServerError)
              next()
            } else {
              res.status(.OK)
              if let doc = doc {
                res.send(json: doc)
              } else {
                res.send("Something is wrong in the doc")
              }
              next()
            }
          }
        default:
          res.status(.badRequest)
          next()
      }
    })
  }
}

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

// BackendRouter.swift
import Foundation
import Kitura
public class BackendRouter {
  public let router = Router()
  var db: DatabaseInteraction
  public init(db: DatabaseInteraction) {
    self.db = db
    router.get("/status") { req, res, callNextHandler in
      res.status(.OK).send("Everything is working")
      callNextHandler()
    }
    
    router.all("*", middleware: BodyParser()) 
    
    self.routeToUser()
  }
  
  func routeToUser() {
    let user = UserRouter(db: self.db) 
    user.bindAll(to: self.router)
  }
}

Напоследок я их все соединил в файл main.swift.

// main.swift
import Kitura
import HeliumLogger
import CouchDB
HeliumLogger.use()
let connProperties = ConnectionProperties(
    host: "127.0.0.1",  // httpd address
    port: 5984,         // httpd port
    secured: false,     // https or http
    username: "admin",  // admin username
    password: "password"// admin password
)
let db = Database(connProperties: connProperties, dbName: "swift_backend_test_db")
let databaseInteraction = DatabaseInteraction(db: db)
let app = MainRouter(db: databaseInteraction)
Kitura.addHTTPServer(onPort: 8090, with: app.router)
Kitura.run()

Итак, давайте посмотрим, как сейчас выглядит общая структура папок.

SwiftBackend
  .gitignore
  |--Sources
    BackendRouter.swift
    DatabaseInteraction.swift
    main.swift
    User.swift
    UserRouter.swift
  |--Tests
  Package.swift

После написания кода пришло время создать API и отправить запрос через Postman. Я добился успеха после некоторых испытаний. (Примечание: будьте осторожны со свойствами соединения и избеганием замыканий). Конечно, в коде есть что улучшить. Но все эти коды были предназначены для создания рабочего бэкэнд API, разработанного на Swift.

В следующем посте я сделал этот проект тестируемым и Dockerized. Есть некоторые моменты, на которые следует обратить внимание при тестировании.

В качестве последнего шага я попытаюсь загрузить его в облако. Но мое первое впечатление было действительно хорошим. Должен сказать, что меня убедили поработать на бэкэнд в Swift. Это очень просто. Я работаю с SublimeText вместо Xcode. Таким образом, я могу понять каждую написанную мной строчку без автозаполнения. Если вы работаете со Swift, вам обязательно стоит попробовать создать некоторые серверные API с помощью Swift.

Если он вам понравился, нажмите чуть ниже, чтобы порекомендовать его своим друзьям. Если вы хотите быть в курсе новых сообщений в блоге, вы можете перейти на мой личный веб-сайт и подписаться на мою рассылку. Вы можете подписаться на меня в Твиттере и GitHub.