- PVSM.RU - https://www.pvsm.ru -

Elm. Удобный и неловкий. Http, Task

Продолжим говорить о Elm 0.18 [1].

Elm. Удобный и неловкий [2]
Elm. Удобный и неловкий. Композиция [3]
Elm. Удобный и неловкий. Json.Encoder и Json.Decoder [4]

В этой статье рассмотрим вопросы взаимодействия с серверной частью.

Выполнение запросов

Примеры простых запросов можно найти в описании к пакету Http [5].

Тип запроса — Http.Request a [6].
Тип результата запроса — Result Http.Error [7] a.
Оба типа параметризуется пользовательским типом, декодер которого должен быть указан при формировании запроса.

Выполнить запрос можно при помощи функций:

  1. Http.send;
  2. Http.toTask.

Http.send позволять выполнить запрос и по его завершению передает сообщение в функцию update указанное в первом аргументе. Сообщение несет с собой данные о результате запроса.

Http.toTask позволяет из запроса создать Task [8], который можно выполнить. Использование функции Http.toTask, на мой взгляд, является наиболее удобной, так как экземпляры Task можно объединяться между собой при помощи различных функций [9], например Task.map2 [10].

Рассмотрим на примере. Допустим, для сохранения данных пользователя необходимо выполнить два последовательных зависимых запроса. Пусть это будет создание поста от пользователя и сохранение фотографий к нему (используется некий CDN).

Сначала рассмотрим реализацию для случая Http.Send. Для этого нам понадобятся две функции:

save : UserData -> Request Http.Error UserData
save userData =
  Http.post “/some/url” (Http.jsonBody (encodeUserData userData)) decodeUserData

saveImages : Int -> Images -> Request Http.Error CDNData
saveImages id images =
  Http.post (“/some/cdn/for/” ++ (toString id)) (imagesBody images) decodedCDNData

Типы UserData и CDNData описывать не будет, для примера не важны. Функция encodeUserData является энкодером. saveImages принимает идентификатор пользовательских данных, который используется при формировании адреса, и список фотографий. Функция imagesBody формирует тело запроса типа multipart/form-data [11]. Функции decodeUserData и decodedCDNData декодируют ответ сервера для пользовательских данных и результат запроса к CDN соответственно.

Далее нам понадобятся два сообщения, результаты запроса:

type Msg
  = DataSaved (Result Http.Error UserData)
  | ImagesSaved (Result Http.Error CDNData)

Предположим, где-то в реализации функции update существует участок кода, который выполняет сохранение данных. Например, это может выглядеть так:

update : Msg -> Model -> (Model, Cmd Msg)
update msg model
  case Msg of

    ClickedSomething ->
      (model, Http.send DataSaved (save model.userData))

В данном случае создается запрос и помечается сообщением DataSaved. Далее это сообщение принимается:

update : Msg -> Model -> (Model, Cmd Msg)
update msg model
  case Msg of

    DataSaved (Ok userData) ->
      ( {model | userData = userData}, Http.send  ImagesSaved (saveImages userData.id model.images))

    DataSaved (Err reason) ->
      (model, Cmd.None)

В случае успешного сохранения, обновляем данные в модели и вызываем запрос на сохранение фотографий куда передаем полученный идентификатор пользовательских данных. Обработка сообщения ImagesSaved будет аналогична DataSaved, необходимо будет обработать успешный и провальный случаи.

Теперь рассмотрим вариант с использование функции Http.toTask. Используя описанные функции определим новую функцию:

saveAll : UserData -> Images -> Task Http.Error (UserData, CDNData)
saveAll : userData images =
  save model.userData
    |> Http.toTask
    |> Task.andThen (newUserData ->
      saveImages usersData.id images 
        |> Http.toTask
        |> Task.map (newImages -> 
           (userData, newImages)
        }
    )

Теперь используя возможность комбинировать задачи, можем получить все данные в виде одного сообщения:

type Msg
  = Saved (Result Http.Error (UserData, CDNData))

update : Msg -> Model -> (Model, Cmd Msg)
update msg model
  case Msg of

    ClickedSomething ->
      (model, Task.attempt Saved (saveAll model.userData model.images))

   DataSaved (Ok (userData, images)) ->
      ( {model | userData = userData, images = images}, Cmd.none)

    DataSaved (Err reason) ->
      (model, Cmd.None)

Для выполнения запросов используем функцию Task.attempt [12], которая позволяет выполнить задачу. Не стоит путать с функцией Task.perform [13]. Task.perform — позволяет выполнить задачи, которые не могут провалиться. Task.attempt — выполняет задачи, которые могут провалиться.

Такой подход получается более компактным с точки зрения количества сообщений, сложности функции update и позволяет держать логику локальнее.

В своих проектах, в приложениях и компонентах часто создаю модуль Commands.elm, в котором описываю функции взаимодействия с серверной частью с типом … -> Task Http.Error a.

Состояние выполнения запросов

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

  1. запрос не выполнен;
  2. запрос выполняется;
  3. запрос выполнен успешно;
  4. запрос провален.

Для подобного описания существует пакет RemoteData [14]. По началу активно его использовал, но со временем наличие дополнительного типа WebData [15] стало излишним, а работа с ним утомительной. Вместо этого пакета появились следующие правила:

  1. все данные от сервера объявлять типом Maybe. В этом случае Nothing, обозначает отсутствие данных;
  2. объявлять в модели приложения или компоненты атрибут loading типа Int. Параметр хранит количество выполняемых запросов. Единственное неудобство этого подхода, необходимость инкрементировать и декрементировать атрибут в начале запроса и по завершению соответственно;
  3. объявлять в модели приложения или компоненты атрибут errors типа List String. Данный атрибут используется для хранения данных об ошибке.

