Статьи

AngularJS в Акке и Спрей

Этот пост посвящен постам Akka и Spray и расширяет его, добавляя обработчик загрузки файлов. Это позволяет нам загружать изображения в аккаунт пользователя. (Обратите внимание, что, как обычно, код Akka не делает ничего особенного с загруженными файлами; он просто демонстрирует, что мы можем принять multipart/form-dataкодированный POST.)

загрузка

Это наше приложение AngularJS в браузере. Мы нажимаем кнопку « Добавить файлы…» , чтобы добавить (изображения), которые будут загружены. Когда пользователи нажимают кнопку « Начать загрузку» , мы начинаем загрузку параллельно, но каждый POST содержит один файл. Кнопка Отмена загрузки очищает форму.

Спрей код

Запросы собираются register/image, поэтому нам нужно добавить соответствующий «обработчик» RegistrationServiceдля обработки POST по этому пути, чтобы демонтировать запрос как MultipartFormData. Затем мы completeзапрос с соответствующим ответом.

class RegistrationService(registration: ActorRef)
                         (implicit executionContext: ExecutionContext)
  extends Directives with DefaultJsonFormats {

  import akka.pattern.ask
  import scala.concurrent.duration._
  implicit val timeout = Timeout(2.seconds)

  implicit val userFormat = jsonFormat4(User)
  implicit val registerFormat = jsonFormat1(Register)
  implicit val registeredFormat = jsonObjectFormat[Registered.type]
  implicit val notRegisteredFormat = jsonObjectFormat[NotRegistered.type]

  implicit object EitherErrorSelector  
    extends ErrorSelector[NotRegistered.type] {
    def apply(v: NotRegistered.type): StatusCode = StatusCodes.BadRequest
  }

  val route =
    path("register") {
      post {
        handleWith { ru: Register => (registration ? ru).
          mapTo[Either[NotRegistered.type, Registered.type]] }
      }
    } ~
    path("register" / "image") {
      post {
        entity(as[MultipartFormData]) { data =>
          complete {
            data.fields.get("files[]") match {
              case Some(imageEntity) =>
                val size = imageEntity.entity.buffer.length
                println(s"Uploaded $size")
                "OK"
              case None =>
                println("No files")
                "Not OK"
            }
          }
        }
      }
    }

}

Это весь исходный код, RegistrationServiceчтобы показать вам, куда вписывается новый код. Новый бит — это использование entity(as[A]) { a => ... }, которое принимает опубликованное HttpEntity, находит экземпляр класса Unmarshallerтипов для типа A. Когда демаршаллинг завершается успешно, он применяет данную функцию { a => ... }для завершения запроса. В нашем случае имеем:

path("register" / "image") {
  post {
    entity(as[MultipartFormData]) { data =>
      complete {
        data.fields.get("files[]") match {
          case Some(imageEntity) =>
            val size = imageEntity.entity.buffer.length
            println(s"Uploaded $size")
            "OK"
          case None =>
            println("No files")
            "Not OK"
        }
      }
    }
  }
}

Это означает, что в POST для register/imageмы демонтируем сущность как MultipartFormDataи применяем функцию:

{ data =>
  complete {
    data.fields.get("files[]") match {
      case Some(imageEntity) =>
        val size = imageEntity.entity.buffer.length
        println(s"Uploaded $size")
        s"""{"size":$size}"""
      case None =>
        println("No files")
        """{"size":0}"""
    }
  }
}

К успешно раскрытой ценности. В этой функции мы completeпросьбу, в конечном счете , возвращающих либо s"""{"size":$size}"""или """{"size":0}""".

Гинденбург

Stringly-типизированных

Ручное конструирование JSON, XML, чего угодно, ужасная идея . Давайте избавиться от тех , s"""{"size":$size}"""и """{"size":0}"""строки и заменить их с удобным классом случае.

case class ImageUploaded(size: Int)

Мы можем легко определить JsonWriterэкземпляр класса типов для ImageUploadedтипа, добавив неявное значение типа JsonWriter[ImageUploaded]; и это именно то, что jsonFormatделают функции.

implicit val imageUploadedFormat = jsonFormat1(ImageUploaded)

Out со строками

Итак, мы добавляем класс case и JsonWriter[ImageUpload]экземпляр класса типов, что позволяет нам избавиться от Strings в completeфункции.

