В моих предыдущих статьях мы обсуждали различные термины Elixir и писали большой объем кода. Однако мы не обсуждали, как структурировать и организовать ваш код так, чтобы его было легко поддерживать и выпускать.
Приложения очень распространены для Erlang и Elixir и используются для создания компонентов многократного использования, которые ведут себя как отдельные устройства. Одно приложение может иметь свое собственное дерево контроля и конфигурацию, и оно может полагаться на другие приложения, которые доступны либо локально, либо на некотором удаленном сервере. В общем, работа с приложениями не так сложна, и люди, пришедшие, скажем, из мира Ruby, найдут много знакомых концепций.
В этой статье вы узнаете, что такое приложения, как их можно создавать, как указывать и устанавливать зависимости и как предоставлять значения среды. В конце статьи мы немного потренируемся и создадим веб-калькулятор.
В этой статье я буду использовать Elixir 1.5 (он был выпущен пару месяцев назад ), но все объясненные концепции должны относиться и к версии 1.4.
Приложения?
Некоторые могут утверждать, что термин «приложение» не очень уместен, потому что в Erlang и Elixir это фактически означает компонент или некоторый код, который имеет множество зависимостей. Само приложение также может быть использовано как зависимость — в мире Ruby мы бы назвали его «жемчужиной».
В общем, приложения очень распространены в Elixir и позволяют создавать многократно используемые компоненты, а также обеспечивают простое управление зависимостями. Они состоят из одного или нескольких модулей с нулевой или более зависимостями и описываются файлом ресурсов приложения. Этот файл содержит информацию об имени приложения, версии, его модулях, зависимостях и некоторых других вещах. Вы можете создать файл ресурсов вручную, но это гораздо проще сделать с помощью инструмента микширования, который также подготовит для вас правильную структуру папок.
Итак, давайте посмотрим, как мы можем создать новое приложение Elixir!
Новое приложение
Чтобы создать новое приложение, все, что вам нужно сделать, это запустить следующую команду:
1
|
mix new app_name
|
Мы также можем предоставить флаг --sup
чтобы создать для нас пустой супервизор. Давайте создадим новое приложение под названием Sample
следующим образом:
1
|
mix new sample —sup
|
Эта команда создаст для вас пример каталога с несколькими файлами и папками внутри. Позвольте мне быстро провести вас через них:
- Папка config содержит единственный файл config.exs, который, как вы можете догадаться, предоставляет конфигурацию для приложения. Изначально в нем есть несколько полезных комментариев, но нет конфигурации. Обратите внимание, что, кстати, конфигурация, представленная в этом файле, ограничена только самим приложением. Если вы загружаете приложение как зависимость, его config.exs будет эффективно игнорироваться.
- lib — это основная папка приложения, содержащая файл sample.ex и образец папки с файлом application.ex . application.ex определяет модуль обратного вызова с функцией
start/2
которая создает пустой супервизор. - test — это папка, содержащая автоматические тесты для приложения. Мы не будем обсуждать автоматизированные тесты в этой статье.
- mix.exs — это файл, который содержит всю необходимую информацию о приложении. Здесь есть несколько функций. Внутри функции
project
вы указываете имя приложения (в виде атома), версию и среду. Функцияapplication
содержит информацию о обратном вызове модуля приложения и зависимостях времени выполнения. В нашем случаеSample.Application
устанавливается как обратный вызов модуля приложения (который может рассматриваться как основная точка входа), и он должен определять функциюstart/2
. Как уже упоминалось выше, эта функция уже была создана для нас инструментомmix
. Наконец, функцияdeps
перечисляет зависимости времени сборки.
зависимости
Очень важно различать зависимости времени выполнения и времени сборки. Зависимости времени сборки загружаются инструментом mix
во время компиляции и в основном компилируются в ваше приложение.
Их можно получить из службы, например, GitHub, или с веб-сайта hex.pm , внешнего менеджера пакетов, который хранит тысячи компонентов для Elixir и Erlang. Зависимости времени выполнения запускаются до запуска приложения. Они уже скомпилированы и доступны для нас.
Есть несколько способов указать зависимости времени сборки в файле mix.exs . Если вы хотите использовать приложение с сайта hex.pm, просто скажите:
1
|
{:dependency_name, «~> 0.0.1»}
|
Первый аргумент — это всегда атом, представляющий имя приложения. Второе — это требование, версия, которую вы хотите использовать — она анализируется модулем Version . В этом примере ~>
означает, что мы хотим загрузить как минимум версию 0.0.1
или выше, но менее 0.1.0
. Если мы говорим ~> 1.0
, это означает, что мы хотели бы использовать версию, большую или равную 1.0
но меньшую, чем 2.0
. Также доступны такие операторы, как ==
, >
, <
, >=
и <=
.
Также можно напрямую указать параметр :git
или a :path
:
1
2
|
{:gettext, git: «https://github.com/elixir-lang/gettext.git», tag: «0.1»}
{:local_dependency, path: «path/to/local_dependency»}
|
Существует также ярлык :github
который позволяет нам указывать только имя владельца и имя репо:
1
|
{:gettext, github: «elixir-lang/gettext»}
|
Чтобы скачать и скомпилировать все зависимости, запустите:
1
|
mix deps.get
|
Это установит Hex-клиент, если у вас его нет, а затем проверит, нужно ли обновить какие-либо из зависимостей. Например, вы можете указать Poison — решение для анализа JSON — как зависимость:
1
2
3
4
5
|
defp deps do
[
{:poison, «~> 3.1»}
]
end
|
Затем запустите:
1
|
mix deps.get
|
Вы увидите похожий вывод:
1
2
3
4
5
6
|
Running dependency resolution…
Dependency resolution completed:
poison 3.1.0
* Getting poison (Hex package)
Checking package (https://repo.hex.pm/tarballs/poison-3.1.0.tar)
Fetched package
|
Poison теперь скомпилирован и доступен на вашем ПК. Более того, файл mix.lock будет создан автоматически. Этот файл содержит точные версии зависимостей, которые будут использоваться при загрузке приложения.
Чтобы узнать больше о зависимостях, выполните следующую команду:
1
|
mix help deps
|
Поведение снова
Приложения — это поведение, как GenServer и супервизоры , о которых мы говорили в предыдущих статьях. Как я уже упоминал выше, мы предоставляем модуль обратного вызова внутри файла mix.exs следующим образом:
1
2
3
4
5
|
def application do
[
mod: {Sample.Application, []}
]
end
|
Sample.Application
— это имя модуля, тогда как []
может содержать список аргументов для передачи в функцию start/2
. Функция start/2
должна быть реализована для правильной загрузки приложения.
Application.ex содержит модуль обратного вызова, который выглядит следующим образом:
01
02
03
04
05
06
07
08
09
10
11
|
defmodule Sample.Application do
use Application
def start(_type, _args) do
children = [
]
opts = [strategy: :one_for_one, name: Sample.Supervisor]
Supervisor.start_link(children, opts)
end
end
|
Функция start/2
должна возвращать {:ok, pid}
(с необязательным состоянием в качестве третьего элемента) или {:error, reason}
.
Стоит также упомянуть, что приложениям вообще не требуется модуль обратного вызова. Это означает, что функция приложения внутри файла mix.exs может стать действительно минималистичной:
1
2
3
|
def application do
[]
end
|
Такие приложения называются библиотечными приложениями . Они не имеют никакого дерева контроля, но все же могут использоваться в качестве зависимостей другими приложениями. Одним из примеров применения библиотеки может быть Poison, который мы указали в качестве зависимости в предыдущем разделе.
Запуск приложения
Самый простой способ запустить ваше приложение — запустить следующую команду:
1
|
iex -S mix
|
Вы увидите вывод, похожий на этот:
1
2
|
Compiling 2 files (.ex)
Generated sample app
|
Каталог _build будет создан внутри папки с примерами . Он будет содержать файлы .beam, а также некоторые другие файлы и папки.
Если вы не хотите запускать оболочку Elixir, запустите другой вариант:
1
|
mix run
|
Проблема, однако, в том, что приложение остановится, как только функция start
завершит свою работу. Следовательно, вы можете предоставить ключ --no-halt
чтобы приложение продолжало работать так долго, как это необходимо:
1
|
mix run —no-halt
|
То же самое может быть достигнуто с помощью команды elixir
:
1
|
elixir -S mix run —no-halt
|
Обратите внимание, однако, что приложение остановится, как только вы закроете терминал, где была выполнена эта команда. Этого можно избежать, запустив приложение в отдельном режиме:
1
|
elixir -S mix run —no-halt —detached
|
Прикладная среда
Иногда вы можете захотеть, чтобы пользователь приложения установил какой-либо параметр до его фактической загрузки. Это полезно, когда, например, пользователь должен иметь возможность контролировать, какой порт должен прослушивать веб-сервер. Такие параметры могут быть указаны в прикладной среде, которая является простым хранилищем ключей в памяти.
Чтобы прочитать какой-либо параметр, используйте fetch_env/2
которая принимает приложение и ключ:
1
|
Application.fetch_env(:sample, :some_key)
|
Если ключ не может быть найден, возвращается :error
атома. Есть также fetch_env!/2
которая вместо этого вызывает ошибку, и get_env/3
которая может предоставить значение по умолчанию.
Чтобы сохранить параметр, используйте put_env/4
:
1
|
Application.put_env(:sample, :key, :value)
|
Четвертое значение содержит параметры и не требует установки.
Наконец, чтобы удалить ключ, используйте функцию delete_env/3
:
1
|
Application.delete_env(:sample, :key)
|
Как мы предоставляем ценность для окружающей среды при запуске приложения? Ну, такие параметры устанавливаются с --erl
ключа --erl
следующим образом:
1
|
iex —erl «-sample key value» -S mix
|
Затем вы можете легко получить значение:
1
|
Application.get_env :sample, :key # => :value
|
Что если пользователь забудет указать параметр при запуске приложения? Ну, скорее всего, нам нужно предоставить значение по умолчанию для таких случаев. Есть два возможных места, где вы можете сделать это: внутри config.exs или внутри файла mix.exs .
Первый вариант является предпочтительным, поскольку config.exs — это файл, который фактически предназначен для хранения различных параметров конфигурации. Если в вашем приложении много параметров среды, вам обязательно следует использовать config.exs :
1
2
|
use Mix.Config
config :sample, key: :value
|
Однако для небольших приложений вполне нормально предоставлять значения среды прямо в mix.exs , настраивая функцию приложения:
1
2
3
4
5
6
7
8
9
|
def application do
[
extra_applications: [:logger],
mod: {Sample.Application, []},
env: [ # <====
key: :value
]
]
end
|
Пример: создание веб-интерфейса CalcServer
Хорошо, чтобы увидеть приложения в действии, давайте изменим пример, который уже обсуждался в моих статьях GenServer и Supervisors . Это простой калькулятор, который позволяет пользователям выполнять различные математические операции и получать результаты довольно легко.
То, что я хочу сделать, это сделать этот калькулятор на основе Интернета, чтобы мы могли отправлять POST-запросы для выполнения вычислений и GET-запрос для получения результата.
Создайте новый файл lib / calc_server.ex со следующим содержимым:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
defmodule Sample.CalcServer do
use GenServer
def start_link(initial_value) do
GenServer.start_link(__MODULE__, initial_value, name: __MODULE__)
end
def init(initial_value) when is_number(initial_value) do
{:ok, initial_value}
end
def init(_) do
{:stop, «The value must be an integer!»}
end
def add(number) do
GenServer.cast(__MODULE__, {:add, number})
end
def result do
GenServer.call(__MODULE__, :result)
end
def handle_call(:result, _, state) do
{:reply, state, state}
end
def handle_cast(operation, state) do
case operation do
{:add, number} -> {:noreply, state + number}
_ -> {:stop, «Not implemented», state}
end
end
def terminate(_reason, _state) do
IO.puts «The server terminated»
end
end
|
Мы только добавим поддержку операции add
. Все остальные математические операции могут быть введены таким же образом, поэтому я не буду перечислять их здесь, чтобы сделать код более компактным.
CalcServer
использует GenServer
, поэтому мы автоматически получаем child_spec
и можем запустить его из функции обратного вызова следующим образом:
1
2
3
4
5
6
7
8
|
def start(_type, _args) do
children = [
{Sample.CalcServer, 0}
]
opts = [strategy: :one_for_one, name: Sample.Supervisor]
Supervisor.start_link(children, opts)
end
|
0
вот начальный результат. Это должен быть номер, иначе CalcServer
немедленно прекратит работу.
Теперь вопрос в том, как добавить веб-поддержку? Для этого нам понадобятся две сторонние зависимости: Plug , который будет действовать как библиотека абстракций, и Cowboy , который будет действовать как настоящий веб-сервер. Конечно, нам нужно указать эти зависимости внутри файла mix.exs :
1
2
3
4
5
6
|
defp deps do
[
{:cowboy, «~> 1.1»},
{:plug, «~> 1.4»}
]
end
|
Теперь мы можем запустить приложение Plug под нашим собственным деревом контроля. Настройте функцию запуска следующим образом:
1
2
3
4
5
6
7
8
9
|
def start(_type, _args) do
children = [
Plug.Adapters.Cowboy.child_spec(
:http, Sample.Router, [], [port: Application.fetch_env!(:sample, :port)]
),
{Sample.CalcServer, 0}
]
# …
end
|
Здесь мы предоставляем child_spec
и устанавливаем Sample.Router
для ответа на запросы. Этот модуль будет создан в ближайшее время. Что мне не нравится в этой настройке, так это то, что номер порта жестко запрограммирован, что не очень удобно. Я мог бы захотеть настроить его при запуске приложения, поэтому давайте вместо этого сохраним его в среде:
1
2
3
|
Plug.Adapters.Cowboy.child_spec(
:http, Sample.Router, [], [port: Application.fetch_env!(:sample, :port)]
)
|
Теперь укажите значение порта по умолчанию в файле config.exs :
1
|
config :sample, port: 8088
|
Большой!
Как насчет роутера? Создайте новый файл lib / router.ex со следующим содержимым:
1
2
3
4
5
|
defmodule Sample.Router do
use Plug.Router
plug :match
plug :dispatch
end
|
Теперь нам нужно определить пару маршрутов, чтобы выполнить сложение и получить результат:
1
2
3
4
5
6
7
8
|
get «/result» do
conn |> ok(to_string(Sample.CalcServer.result))
end
post «/add» do
fetch_number(conn) |> Sample.CalcServer.add
conn |> ok
end
|
Мы используем макросы get
и post
для определения маршрутов /result
и /add
. Эти макросы установят объект conn
для нас.
ok
и fetch_number
— это закрытые функции, определенные следующим образом:
1
2
3
4
5
6
7
8
|
defp fetch_number(conn) do
Plug.Conn.fetch_query_params(conn).params[«number»] |>
String.to_integer
end
defp ok(conn, data \\ «OK») do
send_resp conn, 200, data
end
|
fetch_query_params/2
возвращает объект со всеми параметрами запроса. Нас интересует только номер, который пользователь отправляет нам. Все параметры изначально являются строками, поэтому нам нужно преобразовать их в целое число.
send_resp/3
отправляет клиенту ответ с указанным кодом состояния и телом. Мы не будем выполнять проверку ошибок здесь, поэтому код всегда будет 200
, что означает, что все в порядке.
И это все! Теперь вы можете запустить приложение любым из перечисленных выше способов (например, набрав iex -S mix
) и использовать инструмент curl
для выполнения запросов:
1
2
3
4
5
6
7
8
|
curl http://localhost:8088/result
# => 0
curl http://localhost:8088/add?number=1 -X POST
# => OK
curl http://localhost:8088/result
# => 1
|
Вывод
В этой статье мы обсудили приложения Elixir и их назначение. Вы узнали, как создавать приложения, предоставлять различную информацию и перечислять зависимости внутри файла mix.exs . Вы также узнали, как хранить конфигурацию в среде приложения, и узнали о нескольких способах запуска приложения. Наконец, мы увидели приложения в действии и создали простой веб-калькулятор.
Не забывайте, что на веб- сайте hex.pm перечислены многие сотни сторонних приложений, готовых для использования в ваших проектах, поэтому обязательно просмотрите каталог и выберите подходящее решение!
Надеюсь, вы нашли эту статью полезной и интересной. Я благодарю вас за то, что вы остались со мной и до следующего раза.