Описанная схема не сильно лучше варианта с пакетом RemoteData, как показывает практика. Если у кого-то есть иные варианты, поделитесь в комментариях.

К состоянию выполнения запроса стоит отнести прогресс загрузки из пакета Http.Progress [16].

Последовательность задач

Рассмотрим варианты последовательностей задач, которые часто встречаются в разработке:

  1. последовательные зависимые задачи;
  2. последовательные независимые задачи;
  3. параллельные независимые задачи.

Последовательные зависимые задачи уже рассматривался выше, приведу в этом разделе общее описание и подходы к реализации.

Последовательность задач прерывается при первом провале и возвращается ошибка. В случае успеха возвращается некоторая комбинация результатов:

someTaskA
  |> Task.andThen (resultA ->
    someTaskB 
      |> Task.map (resultB ->
        (resultA, resultB)
      )
  )

Данный код создает задачу типа Task error (a, b), которая может быть выполнена позже.

Функция Task.andThen [17] позволяет передать новую задачу на выполнение в случае успешного завершения предыдущей. Функция Task.map [18] позволяет преобразовать дынные результата выполения в случае успеха.

Возможны варианты, когда успешного выполнения задаче будет недостаточно и необходимо проверить согласованность данных. Допустим, что идентификаторы пользователей совпадают:

someTaskA
  |> Task.andThen (resultA ->
    someTaskB 
      |> Task.andThen (resultB ->
        case resultA.userId == resultB.userId of
          True -> 
            Task.succeed (resultA, resultB)

          False -> 
            Task.fail “User is not the same”
      )
  )

Стоит отметить, что вместо функции Task.map используется функция Task.andThen и успешность выполнения второй задачи мы определяем самостоятельно при помощи функций Task.succeed [19] и Task.fail [20].

Если одна из задач может провалиться и это приемлемо, то необходимо использовать функцию Task.onError [21] для указания значения в случае ошибки:

someTaskA
  |> Task.onError (msg -> Task,succeed defaultValue)
  |> Task.andThen (resultA ->
    someTaskB 
      |> Task.map (resultB ->
        (resultA, resultB)
      )
  )

Вызов функции Task.onError должен быть объявлен непосредственно после объявления задачи.

Последовательные независимые запросы можно выполнять при помощи функций Task.mapN. Которые позволяют объединить несколько результатов задач в один. Первая упавшая задача прерывает выполнение всей цепочки, поэтому для значений по умолчанию используйте функцию Task.onError. Также ознакомьтесь с функцией Task.sequence [22], она позволяет выполнить серию однотипных задач.

Параллельные задачи в текущей реализации языка не описаны. Их реализация возможна на уровне приложения или компоненты через обработку событий в функции update. Вся логика остается на плечах разработчика.

Автор: vturchaninov

Источник [23]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/javascript/294481

Ссылки в тексте:

[1] Elm 0.18: http://elm-lang.org

[2] Elm. Удобный и неловкий: https://habr.com/post/424215/

[3] Elm. Удобный и неловкий. Композиция: https://habr.com/post/424341/

[4] Elm. Удобный и неловкий. Json.Encoder и Json.Decoder: https://habr.com/post/424437/

[5] Http: https://package.elm-lang.org/packages/elm-lang/http/1.0.0

[6] Http.Request a: https://package.elm-lang.org/packages/elm-lang/http/1.0.0/Http#Request

[7] Http.Error: https://package.elm-lang.org/packages/elm-lang/http/1.0.0/Http#Error

[8] Task: https://package.elm-lang.org/packages/elm-lang/core/5.1.1/Task#Task

[9] помощи различных функций: https://package.elm-lang.org/packages/elm-lang/core/5.1.1/Task

[10] Task.map2: https://package.elm-lang.org/packages/elm-lang/core/5.1.1/Task#map2

[11] multipart/form-data: https://package.elm-lang.org/packages/elm-lang/http/1.0.0/Http#multipartBody

[12] Task.attempt: https://package.elm-lang.org/packages/elm-lang/core/5.1.1/Task#attempt

[13] Task.perform: https://package.elm-lang.org/packages/elm-lang/core/5.1.1/Task#perform

[14] RemoteData: https://package.elm-lang.org/packages/krisajenkins/remotedata/5.0.0/RemoteData

[15] WebData: https://package.elm-lang.org/packages/krisajenkins/remotedata/5.0.0/RemoteData#WebData

[16] Http.Progress: https://package.elm-lang.org/packages/elm-lang/http/1.0.0/Http-Progress

[17] Task.andThen: https://package.elm-lang.org/packages/elm-lang/core/5.1.1/Task#andThen

[18] Task.map: https://package.elm-lang.org/packages/elm-lang/core/5.1.1/Task#map

[19] Task.succeed: https://package.elm-lang.org/packages/elm-lang/core/5.1.1/Task#succeed

[20] Task.fail: https://package.elm-lang.org/packages/elm-lang/core/5.1.1/Task#fail

[21] Task.onError: https://package.elm-lang.org/packages/elm-lang/core/5.1.1/Task#onError

[22] Task.sequence: https://package.elm-lang.org/packages/elm-lang/core/5.1.1/Task#sequence

[23] Источник: https://habr.com/post/424979/?utm_campaign=424979