- PVSM.RU - https://www.pvsm.ru -
Продолжим говорить о 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.
Оба типа параметризуется пользовательским типом, декодер которого должен быть указан при формировании запроса.
Выполнить запрос можно при помощи функций:
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.
В процессе выполнения запросов, интерфейс часто приходится блокировать полностью или частично, а также сообщать об ошибках выполнения запросов при их наличии. В общем случае состояние запроса можно описать в виде:
Для подобного описания существует пакет RemoteData [14]. По началу активно его использовал, но со временем наличие дополнительного типа WebData [15] стало излишним, а работа с ним утомительной. Вместо этого пакета появились следующие правила:
Описанная схема не сильно лучше варианта с пакетом RemoteData, как показывает практика. Если у кого-то есть иные варианты, поделитесь в комментариях.
К состоянию выполнения запроса стоит отнести прогресс загрузки из пакета Http.Progress [16].
Рассмотрим варианты последовательностей задач, которые часто встречаются в разработке:
Последовательные зависимые задачи уже рассматривался выше, приведу в этом разделе общее описание и подходы к реализации.
Последовательность задач прерывается при первом провале и возвращается ошибка. В случае успеха возвращается некоторая комбинация результатов:
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
Нажмите здесь для печати.