class RegistrationService(registration: ActorRef)
                         (implicit executionContext: ExecutionContext)
  extends Directives with DefaultJsonFormats {

  case class ImageUploaded(size: Int)
  implicit val imageUploadedFormat = jsonFormat1(ImageUploaded)

  ...

  val route =
    ...
    path("register" / "image") {
      post {
        entity(as[MultipartFormData]) { data =>
          complete {
            data.fields.get("files[]") match {
              case Some(imageEntity) =>
                val size = imageEntity.entity.buffer.length
                println(s"Uploaded $size")
                ImageUploaded(size)
              case None =>
                println("No files")
                ImageUploaded(0)
            }
          }
        }
      }
    }

}

Теперь это лучше. Там нет Strings, и мы прекрасно справляемся с загрузкой файлов. К сожалению, все еще слишком много заметок . Разобрав postобработчик, имеем:

entity(as[MultipartFormData]) { data =>
  complete {
    ImageUploaded(...)
  }
}

entity(as[A]) { a: A => complete { ... } }Может быть заменен handleWith: тот же код , который мы используем в registerдолжности. Итак, окончательный код просто:

path("register" / "image") {
  post {
    handleWith { data: MultipartFormData =>
      data.fields.get("files[]") match {
        case Some(imageEntity) =>
          val size = imageEntity.entity.buffer.length
          println(s"Uploaded $size")
          ImageUploaded(size)
        case None =>
          println("No files")
          ImageUploaded(0)
      }
    }
  }
}

Запуск приложения AngularJS

Теперь, когда у нас есть приложение Spray, давайте разберемся с приложением JavaScript. Его источник в src/main/angular. Основными составляющими являются index.htmlи js/app.js. Нельзя просто открыть index.htmlфайл в браузере. Другие ресурсы (скрипты Java, таблицы стилей) не будут загружаться должным образом, но даже если они это сделают, браузер откажется вызывать наш сервер Spray, поскольку местоположения не совпадают. Наш сервер Spray работает http://localhost:8080, но местоположение приложения AngularJS будет file:///.../index.html.

Чтобы мы могли его протестировать, нам нужно (легко) обслуживать приложение AngularJS, а также наше приложение Spray в одном месте. В этом посте я расскажу только о настройке разработки, и у меня останется место, чтобы рассказать о правильной настройке AWS в будущем.

Настройка разработки

Давайте использовать Apache для обслуживания приложения AngularJS в, http://localhost/~USER/angularи давайте разместим приложение Spray (или, фактически, все, что прослушивает порт 8080) в http://localhost/~USER/app.

Чтобы это произошло, мы включим домашние страницы для каждого пользователя Apache и мы зайдем ProxyPassи ProxyPassReverseдирективу. На моей машине конфигурация для $USERжизни в /etc/apache2/users/$USER.conf.

<Directory "/Users/$USER/Sites/">
	Options Indexes Multiviews +FollowSymLinks
	AllowOverride AuthConfig Limit
	Order allow,deny
	Allow from all
</Directory>

ProxyPass /~$USER/app/ http://localhost:8080/
ProxyPassReverse /~$USER/app/ http://localhost:8080/

Конечно, вы не можете использовать $USER; Вы должны заменить его своим реальным именем пользователя. Я не хочу , чтобы скопировать в src/main/angularкаталог в ~/Sitesкаталог, поэтому я добавил +FollowSymLinksдирективу и создал символическую связь ~/Sitesс точкой туда , где у меня есть src/main/angular. В моем случае, список ~/SitesIS

~/Sites$ ls -la
total 16
lrwxr-xr-x  ... angular -> /.../akka-spray/src/main/angular
-rw-r--r--  ... index.html

Теперь, после добавления этого файла и запуска (или перезапуска) Apache sudo apachectl start(или sudo apachectl restart), вы готовы увидеть приложение, перейдя по ссылке http://localhost/~USER/angular.

Итак, теперь вы готовы к работе. При открытии http://localhost/~USER/angularотображается приложение AngularJS, куда вы можете добавить столько файлов, сколько захотите, и нажатие кнопки « Начать загрузку» отправляет файлы в приложение Spray. Приложение Spray бесцеремонно печатает размер файла. Я оставлю некоторые интересные логические обработки в качестве упражнения для читателей.

Как обычно, исходный код находится по адресу https:// github.com / eigengo / activator-akka-spray ; не стесняйтесь сообщать о проблемах или присылать свои материалы.